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) {