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[] => { 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', }, ]; 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; 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 { 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)) { 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 å·²ç»å ç½®å¥½äºæ°æ® 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 å·²ç»å ç½®å¥½äºæ°æ® 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; } 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(); } } 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 { /** * ææçapi */ @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; } } } 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("æªè·åå°ææçAuthé ç½®"); }; } 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); } 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; } }