diff --git a/config/application-config.yml.example b/config/application-config.yml.example
index ff2f40b29..411b5a5f5 100644
--- a/config/application-config.yml.example
+++ b/config/application-config.yml.example
@@ -6,6 +6,31 @@ spring:
username: { USERNAME }
password: { PASSWORD }
+# security:
+# oauth2:
+# enable: true
+# client:
+# registration:
+# cas:
+# provider: cas
+# client-id: "xxxxx"
+# client-name: "Sign in with CAS"
+# client-secret: "xxx"
+# authorization-grant-type: authorization_code
+# client-authentication-method: post
+# redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
+# scope: userinfo
+# provider:
+# cas:
+# authorization-uri: https://cas.xxx.com/cas/oauth2.0/authorize
+# token-uri: https://cas.xxx.com/cas/oauth2.0/accessToken
+# user-info-uri: https://cas.xxx.com/cas/oauth2.0/profile
+# user-name-attribute: id
+# userMapping:
+# email: "attributes.email"
+# name: "attributes.name"
+# avatar: "attributes.avatar"
+
# mail config
# mail:
@@ -38,10 +63,10 @@ spring:
server:
port: { PORT }
address: { IP }
-
+
# 开启 gzip 压缩,加快请求和响应速度
compression:
- enabled: true
+ enabled: true
mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/*
diff --git a/core/src/main/java/datart/core/common/Application.java b/core/src/main/java/datart/core/common/Application.java
index 70e510a55..be0437c88 100644
--- a/core/src/main/java/datart/core/common/Application.java
+++ b/core/src/main/java/datart/core/common/Application.java
@@ -71,7 +71,7 @@ public static String getWebRootURL() {
}
public static String getApiPrefix() {
- return getProperty("datart.path-prefix");
+ return getProperty("datart.server.path-prefix");
}
public static String getTokenSecret() {
diff --git a/frontend/src/app/pages/LoginPage/LoginForm.tsx b/frontend/src/app/pages/LoginPage/LoginForm.tsx
index b0a389cc2..b1150a02a 100644
--- a/frontend/src/app/pages/LoginPage/LoginForm.tsx
+++ b/frontend/src/app/pages/LoginPage/LoginForm.tsx
@@ -16,21 +16,22 @@
* limitations under the License.
*/
-import { Button, Form, Input } from 'antd';
-import { AuthForm } from 'app/components';
+import {Button, Form, Input} from 'antd';
+import {AuthForm} from 'app/components';
import usePrefixI18N from 'app/hooks/useI18NPrefix';
-import { selectLoggedInUser, selectLoginLoading } from 'app/slice/selectors';
-import { login } from 'app/slice/thunks';
-import React, { useCallback, useState } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
-import { Link, useHistory } from 'react-router-dom';
+import {selectLoggedInUser, selectLoginLoading, selectOauth2Clients, selectVersion} from 'app/slice/selectors';
+import {getOauth2Clients, login, tryOauth} from 'app/slice/thunks';
+import React, {useCallback, useEffect, useState} from 'react';
+import {useDispatch, useSelector} from 'react-redux';
+import {Link, useHistory} from 'react-router-dom';
import styled from 'styled-components/macro';
import {
BORDER_RADIUS,
LINE_HEIGHT_ICON_LG,
SPACE_MD,
} from 'styles/StyleConstants';
-import { getToken } from 'utils/auth';
+import {getToken} from 'utils/auth';
+import {editDashBoardInfoActions} from "../DashBoardPage/pages/BoardEditor/slice";
export function LoginForm() {
const [switchUser, setSwitchUser] = useState(false);
@@ -42,11 +43,24 @@ export function LoginForm() {
const logged = !!getToken();
const t = usePrefixI18N('login');
const tg = usePrefixI18N('global');
+ const oauth2Clients = useSelector(selectOauth2Clients);
const toApp = useCallback(() => {
history.replace('/');
}, [history]);
+ useEffect(() => {
+ dispatch(
+ getOauth2Clients(),
+ );
+ }, [dispatch]);
+
+ useEffect(() => {
+ dispatch(
+ tryOauth(),
+ );
+ }, [dispatch]);
+
const onLogin = useCallback(
values => {
dispatch(
@@ -65,6 +79,10 @@ export function LoginForm() {
setSwitchUser(true);
}, []);
+ let Oauth2BtnList = oauth2Clients.map((client) => {
+ return ({client.name})
+ });
+
return (
{logged && !switchUser ? (
@@ -89,7 +107,7 @@ export function LoginForm() {
},
]}
>
-
+
-
+
{() => (
@@ -112,7 +130,7 @@ export function LoginForm() {
disabled={
loading ||
// !form.isFieldsTouched(true) ||
- !!form.getFieldsError().filter(({ errors }) => errors.length)
+ !!form.getFieldsError().filter(({errors}) => errors.length)
.length
}
block
@@ -125,6 +143,7 @@ export function LoginForm() {
{t('forgotPassword')}
{t('register')}
+ {Oauth2BtnList}
)}
@@ -135,6 +154,16 @@ const Links = styled.div`
display: flex;
`;
+const Oauth2Button = styled.a`
+display: block;
+background-color: blue;
+text-align: center;
+color: #fff;
+font-weight: bold;
+line-height: 36px;
+height: 36px;
+`;
+
const LinkButton = styled(Link)`
flex: 1;
line-height: ${LINE_HEIGHT_ICON_LG};
diff --git a/frontend/src/app/slice/index.ts b/frontend/src/app/slice/index.ts
index afb850a50..74be09c30 100644
--- a/frontend/src/app/slice/index.ts
+++ b/frontend/src/app/slice/index.ts
@@ -19,6 +19,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { useInjectReducer } from 'utils/@reduxjs/injectReducer';
import {
+ getOauth2Clients,
getSystemInfo,
getUserInfoByToken,
login,
@@ -37,6 +38,7 @@ export const initialState: AppState = {
registerLoading: false,
saveProfileLoading: false,
modifyPasswordLoading: false,
+ oauth2Clients:[],
};
const slice = createSlice({
@@ -116,6 +118,13 @@ const slice = createSlice({
builder.addCase(getSystemInfo.fulfilled, (state, action) => {
state.systemInfo = action.payload;
});
+
+ builder.addCase(getOauth2Clients.fulfilled, (state, action) => {
+ state.oauth2Clients =action.payload.map(x=>({
+ name:Object.keys(x)[0],
+ value:x[Object.keys(x)[0]]
+ }));
+ });
},
});
diff --git a/frontend/src/app/slice/selectors.ts b/frontend/src/app/slice/selectors.ts
index 523febb24..d87da95f1 100644
--- a/frontend/src/app/slice/selectors.ts
+++ b/frontend/src/app/slice/selectors.ts
@@ -56,3 +56,8 @@ export const selectModifyPasswordLoading = createSelector(
[selectDomain],
appState => appState.modifyPasswordLoading,
);
+
+export const selectOauth2Clients = createSelector(
+ [selectDomain],
+ appState => appState.oauth2Clients,
+);
diff --git a/frontend/src/app/slice/thunks.ts b/frontend/src/app/slice/thunks.ts
index 7cfd30842..54fd00a30 100644
--- a/frontend/src/app/slice/thunks.ts
+++ b/frontend/src/app/slice/thunks.ts
@@ -16,12 +16,12 @@
* limitations under the License.
*/
-import { createAsyncThunk } from '@reduxjs/toolkit';
-import { StorageKeys } from 'globalConstants';
-import { removeToken, setToken, setTokenExpiration } from 'utils/auth';
-import { request } from 'utils/request';
-import { errorHandle } from 'utils/utils';
-import { appActions } from '.';
+import {createAsyncThunk} from '@reduxjs/toolkit';
+import {StorageKeys} from 'globalConstants';
+import {removeToken, setToken, setTokenExpiration} from 'utils/auth';
+import {request} from 'utils/request';
+import {errorHandle} from 'utils/utils';
+import {appActions} from '.';
import {
LoginParams,
LogoutParams,
@@ -35,9 +35,9 @@ import {
export const login = createAsyncThunk(
'app/login',
- async ({ params, resolve }) => {
+ async ({params, resolve}) => {
try {
- const { data } = await request({
+ const {data} = await request({
url: '/users/login',
method: 'POST',
data: params,
@@ -54,10 +54,10 @@ export const login = createAsyncThunk(
export const getUserInfoByToken = createAsyncThunk(
'app/getUserInfoByToken',
- async ({ token, resolve }) => {
+ async ({token, resolve}) => {
setToken(token);
try {
- const { data } = await request({
+ const {data} = await request({
url: '/users',
method: 'GET',
});
@@ -74,7 +74,7 @@ export const getUserInfoByToken = createAsyncThunk(
export const register = createAsyncThunk(
'app/register',
- async ({ data, resolve }) => {
+ async ({data, resolve}) => {
try {
await request({
url: '/users/register',
@@ -120,7 +120,7 @@ export const logout = createAsyncThunk(
export const updateUser = createAsyncThunk(
'app/updateUser',
- async (user, { dispatch }) => {
+ async (user, {dispatch}) => {
try {
localStorage.setItem(StorageKeys.LoggedInUser, JSON.stringify(user));
dispatch(appActions.updateUser(user));
@@ -134,9 +134,9 @@ export const updateUser = createAsyncThunk(
export const saveProfile = createAsyncThunk(
'app/saveProfile',
- async ({ user, resolve }) => {
+ async ({user, resolve}) => {
const loggedInUser = localStorage.getItem(StorageKeys.LoggedInUser) || '{}';
- const merged = { ...JSON.parse(loggedInUser), ...user };
+ const merged = {...JSON.parse(loggedInUser), ...user};
try {
await request({
url: '/users',
@@ -153,10 +153,8 @@ export const saveProfile = createAsyncThunk(
},
);
-export const modifyAccountPassword = createAsyncThunk<
- void,
- ModifyPasswordParams
->('app/modifyAccountPassword', async ({ params, resolve }) => {
+export const modifyAccountPassword = createAsyncThunk('app/modifyAccountPassword', async ({params, resolve}) => {
try {
await request({
url: '/users/change/password',
@@ -174,7 +172,7 @@ export const getSystemInfo = createAsyncThunk(
'app/getSystemInfo',
async () => {
try {
- const { data } = await request('/sys/info');
+ const {data} = await request('/sys/info');
// minute -> millisecond
const tokenTimeout = Number(data.tokenTimeout) * 60 * 1000;
setTokenExpiration(tokenTimeout);
@@ -185,3 +183,39 @@ export const getSystemInfo = createAsyncThunk(
}
},
);
+
+export const getOauth2Clients = createAsyncThunk<[]>(
+ 'app/getOauth2Clients',
+ async () => {
+ try {
+ const {data} = await request<[]>({
+ url: '/tpa/getOauth2Clients',
+ method: 'GET'
+ });
+ return data;
+ } catch (error) {
+ errorHandle(error);
+ throw error;
+ }
+ },
+);
+
+export const tryOauth = createAsyncThunk(
+ 'app/tryOauth',
+ async () => {
+ try {
+ const {data} = await request({
+ url: '/tpa/oauth2login',
+ method: 'POST'
+ });
+ localStorage.setItem(StorageKeys.LoggedInUser, JSON.stringify(data));
+ setTimeout(() => {
+ window.location.href = '/';
+ });
+ return data;
+ } catch (error) {
+ errorHandle(error);
+ throw error;
+ }
+ },
+);
diff --git a/frontend/src/app/slice/types.ts b/frontend/src/app/slice/types.ts
index 0b56e48c6..26457da6a 100644
--- a/frontend/src/app/slice/types.ts
+++ b/frontend/src/app/slice/types.ts
@@ -22,6 +22,7 @@ export interface AppState {
registerLoading: boolean;
saveProfileLoading: boolean;
modifyPasswordLoading: boolean;
+ oauth2Clients: Array<{name:string,value:string}>;
}
export interface User {
diff --git a/server/pom.xml b/server/pom.xml
index 8b8708e5f..4e79168d3 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -66,6 +66,17 @@
spring-boot-starter-data-redis
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-client
+ 2.6.3
+
+
+ com.jayway.jsonpath
+ json-path
+ 2.7.0
+
+
datart
datart-core
@@ -246,6 +257,9 @@
bootstrap
--registry=https://registry.npm.taobao.org
+
+ --openssl-legacy-provider
+
${project.parent.basedir}/frontend
@@ -262,6 +276,9 @@
run
build:all
+
+ --openssl-legacy-provider
+
${project.parent.basedir}/frontend
diff --git a/server/src/main/java/datart/server/config/WebSecurityConfig.java b/server/src/main/java/datart/server/config/WebSecurityConfig.java
new file mode 100644
index 000000000..c24f6e5a9
--- /dev/null
+++ b/server/src/main/java/datart/server/config/WebSecurityConfig.java
@@ -0,0 +1,29 @@
+package datart.server.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.builders.WebSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+
+import static datart.core.common.Application.getApiPrefix;
+
+@Configuration
+@EnableWebSecurity
+public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
+
+ @Override
+ public void configure(WebSecurity web) throws Exception {
+ web.ignoring().antMatchers(getApiPrefix() + "/tpa");
+ }
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http
+ .authorizeRequests()
+ .antMatchers(getApiPrefix() + "/tpa").permitAll()
+ .and().oauth2Login().loginPage("/")
+ .and().logout().logoutUrl("/tpa/oauth2/logout").permitAll()
+ .and().csrf().disable();
+ }
+}
diff --git a/server/src/main/java/datart/server/controller/ThirdPartyAuthController.java b/server/src/main/java/datart/server/controller/ThirdPartyAuthController.java
new file mode 100644
index 000000000..a1a7e1cb6
--- /dev/null
+++ b/server/src/main/java/datart/server/controller/ThirdPartyAuthController.java
@@ -0,0 +1,87 @@
+package datart.server.controller;
+
+import datart.core.base.annotations.SkipLogin;
+import datart.core.base.consts.Const;
+import datart.core.base.exception.Exceptions;
+import datart.core.entity.User;
+import datart.core.entity.ext.UserBaseInfo;
+import datart.security.base.PasswordToken;
+import datart.security.util.JwtUtils;
+import datart.server.base.dto.ResponseData;
+import datart.server.service.UserService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.authc.UsernamePasswordToken;
+import org.apache.shiro.subject.Subject;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+@Api
+@Slf4j
+@RestController
+@RequestMapping(value = "/tpa")
+public class ThirdPartyAuthController extends BaseController{
+
+ private final UserService userService;
+
+ public ThirdPartyAuthController(UserService userService) {
+ this.userService = userService;
+ }
+
+ @Autowired
+ private ClientRegistrationRepository clientRegistrationRepository;
+
+ @ApiOperation(value = "Get Oauth2 clents")
+ @GetMapping(value = "getOauth2Clients", consumes = MediaType.ALL_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @SkipLogin
+ public ResponseData>> getOauth2Clients(HttpServletRequest request) {
+
+ Iterable clientRegistrations = (Iterable) clientRegistrationRepository;
+ List> clients = new ArrayList<>();
+ clientRegistrations.forEach(registration -> {
+ HashMap map = new HashMap<>();
+ map.put(registration.getClientName(), OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/" + registration.getRegistrationId() + "?redirect_url=/");
+ clients.add(map);
+ });
+
+ return ResponseData.success(clients);
+ }
+
+ @ApiOperation(value = "External Login")
+ @SkipLogin
+ @PostMapping(value = "oauth2login", consumes = MediaType.ALL_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ public ResponseData externalLogin(Principal principal, HttpServletResponse response) {
+ if (null != principal && principal instanceof OAuth2AuthenticationToken) {
+ User user = userService.externalRegist((OAuth2AuthenticationToken) principal);
+ PasswordToken passwordToken = new PasswordToken(user.getUsername(),
+ null,
+ System.currentTimeMillis());
+
+ passwordToken.setPassword(user.getPassword());
+ String token= JwtUtils.toJwtString(passwordToken);
+ response.setHeader(Const.TOKEN, token);
+ response.setStatus(200);
+ return ResponseData.success(new UserBaseInfo(user));
+ }
+ response.setStatus(401);
+ return ResponseData.failure("oauth2登录失败");
+ }
+}
diff --git a/server/src/main/java/datart/server/service/UserService.java b/server/src/main/java/datart/server/service/UserService.java
index b36d4bff7..5322d480b 100644
--- a/server/src/main/java/datart/server/service/UserService.java
+++ b/server/src/main/java/datart/server/service/UserService.java
@@ -19,12 +19,14 @@
package datart.server.service;
import datart.core.base.consts.UserIdentityType;
+import datart.core.base.exception.ServerException;
import datart.core.entity.User;
import datart.core.entity.ext.UserBaseInfo;
import datart.core.mappers.ext.UserMapperExt;
import datart.security.base.PasswordToken;
import datart.server.base.dto.UserProfile;
import datart.server.base.params.*;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import javax.mail.MessagingException;
import java.io.UnsupportedEncodingException;
@@ -55,4 +57,6 @@ public interface UserService extends BaseCRUDService {
boolean resetPassword(UserResetPasswordParam passwordParam);
+ User externalRegist(OAuth2AuthenticationToken oauthAuthToken) throws ServerException;
+
}
diff --git a/server/src/main/java/datart/server/service/impl/UserServiceImpl.java b/server/src/main/java/datart/server/service/impl/UserServiceImpl.java
index ffc878bc7..acfc1e2ac 100644
--- a/server/src/main/java/datart/server/service/impl/UserServiceImpl.java
+++ b/server/src/main/java/datart/server/service/impl/UserServiceImpl.java
@@ -18,9 +18,10 @@
package datart.server.service.impl;
+import com.alibaba.fastjson.JSONObject;
+import com.jayway.jsonpath.JsonPath;
import datart.core.base.consts.UserIdentityType;
-import datart.core.base.exception.BaseException;
-import datart.core.base.exception.Exceptions;
+import datart.core.base.exception.*;
import datart.core.common.UUIDGenerator;
import datart.core.entity.Organization;
import datart.core.entity.User;
@@ -32,8 +33,6 @@
import datart.security.util.SecurityUtils;
import datart.server.base.dto.OrganizationBaseInfo;
import datart.server.base.dto.UserProfile;
-import datart.core.base.exception.NotFoundException;
-import datart.core.base.exception.ParamException;
import datart.server.base.params.ChangeUserPasswordParam;
import datart.server.base.params.UserRegisterParam;
import datart.server.base.params.UserResetPasswordParam;
@@ -46,6 +45,8 @@
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCrypt;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
+import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@@ -56,6 +57,8 @@
import java.util.List;
import java.util.stream.Collectors;
+import static datart.core.common.Application.getProperty;
+
@Service
@Slf4j
public class UserServiceImpl extends BaseService implements UserService {
@@ -294,6 +297,40 @@ public boolean resetPassword(UserResetPasswordParam passwordParam) {
return userMapper.updateByPrimaryKeySelective(update) == 1;
}
+ @Override
+ public User externalRegist(OAuth2AuthenticationToken oauthAuthToken) throws ServerException {
+ OAuth2User oauthUser = oauthAuthToken.getPrincipal();
+
+ User user = getUserByName(oauthUser.getName());
+ if (user != null) {
+ return user;
+ }
+ user = new User();
+
+ String emailMapping= getProperty(String.format("spring.security.oauth2.client.provider.%s.userMapping.email", oauthAuthToken.getAuthorizedClientRegistrationId()));
+ String nameMapping= getProperty(String.format("spring.security.oauth2.client.provider.%s.userMapping.name", oauthAuthToken.getAuthorizedClientRegistrationId()));
+ String avatarMapping= getProperty(String.format("spring.security.oauth2.client.provider.%s.userMapping.avatar", oauthAuthToken.getAuthorizedClientRegistrationId()));
+ JSONObject jsonObj=new JSONObject(oauthUser.getAttributes());
+
+ user.setId(UUIDGenerator.generate());
+ user.setCreateBy(user.getId());
+ user.setCreateTime(new Date());
+ user.setName(JsonPath.read(jsonObj,nameMapping));
+ user.setUsername(oauthUser.getName());
+ user.setActive(true);
+ //todo: oauth2登录后需要设置随机密码,此字段作为密文,显然无法对应原文,即不会有任何密码对应以下值
+ user.setPassword(BCrypt.hashpw("xxx", BCrypt.gensalt()));
+ user.setEmail(JsonPath.read(jsonObj,emailMapping));
+ user.setAvatar(JsonPath.read(jsonObj,avatarMapping));
+ int insert = userMapper.insert(user);
+ if (insert > 0) {
+ return user;
+ } else {
+ log.info("regist fail: {}", oauthUser.getName());
+ throw new ServerException("regist fail: unspecified error");
+ }
+ }
+
@Override
public void requirePermission(User entity, int permission) {