diff --git a/dinky-admin/pom.xml b/dinky-admin/pom.xml index 0621004116..73df3f60ba 100644 --- a/dinky-admin/pom.xml +++ b/dinky-admin/pom.xml @@ -32,7 +32,11 @@ provided + + 4.2.0 42.5.1 + 4.0.1 + @@ -59,6 +63,44 @@ org.xerial sqlite-jdbc + + org.pac4j + pac4j-springboot + ${pac4j.version} + + + org.springframework.boot + spring-boot-starter-web + + + org.apache.logging.log4j + log4j-to-slf4j + + + org.apache.logging.log4j + log4j-api + + + + + org.pac4j + spring-webmvc-pac4j + ${spring-webmvc-pac4j.version} + + + org.springframework + spring-webmvc + + + org.springframework + spring-core + + + org.springframework + spring-aop + + + org.mitre.dsmiley.httpproxy smiley-http-proxy-servlet diff --git a/dinky-admin/src/main/java/org/dinky/configure/AppConfig.java b/dinky-admin/src/main/java/org/dinky/configure/AppConfig.java index a35c4062b1..35cecd0ea5 100644 --- a/dinky-admin/src/main/java/org/dinky/configure/AppConfig.java +++ b/dinky-admin/src/main/java/org/dinky/configure/AppConfig.java @@ -25,8 +25,17 @@ import java.util.Locale; +import org.pac4j.core.config.Config; +import org.pac4j.core.http.adapter.JEEHttpActionAdapter; +import org.pac4j.springframework.annotation.AnnotationConfig; +import org.pac4j.springframework.component.ComponentConfig; +import org.pac4j.springframework.web.SecurityInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -36,6 +45,7 @@ import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.stp.StpUtil; +import lombok.extern.slf4j.Slf4j; /** * AppConfiguration @@ -43,7 +53,15 @@ * @since 2021/11/28 19:35 */ @Configuration +@Slf4j +@Import({ComponentConfig.class, AnnotationConfig.class}) +@ComponentScan(basePackages = "org.pac4j.springframework.web") public class AppConfig implements WebMvcConfigurer { + @Autowired + private Config config; + + @Value("${sso.enabled:false}") + private boolean ssoEnabled; /** * Cookie * @@ -86,11 +104,22 @@ public void addInterceptors(InterceptorRegistry registry) { })) .addPathPatterns("/api/**", "/openapi/**") .excludePathPatterns( - "/api/login", "/api/ldap/ldapEnableStatus", "/download/**", "/druid/**", "/api/version"); - + "/api/sso/ssoEnableStatus", + "/api/login", + "/api/ldap/ldapEnableStatus", + "/download/**", + "/druid/**", + "/api/version"); + if (ssoEnabled) { + log.info("Load{}", config.getClients().getClients().get(0).getName()); + registry.addInterceptor(buildInterceptor( + config.getClients().getClients().get(0).getName())) + .addPathPatterns("/api/sso/login") + .addPathPatterns("/api/sso/token"); + } registry.addInterceptor(new TenantInterceptor()) .addPathPatterns("/api/**") - .excludePathPatterns("/api/login", "/api/ldap/ldapEnableStatus") + .excludePathPatterns("/api/sso/ssoEnableStatus", "/api/login", "/api/ldap/ldapEnableStatus") .addPathPatterns("/api/alertGroup/**") .addPathPatterns("/api/alertHistory/**") .addPathPatterns("/api/alertInstance/**") @@ -110,4 +139,8 @@ public void addInterceptors(InterceptorRegistry registry) { .addPathPatterns("/api/git/**") .addPathPatterns("/api/jar/*"); } + + private SecurityInterceptor buildInterceptor(final String client) { + return new SecurityInterceptor(config, client, JEEHttpActionAdapter.INSTANCE); + } } diff --git a/dinky-admin/src/main/java/org/dinky/controller/SsoController.java b/dinky-admin/src/main/java/org/dinky/controller/SsoController.java new file mode 100644 index 0000000000..3b63991f82 --- /dev/null +++ b/dinky-admin/src/main/java/org/dinky/controller/SsoController.java @@ -0,0 +1,111 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.dinky.controller; + +import org.dinky.data.dto.LoginDTO; +import org.dinky.data.dto.UserDTO; +import org.dinky.data.enums.Status; +import org.dinky.data.exception.AuthException; +import org.dinky.data.result.Result; +import org.dinky.service.UserService; + +import java.util.List; + +import javax.annotation.PostConstruct; + +import org.pac4j.core.config.Config; +import org.pac4j.core.profile.CommonProfile; +import org.pac4j.core.profile.ProfileManager; +import org.pac4j.springframework.web.CallbackController; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.RedirectView; + +import cn.dev33.satoken.annotation.SaIgnore; +import io.swagger.annotations.ApiOperation; +import lombok.NoArgsConstructor; + +/** + * @author 杨泽翰 + */ +@RestController +@NoArgsConstructor +@RequestMapping("/api/sso") +public class SsoController { + @Value("${sso.redirect}") + private String redirect; + + @Value("${sso.enabled:false}") + private Boolean ssoEnabled; + + @Value("${pac4j.properties.principalNameAttribute:#{null}}") + private String principalNameAttribute; + + @Autowired + private Config config; + + @Autowired + CallbackController callbackController; + + @Autowired + private ProfileManager profileManager; + + @Autowired + private UserService userService; + + @PostConstruct + protected void afterPropertiesSet() { + callbackController.setDefaultUrl(redirect); + callbackController.setConfig(config); + } + + @GetMapping("/token") + public Result ssoToken() throws AuthException { + if (!ssoEnabled) { + return Result.failed(Status.SINGLE_LOGIN_DISABLED); + } + List all = profileManager.getAll(true); + String username = all.get(0).getAttribute(principalNameAttribute).toString(); + if (username == null) { + throw new AuthException(Status.NOT_MATCHED_PRINCIPAL_NAME_ATTRIBUTE); + } + LoginDTO loginDTO = new LoginDTO(); + loginDTO.setUsername(username); + loginDTO.setSsoLogin(true); + return userService.loginUser(loginDTO); + } + + @GetMapping("/login") + public ModelAndView ssoLogin() { + RedirectView redirectView = new RedirectView(redirect); + return new ModelAndView(redirectView); + } + + @GetMapping("/ssoEnableStatus") + @SaIgnore + @ApiOperation("Get SSO enable status") + public Result ssoStatus() { + return Result.succeed(ssoEnabled); + } +} diff --git a/dinky-admin/src/main/java/org/dinky/data/dto/LoginDTO.java b/dinky-admin/src/main/java/org/dinky/data/dto/LoginDTO.java index 5794585d4a..125214b2af 100644 --- a/dinky-admin/src/main/java/org/dinky/data/dto/LoginDTO.java +++ b/dinky-admin/src/main/java/org/dinky/data/dto/LoginDTO.java @@ -19,6 +19,8 @@ package org.dinky.data.dto; +import org.dinky.data.enums.UserType; + import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Getter; @@ -48,4 +50,18 @@ public class LoginDTO { @ApiModelProperty(value = "ldapLogin", required = true, example = "false", dataType = "Boolean") private boolean ldapLogin; + + @ApiModelProperty(value = "ssoLogin", required = true, example = "false", dataType = "Boolean") + private boolean ssoLogin; + + public UserType getLoginType() { + if (isLdapLogin()) { + return UserType.LDAP; + } + if (isSsoLogin()) { + return UserType.SSO; + } + + return UserType.LOCAL; + } } diff --git a/dinky-admin/src/main/java/org/dinky/service/impl/UserServiceImpl.java b/dinky-admin/src/main/java/org/dinky/service/impl/UserServiceImpl.java index e280b9f510..4487703f41 100644 --- a/dinky-admin/src/main/java/org/dinky/service/impl/UserServiceImpl.java +++ b/dinky-admin/src/main/java/org/dinky/service/impl/UserServiceImpl.java @@ -63,6 +63,7 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; +import org.pac4j.core.profile.ProfileManager; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -86,6 +87,7 @@ @RequiredArgsConstructor @Slf4j public class UserServiceImpl extends SuperServiceImpl implements UserService { + private final ProfileManager profileManager; private static final String DEFAULT_PASSWORD = "123456"; @@ -177,9 +179,19 @@ public Boolean removeUser(Integer id) { @Override public Result loginUser(LoginDTO loginDTO) { User user = null; + try { - // Determine the login method (LDAP or local) based on the flag in loginDTO - user = loginDTO.isLdapLogin() ? ldapLogin(loginDTO) : localLogin(loginDTO); + + switch (loginDTO.getLoginType()) { + case LDAP: + user = ldapLogin(loginDTO); + break; + case SSO: + user = ssoLogin(loginDTO); + break; + default: + user = localLogin(loginDTO); + } } catch (AuthException e) { // Handle authentication exceptions and return the corresponding error status return Result.authorizeFailed(e.getStatus()); @@ -210,6 +222,36 @@ public Result loginUser(LoginDTO loginDTO) { return Result.succeed(userInfo, Status.LOGIN_SUCCESS); } + private User ssoLogin(LoginDTO loginDTO) throws AuthException { + // Get user from local database by username + User user = getUserByUsername(loginDTO.getUsername()); + if (Asserts.isNull(user)) { + // User doesn't exist,Create a user + // Get default tenant from system configuration + String defaultTeantCode = + SystemConfiguration.getInstances().getLdapDefaultTeant().getValue(); + Tenant tenant = tenantService.getTenantByTenantCode(defaultTeantCode); + User userForm = new User(); + userForm.setUsername(loginDTO.getUsername()); + userForm.setNickname(loginDTO.getUsername()); + userForm.setUserType(UserType.SSO.getCode()); + userForm.setEnabled(true); + userForm.setSuperAdminFlag(false); + userForm.setIsDelete(false); + this.getBaseMapper().insert(userForm); + // Assign the user to the default tenant + List userIds = getUserIdsByTenantId(tenant.getId()); + userIds.add(userForm.getId()); + tenantService.assignUserToTenant(new AssignUserToTenantDTO(tenant.getId(), userIds)); + return userForm; + } else { + if (user.getUserType() != UserType.SSO.getCode()) { + throw new AuthException(Status.USER_TYPE_ERROR); + } + } + return user; + } + private void upsertToken(UserDTO userInfo) { Integer userId = userInfo.getUser().getId(); SysToken sysToken = new SysToken(); @@ -443,6 +485,10 @@ public void buildRowPermission() { @Override public void outLogin() { + if (profileManager != null) { + profileManager.logout(); + } + StpUtil.logout(StpUtil.getLoginIdAsInt()); } diff --git a/dinky-admin/src/main/resources/application-mysql.yml b/dinky-admin/src/main/resources/application-mysql.yml index 6c71564216..d0ac8fda40 100644 --- a/dinky-admin/src/main/resources/application-mysql.yml +++ b/dinky-admin/src/main/resources/application-mysql.yml @@ -20,4 +20,4 @@ spring: url: jdbc:mysql://${MYSQL_ADDR:127.0.0.1:3306}/${MYSQL_DATABASE:dinky}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: ${MYSQL_USERNAME:dinky} password: ${MYSQL_PASSWORD:dinky} - driver-class-name: com.mysql.cj.jdbc.Driver + driver-class-name: com.mysql.cj.jdbc.Driver \ No newline at end of file diff --git a/dinky-admin/src/main/resources/application.yml b/dinky-admin/src/main/resources/application.yml index a8c537a333..d11263c1c8 100644 --- a/dinky-admin/src/main/resources/application.yml +++ b/dinky-admin/src/main/resources/application.yml @@ -165,3 +165,28 @@ knife4j: crypto: enabled: false encryption-password: + +--- +################################################################################################################# +################################################# SSO Config #################################################### +################################################################################################################# +#see https://github.com/pac4j/spring-webmvc-pac4j-boot-demo/blob/master/src/main/resources/application.properties +sso: + enabled: false #enable sso default false + redirect: http://localhost:8000/#/user/login?from=sso #Front-end address + +--- +################################################################################################################# +################################################# pac4j Config #################################################### +################################################################################################################# +pac4j: + callbackUrl: http://localhost:${server.port}/callback # The callback URL + # Put all parameters under `properties` + # Check supported sso config parameters for different authentication clients from the below link + # https://github.com/pac4j/pac4j/blob/master/documentation/docs/config-module.md + properties: + principalNameAttribute: login #Authenticate user principal + github.id: + github.secret: + + diff --git a/dinky-common/src/main/java/org/dinky/data/enums/Status.java b/dinky-common/src/main/java/org/dinky/data/enums/Status.java index e9d2cee6f5..d96ec6f804 100644 --- a/dinky-common/src/main/java/org/dinky/data/enums/Status.java +++ b/dinky-common/src/main/java/org/dinky/data/enums/Status.java @@ -255,6 +255,12 @@ public enum Status { DS_TASK_TYPE_NOT_SUPPORT(17008, "ds.task.type.not.support"), DS_WORK_FLOW_DEFINITION_NOT_EXIST(17009, "ds.work.flow.definition.not.exist"), DS_PROCESS_DEFINITION_UPDATE(17010, "ds.work.flow.definition.process.update"), + /** + * SSO About * + */ + USER_TYPE_ERROR(22001, "sso.user.type.error"), + NOT_MATCHED_PRINCIPAL_NAME_ATTRIBUTE(22002, "sso.user.type.error"), + SINGLE_LOGIN_DISABLED(22003, "sso.not.enabled"), /** * LDAP About * @@ -461,6 +467,7 @@ public enum Status { SYS_FLINK_SETTINGS_FLINK_HISTORY_SERVER_ARCHIVE_REFRESH_INTERVAL_NOTE( 205, "sys.flink.settings.flinkHistoryServerArchiveRefreshInterval.note"), ; + private final int code; private final String key; diff --git a/dinky-common/src/main/java/org/dinky/data/enums/UserType.java b/dinky-common/src/main/java/org/dinky/data/enums/UserType.java index 68241c259c..50b0b7350e 100644 --- a/dinky-common/src/main/java/org/dinky/data/enums/UserType.java +++ b/dinky-common/src/main/java/org/dinky/data/enums/UserType.java @@ -21,7 +21,8 @@ public enum UserType { LDAP(1, "LDAP"), - LOCAL(0, "LOCAL"); + LOCAL(0, "LOCAL"), + SSO(2, "SSO"); private final int code; private final String type; diff --git a/dinky-web/src/locales/en-US/pages.ts b/dinky-web/src/locales/en-US/pages.ts index ac6bd8b55f..e9976ebfd1 100644 --- a/dinky-web/src/locales/en-US/pages.ts +++ b/dinky-web/src/locales/en-US/pages.ts @@ -365,6 +365,7 @@ export default { 'login.chooseTenantFailed': 'Tenant selection failed, please check. . . ', 'login.chooseTenantSuccess': '{msg}, Use [ {tenantCode} ] to enter the system, loading. . .', 'login.ldapLogin': 'LDAP Login', + 'login.ssoLogin': 'SSO Login', 'login.notbindtenant': 'You have not bound a tenant, please contact the administrator', 'login.password.placeholder': 'Password', 'login.password.required': 'Please input your password!', diff --git a/dinky-web/src/locales/zh-CN/pages.ts b/dinky-web/src/locales/zh-CN/pages.ts index 3f9080716c..2b2984830d 100644 --- a/dinky-web/src/locales/zh-CN/pages.ts +++ b/dinky-web/src/locales/zh-CN/pages.ts @@ -318,6 +318,7 @@ export default { 'login.chooseTenantFailed': '租户选择失败,请检查...', 'login.chooseTenantSuccess': '{msg},使用【 {tenantCode} 】进入系统,加载中...', 'login.ldapLogin': 'LDAP登录', + 'login.ssoLogin': 'SSO 登录', 'login.notbindtenant': '您还没有绑定租户,请联系管理员', 'login.password.placeholder': '密码', 'login.password.required': '密码是必填项!', diff --git a/dinky-web/src/pages/AuthCenter/User/components/constants.tsx b/dinky-web/src/pages/AuthCenter/User/components/constants.tsx index 71580b4be5..fb99ec40e4 100644 --- a/dinky-web/src/pages/AuthCenter/User/components/constants.tsx +++ b/dinky-web/src/pages/AuthCenter/User/components/constants.tsx @@ -20,5 +20,5 @@ export const UserType = { LOCAL: 0, LDAP: 1 }; export const USER_TYPE_ENUM = () => { - return { 0: 'LOCAL', 1: 'LDAP' }; + return { 0: 'LOCAL', 1: 'LDAP',2:"SSO" }; }; diff --git a/dinky-web/src/pages/Other/Login/LoginForm/index.tsx b/dinky-web/src/pages/Other/Login/LoginForm/index.tsx index 8b05465ab6..31627bd200 100644 --- a/dinky-web/src/pages/Other/Login/LoginForm/index.tsx +++ b/dinky-web/src/pages/Other/Login/LoginForm/index.tsx @@ -40,8 +40,15 @@ const LoginForm: React.FC = (props) => { const [submitting, setSubmitting] = useState(false); const [ldapEnabled, setLdapEnabled] = useState(false); + const [ssoEnabled, setSsoEnabled] = useState(false); useEffect(() => { + getData(API_CONSTANTS.GET_SSO_ENABLE).then( + (res) => { + setSsoEnabled(res.data); + }, + (err) => console.error(err) + ); getData(API_CONSTANTS.GET_LDAP_ENABLE).then( (res) => { setLdapEnabled(res.data); @@ -98,6 +105,11 @@ const LoginForm: React.FC = (props) => { {l('login.ldapLogin')} + + + ); @@ -225,3 +237,4 @@ const LoginForm: React.FC = (props) => { }; export default LoginForm; + diff --git a/dinky-web/src/pages/Other/Login/index.tsx b/dinky-web/src/pages/Other/Login/index.tsx index 4a0ee89c74..0ebd4983fd 100644 --- a/dinky-web/src/pages/Other/Login/index.tsx +++ b/dinky-web/src/pages/Other/Login/index.tsx @@ -21,7 +21,7 @@ import Footer from '@/components/Footer'; import ChooseModal from '@/pages/Other/Login/ChooseModal'; import { gotoRedirectUrl, initSomeThing, redirectToLogin } from '@/pages/Other/Login/function'; import LangSwitch from '@/pages/Other/Login/LangSwitch'; -import { chooseTenantSubmit, login, queryDataByParams } from '@/services/BusinessCrud'; +import {chooseTenantSubmit, login, queryDataByParams, ssoToken} from '@/services/BusinessCrud'; import { API } from '@/services/data'; import { API_CONSTANTS } from '@/services/endpoints'; import { SaTokenInfo, UserBaseInfo } from '@/types/AuthCenter/data.d'; @@ -35,6 +35,8 @@ import React, { useEffect, useState } from 'react'; import HelmetTitle from './HelmetTitle'; import LoginForm from './LoginForm'; import { TOKEN_KEY } from '@/services/constants'; +import {getData} from "@/services/api"; +import {createSearchParams} from "@@/exports"; const Login: React.FC = () => { const [submitting, setSubmitting] = useState(false); @@ -51,6 +53,32 @@ const Login: React.FC = () => { height: '100%' }; }); + useEffect(() => { + if (location.hash==("#/user/login?from=sso")){ + ssoToken().then( + res => { + if (res) { + setLocalStorageOfToken(JSON.stringify(res)); + } else { + // 如果没有获取到token信息,直接跳转到登录页 + redirectToLogin(); + } + setInitialState((s) => ({ ...s, currentUser: res.data })); + SuccessMessageAsync(l('login.result', '', { msg: res.msg, time: res.time })); + const tenantList: UserBaseInfo.Tenant[] = res.data.tenantList; + assertTenant(tenantList); + if (tenantList && tenantList.length > 1) { + + handleTenantVisible(true); + } else { + singleTenant(tenantList); + } + } + ) + } + + + }, []); const fetchUserInfo = async () => { const userInfo = await initialState?.fetchUserInfo?.(); diff --git a/dinky-web/src/services/BusinessCrud.ts b/dinky-web/src/services/BusinessCrud.ts index d1131f64a7..757c7d5d51 100644 --- a/dinky-web/src/services/BusinessCrud.ts +++ b/dinky-web/src/services/BusinessCrud.ts @@ -65,6 +65,20 @@ export async function login(body: API.LoginParams, options?: { [key: string]: an ...(options ?? {}) }); } +/** + * user sso login + */ +export async function ssoToken() { + return request(API_CONSTANTS.SSO_TOKEN, { + method: METHOD_CONSTANTS.GET, + headers: { + CONTENT_TYPE: APPLICATION_JSON + } + }); +} + + + /** * choose tenant diff --git a/dinky-web/src/services/endpoints.tsx b/dinky-web/src/services/endpoints.tsx index b42731dbd5..d1110834ea 100644 --- a/dinky-web/src/services/endpoints.tsx +++ b/dinky-web/src/services/endpoints.tsx @@ -231,6 +231,10 @@ export enum API_CONSTANTS { LDAP_LIST_USER = '/api/ldap/listUser', LDAP_IMPORT_USERS = '/api/ldap/importUsers', + // ----------------------------------------- sso ------------------------------------ + GET_SSO_ENABLE = '/api/sso/ssoEnableStatus', + SSO_TOKEN = '/api/sso/token', + SSO_LOGIN = '/api/sso/login', // ------------------------------------ home ------------------------------------ GET_RESOURCE_OVERVIEW = '/api/home/getResourceOverview', GET_JOB_STATUS_OVERVIEW = '/api/home/getJobStatusOverview',