🎉 2.7.2.RELEASE 集成JustAuth支持第三方登录

This commit is contained in:
smallchill 2020-08-20 10:39:42 +08:00
parent 43a49063e2
commit d1a0f5d8e8
20 changed files with 446 additions and 48 deletions

View File

@ -1,9 +1,9 @@
<p align="center">
<img src="https://img.shields.io/badge/Release-V2.7.1-green.svg" alt="Downloads">
<img src="https://img.shields.io/badge/Release-V2.7.2-green.svg" alt="Downloads">
<img src="https://img.shields.io/badge/JDK-1.8+-green.svg" alt="Build Status">
<img src="https://img.shields.io/badge/license-Apache%202-blue.svg" alt="Build Status">
<img src="https://img.shields.io/badge/Spring%20Cloud-Hoxton.SR5-blue.svg" alt="Coverage Status">
<img src="https://img.shields.io/badge/Spring%20Boot-2.2.7.RELEASE-blue.svg" alt="Downloads">
<img src="https://img.shields.io/badge/Spring%20Cloud-Hoxton.SR7-blue.svg" alt="Coverage Status">
<img src="https://img.shields.io/badge/Spring%20Boot-2.2.9.RELEASE-blue.svg" alt="Downloads">
<a target="_blank" href="https://bladex.vip">
<img src="https://img.shields.io/badge/Author-Small%20Chill-ff69b4.svg" alt="Downloads">
</a>
@ -22,7 +22,7 @@
* 极简封装了多租户底层用更少的代码换来拓展性更强的SaaS多租户系统。
* 借鉴OAuth2实现了多终端认证系统可控制子系统的token权限互相隔离。
* 借鉴Security封装了Secure模块采用JWT做Token认证可拓展集成Redis等细颗粒度控制方案。
* 稳定生产了一年经历了从Camden -> Greenwich的技术架构也经历了从fat jar -> docker -> k8s + jenkins的部署架构
* 稳定生产了一年经历了从Camden -> Hoxton的技术架构也经历了从fat jar -> docker -> k8s + jenkins的部署架构
* 项目分包明确,规范微服务的开发模式,使包与包之间的分工清晰。
## 架构图
@ -59,7 +59,8 @@ SpringBlade
* 交流一群:`477853168`(满)
* 交流二群:`751253339`(满)
* 交流三群:`784729540`(满)
* 交流四群:`1034621754`
* 交流四群:`1034621754`(满)
* 交流五群:`946350912`
## 在线演示
* Saber-基于Vue[https://saber.bladex.vip](https://saber.bladex.vip)

View File

@ -15,7 +15,7 @@ export default [
path: '/',
component: '../layouts/BasicLayout',
Routes: ['src/pages/Authorized'],
authority: ['administrator', 'admin', 'user', 'test'],
authority: ['administrator', 'admin', 'user', 'test', 'guest'],
routes: [
// dashboard
{ path: '/', redirect: '/dashboard/workplace' },

View File

@ -1,13 +1,13 @@
{
"name": "sword",
"version": "2.7.0",
"version": "2.7.2",
"description": "An out-of-box UI solution for enterprise applications",
"private": true,
"scripts": {
"presite": "cd functions && npm install",
"start": "cross-env MOCK=none PORT=8888 umi dev",
"start:no-mock": "cross-env MOCK=none PORT=8888 umi dev",
"start:mock": "cross-env APP_TYPE=site PORT=8888 umi dev",
"start": "cross-env MOCK=none PORT=1888 umi dev",
"start:no-mock": "cross-env MOCK=none PORT=1888 umi dev",
"start:mock": "cross-env APP_TYPE=site PORT=1888 umi dev",
"build": "umi build",
"site": "npm run presite && cross-env APP_TYPE=site npm run build && firebase deploy && npm run docker:push",
"analyze": "cross-env ANALYZE=1 umi build",

View File

@ -47,7 +47,7 @@ class WrapFormItem extends Component {
refreshCaptcha = () => {
// 获取验证码
getCaptchaImage().then(resp => {
const {data} = resp;
const { data } = resp;
if (data.key) {
this.setState({ image: data.image });
setCaptchaKey(data.key);

View File

@ -0,0 +1,181 @@
import React, { PureComponent } from 'react';
import { connect } from 'dva';
import { Card, Col, Form, Input, Modal, Button, Row, message } from 'antd';
import styles from '@/layouts/Sword.less';
import { getCurrentUser, removeAll } from '@/utils/authority';
import { validateNull } from '@/utils/utils';
import { tenantMode } from '@/defaultSettings';
import { getUserInfo, registerGuest } from '@/services/user';
import router from 'umi/router';
const FormItem = Form.Item;
@connect(({ tenant }) => ({
tenant,
}))
@Form.create()
class ThirdRegister extends PureComponent {
state = {
loading: false,
visible: false,
user: {},
};
componentDidMount() {
const user = getCurrentUser();
if (validateNull(user) || validateNull(user.userId) || user.userId < 0) {
// 第三方注册用户,弹出注册框
this.setState({ visible: true, user });
} else {
// 获取用户信息,也可用于校验当前用户token是否有效
getUserInfo().then(resp => {
window.console.log(resp);
});
}
}
handleSubmit = e => {
e.preventDefault();
const { form } = this.props;
const user = getCurrentUser();
form.validateFieldsAndScroll((err, values) => {
if (!err) {
const password = form.getFieldValue('password');
const password2 = form.getFieldValue('password2');
if (password !== password2) {
message.warning('两次密码输入不一致');
} else {
registerGuest(values, user.oauthId).then(resp => {
if (resp.success) {
this.setState({ visible: false });
Modal.success({ content: '注册申请已提交,请耐心等待管理员通过!' });
removeAll();
router.push('/user/login');
}
form.resetFields();
});
}
}
});
};
render() {
const {
form,
} = this.props;
const { loading, visible, user } = this.state;
const { getFieldDecorator } = form;
const tenantVisible = tenantMode;
const formItemLayout = {
labelCol: {
span: 8,
},
wrapperCol: {
span: 16,
},
};
const formAllItemLayout = {
labelCol: {
span: 4,
},
wrapperCol: {
span: 20,
},
};
return (
<Modal
title="第三方系统用户注册"
width={800}
visible={visible}
closable={false}
footer={[
<Button key="submit" type="primary" loading={loading} onClick={this.handleSubmit}>
注册
</Button>,
]}
>
<Form style={{ marginTop: 8 }}>
<Card className={styles.card} bordered={false}>
{tenantVisible ? (
<Row gutter={24}>
<Col span={20}>
<FormItem {...formAllItemLayout} label="租户编号">
{getFieldDecorator('tenantId', {
rules: [
{
required: true,
message: '请输入租户编号',
},
],
})(<Input placeholder="请输入租户编号" />)}
</FormItem>
</Col>
</Row>
) : null}
<Row gutter={24}>
<Col span={10}>
<FormItem {...formItemLayout} label="用户姓名">
{getFieldDecorator('name', {
rules: [
{
required: true,
message: '请输入用户姓名',
},
],
initialValue: user.name,
})(<Input placeholder="请输入用户姓名" />)}
</FormItem>
</Col>
<Col span={10}>
<FormItem {...formItemLayout} label="账号名称">
{getFieldDecorator('account', {
rules: [
{
required: true,
message: '请输入账号名称',
},
],
initialValue: user.account,
})(<Input placeholder="请输入账号名称" />)}
</FormItem>
</Col>
</Row>
<Row gutter={24}>
<Col span={10}>
<FormItem {...formItemLayout} label="账号密码">
{getFieldDecorator('password', {
rules: [
{
required: true,
message: '请输入密码',
},
],
})(<Input placeholder="请输入账号密码" />)}
</FormItem>
</Col>
<Col span={10}>
<FormItem {...formItemLayout} label="账号密码">
{getFieldDecorator('password2', {
rules: [
{
required: true,
message: '请输入确认密码',
},
],
})(<Input placeholder="请确认账号密码" />)}
</FormItem>
</Col>
</Row>
</Card>
</Form>
</Modal>
);
}
}
export default ThirdRegister;

View File

@ -16,4 +16,6 @@ module.exports = {
disableLocal: false,
},
pwa: true,
// 第三方登陆授权地址
authUrl: 'http://localhost/blade-auth/oauth/render',
};

View File

@ -6,6 +6,7 @@ export default {
'app.login.message-invalid-verification-code': 'Invalid verification code',
'app.login.tab-login-credentials': 'Credentials',
'app.login.tab-login-mobile': 'Mobile number',
'app.login.tab-login-social': 'Social System',
'app.login.remember-me': 'Remember me',
'app.login.forgot-password': 'Forgot your password?',
'app.login.sign-in-with': 'Sign in with',

View File

@ -6,6 +6,7 @@ export default {
'app.login.message-invalid-verification-code': '验证码错误',
'app.login.tab-login-credentials': '账户密码登录',
'app.login.tab-login-mobile': '手机号登录',
'app.login.tab-login-social': '第三方系统登陆',
'app.login.remember-me': '自动登录',
'app.login.forgot-password': '忘记密码',
'app.login.sign-in-with': '其他登录方式',

View File

@ -6,6 +6,7 @@ export default {
'app.login.message-invalid-verification-code': '驗證碼錯誤',
'app.login.tab-login-credentials': '賬戶密碼登錄',
'app.login.tab-login-mobile': '手機號登錄',
'app.login.tab-login-social': '第三方系統登陸',
'app.login.remember-me': '自動登錄',
'app.login.forgot-password': '忘記密碼',
'app.login.sign-in-with': '其他登錄方式',

View File

@ -1,7 +1,8 @@
import { routerRedux } from 'dva/router';
import { notification } from 'antd';
import { stringify } from 'qs';
import { getFakeCaptcha } from '../services/api';
import { accountLogin } from '../services/user';
import { accountLogin, socialLogin } from '../services/user';
import { dynamicRoutes, dynamicButtons } from '../services/menu';
import {
setAuthority,
@ -14,6 +15,7 @@ import {
} from '../utils/authority';
import { getPageQuery, formatRoutes, formatButtons } from '../utils/utils';
import { reloadAuthorized } from '../utils/Authorized';
import { getTopUrl } from '../utils/utils';
export default {
namespace: 'login',
@ -63,7 +65,29 @@ export default {
yield put(routerRedux.replace(redirect || '/'));
}
},
*socialLogin({ payload }, { call, put }) {
const response = yield call(socialLogin, payload);
if (response.success) {
yield put({
type: 'changeLoginStatus',
payload: {
status: true,
type: 'login',
data: { ...response.data },
},
});
reloadAuthorized();
const topUrl = getTopUrl();
const redirectUrl = '/oauth/redirect/';
// eslint-disable-next-line prefer-destructuring
window.location.href = topUrl.split(redirectUrl)[0];
yield put(routerRedux.replace('/'));
} else {
notification.error({
message: response.msg,
});
}
},
*getCaptcha({ payload }, { call }) {
yield call(getFakeCaptcha, payload);
},
@ -95,16 +119,15 @@ export default {
reducers: {
changeLoginStatus(state, { payload }) {
const { status, type } = payload;
if (status) {
const {
data: { tokenType, accessToken, authority, account, userName, avatar },
data: { tokenType, accessToken, authority, account, userId, oauthId, userName, avatar },
} = payload;
const token = `${tokenType} ${accessToken}`;
setToken(token);
setAccessToken(accessToken);
setAuthority(authority);
setCurrentUser({ avatar, account, name: userName, authority });
setCurrentUser({ avatar, userId, oauthId, account, name: userName, authority });
} else {
removeAll();
}

View File

@ -236,11 +236,7 @@ class Region extends PureComponent {
const buttons = getButton('region');
const {
treeData,
treeCascader,
debugVisible,
} = this.state;
const { treeData, treeCascader, debugVisible } = this.state;
const formItemLayout = {
labelCol: {

View File

@ -2,7 +2,8 @@ import React, { PureComponent } from 'react';
import { Card, Col, Collapse, Row, Divider, Tag } from 'antd';
import styles from '../../layouts/Sword.less';
import PageHeaderWrapper from '@/components/PageHeaderWrapper';
import PageHeaderWrapper from '../../components/PageHeaderWrapper';
import ThirdRegister from '../../components/ThirdRegister';
const { Panel } = Collapse;
@ -11,6 +12,11 @@ class Workplace extends PureComponent {
return (
<PageHeaderWrapper>
<Card className={styles.card} bordered={false}>
<Row gutter={24}>
<Col span={24}>
<ThirdRegister />
</Col>
</Row>
<Row gutter={24}>
<Col span={24}>
<div style={{ textAlign: 'center' }}>
@ -211,13 +217,15 @@ class Workplace extends PureComponent {
<div>1.升级至 SpringCloud Hoxton.SR5</div>
<div>2.升级至 SpringBoot 2.2.7.RELEASE</div>
<div>3.升级至 Seata 1.2.0</div>
<div>4.升级至 FastJson 1.2.70</div>
<div>5.升级至 Avue 2.5.3</div>
<div>6.新增行政区划管理模块</div>
<div>7.优化用户导入的密码配置逻辑</div>
<div>8.优化INode结构支持懒加载数据格式</div>
<div>9.优化代码生成模板支持最新版Saber结构</div>
<div>10.修复Log模块在多线程异步场景下报错的问题</div>
<div>4.升级至 MybatisPlus 3.3.2</div>
<div>5.升级至 Kinfe4j 2.0.3</div>
<div>6.升级至 FastJson 1.2.70</div>
<div>7.升级至 Avue 2.5.3</div>
<div>8.新增行政区划管理模块</div>
<div>9.优化用户导入的密码配置逻辑</div>
<div>10.优化INode结构支持懒加载数据格式</div>
<div>11.优化代码生成模板支持最新版Saber结构</div>
<div>12.修复Log模块在多线程异步场景下报错的问题</div>
</Panel>
<Panel header="2.7.0发布 内核全面升级,增加岗位管理,用户导入导出" key="17">
<div>1.升级至 SpringCloud Hoxton.SR3</div>

View File

@ -1,10 +1,11 @@
import React, { Component } from 'react';
import { connect } from 'dva';
import { formatMessage, FormattedMessage } from 'umi/locale';
import { Checkbox, Alert } from 'antd';
import { Checkbox, Alert, Icon, Row, Col, Card, Spin } from 'antd';
import Login from '../../components/Login';
import styles from './Login.less';
import { tenantMode, captchaMode } from '../../defaultSettings';
import { tenantMode, captchaMode, authUrl } from '../../defaultSettings';
import { getQueryString, getTopUrl, validateNull } from '@/utils/utils';
const { Tab, TenantId, UserName, Password, Captcha, Submit } = Login;
@ -18,6 +19,36 @@ class LoginPage extends Component {
autoLogin: true,
};
componentDidMount() {
const domain = getTopUrl();
const redirectUrl = '/oauth/redirect/';
const {
dispatch,
route: { routes, authority },
} = this.props;
let source = getQueryString('source');
const code = getQueryString('code');
const state = getQueryString('state');
if (validateNull(source) && domain.includes(redirectUrl)) {
// eslint-disable-next-line prefer-destructuring
source = domain.split('?')[0];
// eslint-disable-next-line prefer-destructuring
source = source.split(redirectUrl)[1];
}
if (!validateNull(source) && !validateNull(code) && !validateNull(state)) {
dispatch({
type: 'login/socialLogin',
payload: { source, code, state, tenantId: '000000' },
});
} else {
dispatch({
type: 'menu/fetchMenuData',
payload: { routes, authority },
});
}
}
onTabChange = type => {
this.setState({ type });
};
@ -53,6 +84,10 @@ class LoginPage extends Component {
}
};
handleClick = source => {
window.location.href = `${authUrl}/${source}`;
};
changeAutoLogin = e => {
this.setState({
autoLogin: e.target.checked,
@ -65,7 +100,7 @@ class LoginPage extends Component {
render() {
const { login, submitting } = this.props;
const { type, autoLogin } = this.state;
const { type, autoLogin, loading } = this.state;
return (
<div className={styles.main}>
<Login
@ -119,6 +154,66 @@ class LoginPage extends Component {
/>
{captchaMode ? <Captcha name="code" mode="image" /> : null}
</Tab>
<Tab key="social" tab={formatMessage({ id: 'app.login.tab-login-social' })}>
<Card className={styles.card} bordered={false}>
<Row gutter={24} className={styles.iconPreview}>
<Col span={4} key="github">
<Icon
type="github"
theme="filled"
onClick={() => {
this.handleClick('github');
}}
/>
</Col>
<Col span={4} key="gitee">
<Icon
type="google-circle"
theme="filled"
onClick={() => {
this.handleClick('gitee');
}}
/>
</Col>
<Col span={4} key="wechat">
<Icon
type="wechat"
theme="filled"
onClick={() => {
this.handleClick('wechat_open');
}}
/>
</Col>
<Col span={4} key="dingtalk">
<Icon
type="dingtalk-circle"
theme="filled"
onClick={() => {
this.handleClick('dingtalk');
}}
/>
</Col>
<Col span={4} key="alipay">
<Icon
type="alipay-circle"
theme="filled"
onClick={() => {
this.handleClick('alipay');
}}
/>
</Col>
<Col span={4} key="taobao">
<Icon
type="taobao-circle"
theme="filled"
onClick={() => {
this.handleClick('taobao');
}}
/>
</Col>
</Row>
</Card>
</Tab>
<div>
<Checkbox checked={autoLogin} onChange={this.changeAutoLogin}>
<FormattedMessage id="app.login.remember-me" />

View File

@ -29,4 +29,14 @@
float: right;
}
}
.card {
margin-bottom: 24px;
}
.iconPreview {
font-size: 45px;
text-align: center;
cursor: pointer;
}
}

View File

@ -1,17 +1,6 @@
import React, { PureComponent } from 'react';
import { connect } from 'dva';
import {
Upload,
Icon,
Button,
Col,
Form,
Input,
message,
Modal,
Row,
Tree,
} from 'antd';
import { Upload, Icon, Button, Col, Form, Input, message, Modal, Row, Tree } from 'antd';
import Panel from '../../../components/Panel';
import Grid from '../../../components/Sword/Grid';
import { USER_INIT, USER_LIST, USER_ROLE_GRANT } from '../../../actions/user';
@ -249,7 +238,6 @@ class User extends PureComponent {
</div>
);
render() {
const code = 'user';
@ -269,7 +257,7 @@ class User extends PureComponent {
headers: {
'Blade-Auth': getToken(),
},
action: "/api/blade-user/import-user",
action: '/api/blade-user/import-user',
};
const formItemLayout = {

View File

@ -23,7 +23,7 @@ class UserEdit extends PureComponent {
params: { id },
},
} = this.props;
dispatch(USER_DETAIL(id)).then(()=>{
dispatch(USER_DETAIL(id)).then(() => {
const {
user: { detail },
} = this.props;
@ -227,7 +227,7 @@ class UserEdit extends PureComponent {
<Col span={10}>
<FormItem {...formItemLayout} label="用户编号">
{getFieldDecorator('code', {
initialValue: detail.code
initialValue: detail.code,
})(<Input placeholder="请输入用户编号" />)}
</FormItem>
</Col>

View File

@ -20,6 +20,25 @@ export async function accountLogin(params) {
});
}
export async function socialLogin(params) {
const values = params;
values.grantType = 'social';
values.scope = 'all';
return request('/api/blade-auth/token', {
method: 'POST',
body: func.toFormData(values),
});
}
export async function registerGuest(form, oauthId) {
const values = form;
values.oauthId = oauthId;
return request('/api/blade-user/register-guest', {
method: 'POST',
body: func.toFormData(values),
});
}
export async function query() {
return request('/api/users');
}

View File

@ -88,6 +88,9 @@ export default class Func {
* @returns {string}
*/
static split(str) {
if (String(str) === '-1') {
return null;
}
return str ? String(str).split(',') : '';
}
}

27
src/utils/getPageTitle.js Normal file
View File

@ -0,0 +1,27 @@
import { formatMessage } from 'umi/locale';
import pathToRegexp from 'path-to-regexp';
import isEqual from 'lodash/isEqual';
import memoizeOne from 'memoize-one';
import { menu, title } from '../defaultSettings';
export const matchParamsPath = (pathname, breadcrumbNameMap) => {
const pathKey = Object.keys(breadcrumbNameMap).find(key => pathToRegexp(key).test(pathname));
return breadcrumbNameMap[pathKey];
};
const getPageTitle = (pathname, breadcrumbNameMap) => {
const currRouterData = matchParamsPath(pathname, breadcrumbNameMap);
if (!currRouterData) {
return title;
}
const pageName = menu.disableLocal
? currRouterData.name
: formatMessage({
id: currRouterData.locale || currRouterData.name,
defaultMessage: currRouterData.name,
});
return `${pageName} - ${title}`;
};
export default memoizeOne(getPageTitle, isEqual);

View File

@ -212,3 +212,45 @@ export function formatButtons(buttons) {
};
});
}
/**
* 判断是否为空
*/
export function validateNull(val) {
if (typeof val === 'boolean') {
return false;
}
if (typeof val === 'number') {
return false;
}
if (val instanceof Array) {
if (val.length === 0) return true;
} else if (val instanceof Object) {
if (JSON.stringify(val) === '{}') return true;
} else {
if (val === 'null' || val == null || val === 'undefined' || val === undefined || val === '') {
return true;
}
return false;
}
return false;
}
/**
* 获取顶部地址栏地址
*/
export function getTopUrl() {
return window.location.href.split('/#/')[0];
}
/**
* 获取url参数
* @param name 参数名
*/
export function getQueryString(name) {
// eslint-disable-next-line no-shadow
const reg = new RegExp(`(^|&)${name}=([^&]*)(&|$)`, 'i');
const r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(decodeURI(r[2]));
return null;
}