From fc5d43e590ac5453f1e1f96fcf84f558f383ca40 Mon Sep 17 00:00:00 2001 From: baoshiwei <baoshiwei@shlanbao.cn> Date: 星期五, 21 三月 2025 14:18:21 +0800 Subject: [PATCH] feat(social): 添加 Keycloak单点登录功能 --- eims/ruoyi-admin/src/main/resources/application-dev.yml | 10 + eims-ui/apps/web-antd/src/views/_core/authentication/login.vue | 6 eims/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/keycloak/AuthKeycloakSource.java | 36 ++++++ eims/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysSocialService.java | 1 eims/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/keycloak/AuthKeycloakRequest.java | 144 ++++++++++++++++++++++++ eims/ruoyi-admin/src/main/resources/application-prod.yml | 10 + eims-ui/apps/web-antd/src/views/_core/social-callback/index.vue | 2 eims/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java | 44 +++++++ eims-ui/apps/web-antd/src/views/_core/oauth-common.ts | 10 + eims/ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java | 29 ++++ eims/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialLoginConfigProperties.java | 3 eims/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java | 2 eims/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysSocialServiceImpl.java | 7 + 13 files changed, 300 insertions(+), 4 deletions(-) diff --git a/eims-ui/apps/web-antd/src/views/_core/authentication/login.vue b/eims-ui/apps/web-antd/src/views/_core/authentication/login.vue index d68624c..4652260 100644 --- a/eims-ui/apps/web-antd/src/views/_core/authentication/login.vue +++ b/eims-ui/apps/web-antd/src/views/_core/authentication/login.vue @@ -8,7 +8,7 @@ import { omit } from 'lodash-es'; -import { tenantList, type TenantResp } from '#/api'; +import { authBinding, tenantList, type TenantResp } from '#/api'; import { captchaImage, type CaptchaResponse } from '#/api/core/captcha'; import { useAuthStore } from '#/store'; @@ -50,7 +50,11 @@ } onMounted(async () => { + // 鍚姩鍗曠偣鐧诲綍娉ㄩ噴鎺変笅杈硅繖涓�琛岋紝鍚﹀垯鏀惧紑 await Promise.all([loadCaptcha(), loadTenant()]); + // 鍚姩鍗曠偣鐧诲綍鏀惧紑涓嬭竟涓よ娉ㄩ噴锛屽惁鍒欐敞閲婃帀 + // const href = await authBinding('keycloak', '000000'); + // window.location.href = href; }); const formSchema = computed((): VbenFormSchema[] => { diff --git a/eims-ui/apps/web-antd/src/views/_core/oauth-common.ts b/eims-ui/apps/web-antd/src/views/_core/oauth-common.ts index 6627442..dc7d50f 100644 --- a/eims-ui/apps/web-antd/src/views/_core/oauth-common.ts +++ b/eims-ui/apps/web-antd/src/views/_core/oauth-common.ts @@ -59,6 +59,7 @@ * action涓嶄负绌虹殑浼氬湪鐧诲綍椤垫樉绀� */ export const accountBindList: BindItem[] = [ + { avatar: TaobaoIcon, color: '#ff4000', @@ -101,4 +102,13 @@ source: 'github', title: 'GITHUB', }, + { + action: () => handleAuthBinding('keycloak'), + avatar: TaobaoIcon, + color: '#ff4000', + description: 'keycloak鐧诲綍', + key: '6', + source: 'keycloak', + title: 'keycloak', + }, ]; diff --git a/eims-ui/apps/web-antd/src/views/_core/social-callback/index.vue b/eims-ui/apps/web-antd/src/views/_core/social-callback/index.vue index ad7135f..b343283 100644 --- a/eims-ui/apps/web-antd/src/views/_core/social-callback/index.vue +++ b/eims-ui/apps/web-antd/src/views/_core/social-callback/index.vue @@ -18,7 +18,7 @@ const state = route.query.state as string; const stateJson = JSON.parse(atob(state)); // 鏉ユ簮 -const source = route.query.source as string; +const source = 'keycloak'; // 绉熸埛ID const defaultTenantId = '000000'; const tenantId = (stateJson.tenantId as string) ?? defaultTenantId; diff --git a/eims/ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java b/eims/ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java index c7ad917..d99a2fd 100644 --- a/eims/ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java +++ b/eims/ruoyi-admin/src/main/java/org/dromara/web/service/SysLoginService.java @@ -6,6 +6,8 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Opt; import cn.hutool.core.util.ObjectUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; import com.baomidou.lock.annotation.Lock4j; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -48,6 +50,12 @@ @Slf4j @Service public class SysLoginService { + + @Value("${justauth.type.keycloak.server-url}") + private String keycloakServerUrl; + + @Value("${justauth.type.keycloak.realm}") + private String keycloakRealm; @Value("${user.password.maxRetryCount}") private Integer maxRetryCount; @@ -116,6 +124,27 @@ TenantHelper.clearDynamic(); } recordLogininfor(loginUser.getTenantId(), loginUser.getUsername(), Constants.LOGOUT, MessageUtils.message("user.logout.success")); + + // 鏂板Keycloak鐧诲嚭閫昏緫 + + Long userId = loginUser.getUserId(); + + SysSocialVo social = sysSocialService.selectByUserId(userId); + if (social == null) { + return; + } + + String logoutUrl = keycloakServerUrl + "/realms/" + keycloakRealm + "/protocol/openid-connect/logout"; + HttpRequest request = HttpRequest.get(logoutUrl) + .form("refresh_token", social.getRefreshToken()) + .form("id_token_hint", social.getIdToken()); + + + HttpResponse response = request.execute(); + if (response.isOk()) { + System.out.println("1234"); + } + } catch (NotLoginException ignored) { } finally { try { diff --git a/eims/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java b/eims/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java index 8463026..ef2caf8 100644 --- a/eims/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java +++ b/eims/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java @@ -7,6 +7,7 @@ import cn.hutool.core.util.ObjectUtil; import cn.hutool.http.HttpUtil; import cn.hutool.http.Method; +import com.aizuda.snailjob.common.core.constant.SystemConstants; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import me.zhyd.oauth.model.AuthResponse; @@ -23,6 +24,8 @@ import org.dromara.common.social.config.properties.SocialProperties; import org.dromara.common.social.utils.SocialUtils; import org.dromara.common.tenant.helper.TenantHelper; +import org.dromara.system.domain.SysUser; +import org.dromara.system.domain.bo.SysSocialBo; import org.dromara.system.domain.vo.SysClientVo; import org.dromara.system.domain.vo.SysSocialVo; import org.dromara.system.domain.vo.SysUserVo; @@ -77,6 +80,47 @@ .formStr(MapUtil.of("access_token", authUserData.getToken().getAccessToken())) .executeAsync(); } + if ("KEYCLOAK".equals(authUserData.getSource())) { + // 鏂板KEYCLOAK鐢ㄦ埛鑷姩鍒涘缓閫昏緫 + String authId = authUserData.getSource() + authUserData.getUuid(); + List<SysSocialVo> list = sysSocialService.selectByAuthId(authId); + if (CollUtil.isEmpty(list)) { + // 鑷姩鍒涘缓鏂扮敤鎴� + SysUser newUser = new SysUser(); + newUser.setUserName(authUserData.getUsername()); + newUser.setEmail(authUserData.getEmail()); + newUser.setNickName(authUserData.getNickname()); + newUser.setPassword("Initial123@"); // 鍒濆瀵嗙爜闇�绗﹀悎瀹夊叏绛栫暐 + newUser.setStatus("0"); + + userMapper.insert(newUser); // 鍋囪瀛樺湪鎻掑叆鏂规硶 + + // 鍒涘缓绀句氦缁戝畾璁板綍 + SysSocialBo newSocial = new SysSocialBo(); + newSocial.setUserId(newUser.getUserId()); + newSocial.setUserName(newUser.getUserName()); + newSocial.setAuthId(authId); + newSocial.setSource(authUserData.getSource()); + newSocial.setTenantId(newUser.getTenantId()); + newSocial.setOpenId(authUserData.getUuid()); + newSocial.setAccessToken(authUserData.getToken().getAccessToken()); + newSocial.setRefreshToken(authUserData.getToken().getRefreshToken()); + newSocial.setIdToken(authUserData.getToken().getIdToken()); + sysSocialService.insertByBo(newSocial); // 闇�纭繚鏈嶅姟鏈夋柊澧炴柟娉� + + // 閲嶆柊鏌ヨ纭繚鏁版嵁鍙敤 + list = sysSocialService.selectByAuthId(authId); + } else { + // 鏇存柊绀句氦缁戝畾璁板綍 + SysSocialBo socialBo = new SysSocialBo(); + socialBo.setId(list.get(0).getId()); + socialBo.setAccessToken(authUserData.getToken().getAccessToken()); + socialBo.setRefreshToken(authUserData.getToken().getRefreshToken()); + socialBo.setIdToken(authUserData.getToken().getIdToken()); + sysSocialService.updateByBo(socialBo); + + } + } List<SysSocialVo> list = sysSocialService.selectByAuthId(authUserData.getSource() + authUserData.getUuid()); if (CollUtil.isEmpty(list)) { diff --git a/eims/ruoyi-admin/src/main/resources/application-dev.yml b/eims/ruoyi-admin/src/main/resources/application-dev.yml index a2da72f..cd1faed 100644 --- a/eims/ruoyi-admin/src/main/resources/application-dev.yml +++ b/eims/ruoyi-admin/src/main/resources/application-dev.yml @@ -187,8 +187,16 @@ --- # 涓夋柟鎺堟潈 justauth: # 鍓嶇澶栫綉璁块棶鍦板潃 - address: http://localhost:80 + address: http://192.168.12.236:80 type: + keycloak: + # keycloak 鏈嶅姟鍣ㄥ湴鍧� + server-url: https://lanbaosystem.shlanbao.cn:8443 + realm: lanbao + client-id: DataCapture + client-secret: kplisa4lJHEIM6knqefVbxln85QbA5NX + redirect-uri: ${justauth.address}/social-callback + scopes: [ openid, email, phone, profile ] maxkey: # maxkey 鏈嶅姟鍣ㄥ湴鍧� # 娉ㄦ剰 濡備笅鍧囬厤缃潎涓嶉渶瑕佷慨鏀� maxkey 宸茬粡鍐呯疆濂戒簡鏁版嵁 diff --git a/eims/ruoyi-admin/src/main/resources/application-prod.yml b/eims/ruoyi-admin/src/main/resources/application-prod.yml index 2823bba..ee68d30 100644 --- a/eims/ruoyi-admin/src/main/resources/application-prod.yml +++ b/eims/ruoyi-admin/src/main/resources/application-prod.yml @@ -189,8 +189,16 @@ --- # 涓夋柟鎺堟潈 justauth: # 鍓嶇澶栫綉璁块棶鍦板潃 - address: http://localhost:80 + address: http://192.168.0.24:80 type: + keycloak: + # keycloak 鏈嶅姟鍣ㄥ湴鍧� + server-url: https://lanbaosystem.shlanbao.cn:8443 + realm: lanbao + client-id: DataCapture + client-secret: kplisa4lJHEIM6knqefVbxln85QbA5NX + redirect-uri: ${justauth.address}/social-callback + scopes: [ openid, email, phone, profile ] maxkey: # maxkey 鏈嶅姟鍣ㄥ湴鍧� # 娉ㄦ剰 濡備笅鍧囬厤缃潎涓嶉渶瑕佷慨鏀� maxkey 宸茬粡鍐呯疆濂戒簡鏁版嵁 diff --git a/eims/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialLoginConfigProperties.java b/eims/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialLoginConfigProperties.java index 5f49d9c..ec0131a 100644 --- a/eims/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialLoginConfigProperties.java +++ b/eims/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/config/properties/SocialLoginConfigProperties.java @@ -72,4 +72,7 @@ */ private List<String> scopes; + private String realm; + + } diff --git a/eims/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/keycloak/AuthKeycloakRequest.java b/eims/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/keycloak/AuthKeycloakRequest.java new file mode 100644 index 0000000..f3da8a1 --- /dev/null +++ b/eims/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/keycloak/AuthKeycloakRequest.java @@ -0,0 +1,144 @@ +package org.dromara.common.social.keycloak; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import com.xkcoding.http.support.HttpHeader; +import me.zhyd.oauth.cache.AuthStateCache; +import me.zhyd.oauth.config.AuthConfig; +import me.zhyd.oauth.exception.AuthException; +import me.zhyd.oauth.model.AuthCallback; +import me.zhyd.oauth.model.AuthToken; +import me.zhyd.oauth.model.AuthUser; +import me.zhyd.oauth.request.AuthDefaultRequest; +import me.zhyd.oauth.utils.HttpUtils; +import me.zhyd.oauth.utils.UrlBuilder; +import org.dromara.common.core.domain.model.PasswordLoginBody; +import org.dromara.common.core.utils.SpringUtils; +import org.dromara.common.json.utils.JsonUtils; + +/** + * Keycloak OAuth2 璁よ瘉璇锋眰 + */ +public class AuthKeycloakRequest extends AuthDefaultRequest { + + public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.keycloak.server-url"); + public static final String REALM = SpringUtils.getProperty("justauth.type.keycloak.realm"); + + public AuthKeycloakRequest(AuthConfig config) { + super(config, AuthKeycloakSource.KEYCLOAK); + } + + public AuthKeycloakRequest(AuthConfig config, AuthStateCache authStateCache) { + super(config, AuthKeycloakSource.KEYCLOAK, authStateCache); + } + + + public AuthToken getAccessToken(PasswordLoginBody loginBody) { + + HttpRequest request = HttpRequest.post(SERVER_URL + "/realms/" + REALM + "/protocol/openid-connect/token") + .form("grant_type", "password") + .form("client_id", config.getClientId()) + .form("client_secret", config.getClientSecret()) + .form("username", loginBody.getUsername()) + .form("password", loginBody.getPassword()) + .form("scope", "openid"); + + HttpResponse response = request.execute(); + + Dict object = JsonUtils.parseMap(response.body()); + + if (object.containsKey("error")) { + throw new AuthException(object.getStr("error_description")); + } + if (object.containsKey("message")) { + throw new AuthException(object.getStr("message")); + } + return AuthToken.builder() + .accessToken(object.getStr("access_token")) + .refreshToken(object.getStr("refresh_token")) + .idToken(object.getStr("id_token")) + .tokenType(object.getStr("token_type")) + .expireIn(object.getInt("expires_in")) + .build(); + } + + @Override + public AuthToken getAccessToken(AuthCallback authCallback) { + String body = doPostAuthorizationCode(authCallback.getCode()); + Dict object = JsonUtils.parseMap(body); + + if (object.containsKey("error")) { + throw new AuthException(object.getStr("error_description")); + } + if (object.containsKey("message")) { + throw new AuthException(object.getStr("message")); + } + + return AuthToken.builder() + .accessToken(object.getStr("access_token")) + .refreshToken(object.getStr("refresh_token")) + .idToken(object.getStr("id_token")) + .tokenType(object.getStr("token_type")) + .expireIn(object.getInt("expires_in")) + .build(); + } + + @Override + public AuthUser getUserInfo(AuthToken authToken) { + String body = doGetUserInfo(authToken); + Dict object = JsonUtils.parseMap(body); + + if (object.containsKey("error")) { + throw new AuthException(object.getStr("error_description")); + } + if (object.containsKey("message")) { + throw new AuthException(object.getStr("message")); + } + + return AuthUser.builder() + .uuid(object.getStr("sub")) + .username(object.getStr("preferred_username")) + .nickname(object.getStr("name")) + .email(object.getStr("email")) + .token(authToken) + .source(this.source.toString()) + .build(); + } + + @Override + protected String doPostAuthorizationCode(String code) { + HttpRequest request = HttpRequest.post(source.accessToken()) + .header("Authorization", "Basic " + Base64.encode("%s:%s".formatted(config.getClientId(), config.getClientSecret()))) + .form("grant_type", "authorization_code") + .form("code", code) + .form("redirect_uri", config.getRedirectUri()); + HttpResponse response = request.execute(); + return response.body(); + } + + @Override + protected String doGetUserInfo(AuthToken authToken) { + try { + return new HttpUtils(config.getHttpConfig()).get(source.userInfo(), null, new HttpHeader() + .add("Content-Type", "application/json") + .add("Authorization", "Bearer " + authToken.getAccessToken()), false).getBody(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + @Override + public String authorize(String state) { + return UrlBuilder.fromBaseUrl(super.authorize(state)) + .queryParam("scope", StrUtil.join("%20", config.getScopes())) + .build(); + } + + + + +} diff --git a/eims/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/keycloak/AuthKeycloakSource.java b/eims/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/keycloak/AuthKeycloakSource.java new file mode 100644 index 0000000..e232c34 --- /dev/null +++ b/eims/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/keycloak/AuthKeycloakSource.java @@ -0,0 +1,36 @@ +package org.dromara.common.social.keycloak; + +import me.zhyd.oauth.config.AuthSource; +import me.zhyd.oauth.request.AuthDefaultRequest; + +public enum AuthKeycloakSource implements AuthSource { + KEYCLOAK { + /** + * 鎺堟潈鐨刟pi + */ + @Override + public String authorize() { + return String.format("%s/realms/%s/protocol/openid-connect/auth", AuthKeycloakRequest.SERVER_URL, AuthKeycloakRequest.REALM); + } + + @Override + public String accessToken() { + return String.format("%s/realms/%s/protocol/openid-connect/token", AuthKeycloakRequest.SERVER_URL, AuthKeycloakRequest.REALM); + } + + @Override + public String userInfo() { + return String.format("%s/realms/%s/protocol/openid-connect/userinfo", AuthKeycloakRequest.SERVER_URL, AuthKeycloakRequest.REALM); + } + + + public String logout() { + return String.format("%s/realms/%s/protocol/openid-connect/logout", AuthKeycloakRequest.SERVER_URL, AuthKeycloakRequest.REALM); + } + + @Override + public Class<? extends AuthDefaultRequest> getTargetClass() { + return AuthKeycloakRequest.class; + } + } +} diff --git a/eims/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java b/eims/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java index 9191fca..3c334f9 100644 --- a/eims/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java +++ b/eims/ruoyi-common/ruoyi-common-social/src/main/java/org/dromara/common/social/utils/SocialUtils.java @@ -10,6 +10,7 @@ import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.social.config.properties.SocialLoginConfigProperties; import org.dromara.common.social.config.properties.SocialProperties; +import org.dromara.common.social.keycloak.AuthKeycloakRequest; import org.dromara.common.social.maxkey.AuthMaxKeyRequest; import org.dromara.common.social.topiam.AuthTopIamRequest; @@ -66,6 +67,7 @@ case "aliyun" -> new AuthAliyunRequest(builder.build(), STATE_CACHE); case "maxkey" -> new AuthMaxKeyRequest(builder.build(), STATE_CACHE); case "topiam" -> new AuthTopIamRequest(builder.build(), STATE_CACHE); + case "keycloak" -> new AuthKeycloakRequest(builder.build(), STATE_CACHE); default -> throw new AuthException("鏈幏鍙栧埌鏈夋晥鐨凙uth閰嶇疆"); }; } diff --git a/eims/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysSocialService.java b/eims/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysSocialService.java index cc7016e..e88961b 100644 --- a/eims/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysSocialService.java +++ b/eims/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysSocialService.java @@ -49,5 +49,6 @@ */ List<SysSocialVo> selectByAuthId(String authId); + SysSocialVo selectByUserId(Long userId); } diff --git a/eims/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysSocialServiceImpl.java b/eims/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysSocialServiceImpl.java index 9c54cbc..f13ba15 100644 --- a/eims/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysSocialServiceImpl.java +++ b/eims/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysSocialServiceImpl.java @@ -109,4 +109,11 @@ return baseMapper.selectVoList(new LambdaQueryWrapper<SysSocial>().eq(SysSocial::getAuthId, authId)); } + @Override + public SysSocialVo selectByUserId(Long userId) { + SysSocialVo socialVo = baseMapper.selectVoOne(new LambdaQueryWrapper<SysSocial>().eq(SysSocial::getUserId, userId)); + return socialVo; + } + + } -- Gitblit v1.9.3