From a7a3e8c14f8f333f2dc9e8188e6e5a19c35649ef Mon Sep 17 00:00:00 2001 From: click33 <2393584716@qq.com> Date: Sat, 24 Aug 2024 00:20:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20OIDC=20=E5=8D=8F=E8=AE=AE?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/templates/index.html | 4 +- .../sa-token-demo-oauth2-server/pom.xml | 7 + .../com/pj/SaOAuth2ServerApplication.java | 6 +- .../com/pj/oauth2/SaOAuth2DataLoaderImpl.java | 2 +- .../oauth2/custom/CustomOidcScopeHandler.java | 32 ++++ .../src/main/resources/application.yml | 2 + sa-token-doc/_sidebar.md | 1 + sa-token-doc/oauth2/oauth2-oidc.md | 154 ++++++++++++++++++ .../cn/dev33/satoken/jwt/SaJwtTemplate.java | 20 +++ .../java/cn/dev33/satoken/jwt/SaJwtUtil.java | 17 +- sa-token-plugin/sa-token-oauth2/pom.xml | 6 + .../dev33/satoken/oauth2/SaOAuth2Manager.java | 16 +- .../oauth2/config/SaOAuth2OidcConfig.java | 62 +++++++ .../oauth2/config/SaOAuth2ServerConfig.java | 26 +++ .../dev33/satoken/oauth2/dao/SaOAuth2Dao.java | 6 +- .../data/loader/SaOAuth2DataLoader.java | 2 +- .../data/model/loader/SaClientModel.java | 2 +- .../oauth2/data/model/oidc/IdTokenModel.java | 90 ++++++++++ .../oauth2/exception/SaOAuth2Exception.java | 10 +- .../handler/PasswordGrantTypeHandler.java | 2 +- .../processor/SaOAuth2ServerProcessor.java | 6 +- .../scope/handler/OidcScopeHandler.java | 122 +++++++++++++- .../oauth2/strategy/SaOAuth2Strategy.java | 2 +- .../oauth2/template/SaOAuth2Template.java | 4 +- .../solon/oauth2/SaOAuth2AutoConfigure.java | 7 +- .../spring/oauth2/SaOAuth2BeanInject.java | 2 +- 26 files changed, 576 insertions(+), 34 deletions(-) create mode 100644 sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/java/com/pj/oauth2/custom/CustomOidcScopeHandler.java create mode 100644 sa-token-doc/oauth2/oauth2-oidc.md create mode 100644 sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2OidcConfig.java create mode 100644 sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/oidc/IdTokenModel.java diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/templates/index.html b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/templates/index.html index 79c33db3..adc3bd0a 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/templates/index.html +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-client/src/main/resources/templates/index.html @@ -48,11 +48,11 @@ 当请求链接不包含 scope 权限,或请求的 scope 近期已授权时,将无需用户手动确认,做到静默授权 http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/ - + 当请求链接包含具体的 scope 权限时,将需要用户手动确认,此时 OAuth-Server 会返回更多的数据 - http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=openid,userid,userinfo + http://sa-oauth-server.com:8000/oauth2/authorize?response_type=code&client_id=1001&redirect_uri=http://sa-oauth-client.com:8002/&scope=openid,userid,userinfo,oidc 我们可以拿着 Refresh-Token 去刷新我们的 Access-Token,每次刷新后旧Token将作废 diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/pom.xml b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/pom.xml index a9599fba..db0b6c7d 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/pom.xml +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/pom.xml @@ -59,6 +59,13 @@ spring-boot-starter-thymeleaf + + + cn.dev33 + sa-token-jwt + ${sa-token.version} + + diff --git a/sa-token-doc/oauth2/oauth2-oidc.md b/sa-token-doc/oauth2/oauth2-oidc.md new file mode 100644 index 00000000..754822ef --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-oidc.md @@ -0,0 +1,154 @@ +# OAuth2 开启 OIDC 协议 (OpenID Connect) + +--- + +### 1、开启步骤 + +1、引入 `sa-token-jwt` 依赖,用来签发 `id_token` + + + +``` xml + + + cn.dev33 + sa-token-jwt + ${sa.top.version} + +``` + +``` gradle +// sa-token-jwt 签发 OIDC id_token 令牌 +implementation 'cn.dev33:sa-token-jwt:${sa.top.version}' +``` + + + +2、在 `SaOAuth2DataLoader` 实现类中,返回的 `SaClientModel` 中添加 `oidc` 的签约权限。 + +``` java +@Component +public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { + @Override + public SaClientModel getClientModel(String clientId) { + // 此为模拟数据,真实环境需要从数据库查询 + if("1001".equals(clientId)) { + return new SaClientModel() + // .... + .addContractScopes("openid", "userid", "userinfo", "oidc") // 此处添加上签约权限:oidc + .addAllowGrantTypes( + // ... + ) + ; + } + return null; + } + // 其它代码 ... +} +``` + + +### 2、测试 + +1、在 OAuth2-Client 端申请授权码时,添加上 `oidc` 权限: +``` url +http://sa-oauth-server.com:8000/oauth2/authorize + ?response_type=code + &client_id=1001 + &redirect_uri=http://sa-oauth-client.com:8002/ + &scope=oidc +``` + +2、得到授权码后,然后拿着 `code` 换 `access_token` +``` url +http://sa-oauth-server.com:8000/oauth2/token + ?grant_type=authorization_code + &client_id=1001 + &client_secret=aaaa-bbbb-cccc-dddd-eeee + &code=${code} +``` + +3、返回的结果中将包含 `id_token` 字段: +``` js +{ + "code": 200, + "msg": "ok", + "data": null, + "token_type": "bearer", + "access_token": "WdpjZdGlXdOzsAcr7gqPwmLVInHrhpznQa2pDOVqZmLXQynBflkcWqE6f5o2", + "refresh_token": "hKHwBm3eH6iqSHlXRGWQaziV8OoyHvzmUb97lKEEZnZJLt3NunBFx7rVZWbT", + "expires_in": 7199, + "refresh_expires_in": 2591999, + "client_id": "1001", + "scope": "oidc", + "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vc2Etb2F1dGgtc2VydmVyLmNvbTo4MDAwIiwic3ViIjoiMTAwMDEiLCJhdWQiOiIxMDAxIiwiZXhwIjoxNzI0NDI1OTg5LCJpYXQiOjE3MjQ0MjUzODksImF1dGhfdGltZSI6MTcyNDQwMDUyNiwibm9uY2UiOiJLTHlNR08zZ1R0YVdhMEFRcHF0RUNpTk9SWkY1QkhvRCIsImF6cCI6IjEwMDEifQ.gP3UYMexaQ9v0huKUuqhV9-dPxPpaEuFPIlPb2UZaOI" +} +``` + +4、解析 `id_token` 将得到以下载荷 +``` js +{ + "iss": "http://sa-oauth-server.com:8000", // 签发人 + "sub": "10001", // userId + "aud": "1001", // clientId + "exp": 1724425989, // 令牌到期时间,10位时间戳 + "iat": 1724425389, // 签发此令牌的时间,10位时间戳 + "auth_time": 1724400526, // 用户认证时间,10位时间戳 + "nonce": "KLyMGO3gTtaWa0AQpqtECiNORZF5BHoD", // 随机数,防止重放攻击 + "azp": "1001" // clientId +} +``` + +如果默认携带的载荷无法满足你的业务需求,你还可以自定义追加扩展字段,让 `id_token` 返回更多信息 + + +### 3、扩展 id_token 载荷 + +新建 `CustomOidcScopeHandler` 集成 `OidcScopeHandler`,扩展 OIDC 权限处理器,返回更多字段: +``` java +/** + * 扩展 OIDC 权限处理器,返回更多字段 + */ +@Component +public class CustomOidcScopeHandler extends OidcScopeHandler { + + @Override + public IdTokenModel workExtraData(IdTokenModel idToken) { + Object userId = idToken.sub; + System.out.println("----- 为 idToken 追加扩展字段 ----- "); + + idToken.extraData.put("uid", userId); // 用户id + idToken.extraData.put("nickname", "lin_xiao_lin"); // 昵称 + idToken.extraData.put("picture", "https://sa-token.cc/logo.png"); // 头像 + idToken.extraData.put("email", "456456@xx.com"); // 邮箱 + idToken.extraData.put("phone_number", "13144556677"); // 手机号 + // 更多字段 ... + // 可参考:https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + + return idToken; + } + +} +``` + +重启项目,再次请求授权,返回的 `id_token` 载荷将包含更多字段: + +``` js +{ + "iss": "http://sa-oauth-server.com:8000", + "sub": "10001", + "aud": "1001", + "exp": 1724430149, + "iat": 1724429549, + "auth_time": 1724400526, + "nonce": "SBRLOcfeo9FFmLTB8OINvuulam5FMOre", + "azp": "1001", + "uid": "10001", + "nickname": "lin_xiao_lin", + "picture": "https://sa-token.cc/logo.png", + "email": "456456@xx.com", + "phone_number": "13144556677" +} +``` + + diff --git a/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtTemplate.java b/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtTemplate.java index 90ad98ef..1f828409 100644 --- a/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtTemplate.java +++ b/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtTemplate.java @@ -309,4 +309,24 @@ public class SaJwtTemplate { return (effTime - System.currentTimeMillis()) / 1000; } + + + // -------------- 其它方法 + + /** + * 创建 jwt (Map 参数方式) + * + * @param map 扩展数据 + * @param keyt 秘钥 + * @return jwt-token + */ + public String createToken(Map map, String keyt) { + // 创建 + JWT jwt = JWT.create().addPayloads(map); + + // 返回 + return generateToken(jwt, keyt); + } + + } diff --git a/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtUtil.java b/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtUtil.java index 3661b614..42c4b541 100644 --- a/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtUtil.java +++ b/sa-token-plugin/sa-token-jwt/src/main/java/cn/dev33/satoken/jwt/SaJwtUtil.java @@ -15,11 +15,11 @@ */ package cn.dev33.satoken.jwt; -import java.util.Map; - import cn.hutool.json.JSONObject; import cn.hutool.jwt.JWT; +import java.util.Map; + /** * jwt 操作工具类封装 * @@ -195,4 +195,17 @@ public class SaJwtUtil { return saJwtTemplate.getTimeout(token, loginType, keyt); } + + // -------------- 其它方法 + + /** + * 创建 jwt (Map 参数方式) + * + * @param map 扩展数据 + * @param keyt 秘钥 + * @return jwt-token + */ + public static String createToken(Map map, String keyt) { + return saJwtTemplate.createToken(map, keyt); + } } diff --git a/sa-token-plugin/sa-token-oauth2/pom.xml b/sa-token-plugin/sa-token-oauth2/pom.xml index f8717b99..f3f5bee9 100644 --- a/sa-token-plugin/sa-token-oauth2/pom.xml +++ b/sa-token-plugin/sa-token-oauth2/pom.xml @@ -22,6 +22,12 @@ cn.dev33 sa-token-core + + + cn.dev33 + sa-token-jwt + true + diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/SaOAuth2Manager.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/SaOAuth2Manager.java index 8ba535b8..270da526 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/SaOAuth2Manager.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/SaOAuth2Manager.java @@ -41,20 +41,20 @@ public class SaOAuth2Manager { /** * OAuth2 配置 Bean */ - private static volatile SaOAuth2ServerConfig config; - public static SaOAuth2ServerConfig getConfig() { - if (config == null) { + private static volatile SaOAuth2ServerConfig serverConfig; + public static SaOAuth2ServerConfig getServerConfig() { + if (serverConfig == null) { // 初始化默认值 synchronized (SaOAuth2Manager.class) { - if (config == null) { - setConfig(new SaOAuth2ServerConfig()); + if (serverConfig == null) { + setServerConfig(new SaOAuth2ServerConfig()); } } } - return config; + return serverConfig; } - public static void setConfig(SaOAuth2ServerConfig config) { - SaOAuth2Manager.config = config; + public static void setServerConfig(SaOAuth2ServerConfig serverConfig) { + SaOAuth2Manager.serverConfig = serverConfig; } /** diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2OidcConfig.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2OidcConfig.java new file mode 100644 index 00000000..2eb817d4 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2OidcConfig.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed 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 cn.dev33.satoken.oauth2.config; + +import java.io.Serializable; + +/** + * Sa-Token OAuth2 Server 端 Oidc 配置类 Model + * + * @author click33 + * @since 1.39.0 + */ +public class SaOAuth2OidcConfig implements Serializable { + + private static final long serialVersionUID = -6541180061782004705L; + + /** iss 值,如不配置则自动计算 */ + public String iss; + + /** idToken 有效期(单位秒) 默认十分钟 */ + public long idTokenTimeout = 60 * 10; + + public String getIss() { + return iss; + } + + public SaOAuth2OidcConfig setIss(String iss) { + this.iss = iss; + return this; + } + + public long getIdTokenTimeout() { + return idTokenTimeout; + } + + public SaOAuth2OidcConfig setIdTokenTimeout(long idTokenTimeout) { + this.idTokenTimeout = idTokenTimeout; + return this; + } + + @Override + public String toString() { + return "SaOAuth2OidcConfig{" + + "iss='" + iss + '\'' + + ", idTokenTimeout=" + idTokenTimeout + + '}'; + } + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2ServerConfig.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2ServerConfig.java index 1ae2e05f..dbb2e74f 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2ServerConfig.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/config/SaOAuth2ServerConfig.java @@ -72,6 +72,11 @@ public class SaOAuth2ServerConfig implements Serializable { /** 指定低级权限,多个用逗号隔开 */ public String lowerScope; + /** + * oidc 相关配置 + */ + SaOAuth2OidcConfig oidc = new SaOAuth2OidcConfig(); + /** * @return enableCode */ @@ -287,6 +292,26 @@ public class SaOAuth2ServerConfig implements Serializable { return this; } + /** + * 获取 oidc 相关配置 + * + * @return oidc 相关配置 + */ + public SaOAuth2OidcConfig getOidc() { + return this.oidc; + } + + /** + * 设置 oidc 相关配置 + * + * @param oidc / + * @return / + */ + public SaOAuth2ServerConfig setOidc(SaOAuth2OidcConfig oidc) { + this.oidc = oidc; + return this; + } + // -------------------- SaOAuth2Handle 所有回调函数 -------------------- @@ -321,6 +346,7 @@ public class SaOAuth2ServerConfig implements Serializable { ", openidDigestPrefix='" + openidDigestPrefix + ", higherScope='" + higherScope + ", lowerScope='" + lowerScope + + ", oidc='" + oidc + '}'; } } diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/dao/SaOAuth2Dao.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/dao/SaOAuth2Dao.java index 209f8a55..801e22cf 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/dao/SaOAuth2Dao.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/dao/SaOAuth2Dao.java @@ -45,7 +45,7 @@ public interface SaOAuth2Dao { if(c == null) { return; } - getSaTokenDao().setObject(splicingCodeSaveKey(c.code), c, SaOAuth2Manager.getConfig().getCodeTimeout()); + getSaTokenDao().setObject(splicingCodeSaveKey(c.code), c, SaOAuth2Manager.getServerConfig().getCodeTimeout()); } /** @@ -56,7 +56,7 @@ public interface SaOAuth2Dao { if(c == null) { return; } - getSaTokenDao().set(splicingCodeIndexKey(c.clientId, c.loginId), c.code, SaOAuth2Manager.getConfig().getCodeTimeout()); + getSaTokenDao().set(splicingCodeIndexKey(c.clientId, c.loginId), c.code, SaOAuth2Manager.getServerConfig().getCodeTimeout()); } /** @@ -151,7 +151,7 @@ public interface SaOAuth2Dao { default void saveGrantScope(String clientId, Object loginId, List scopes) { if( ! SaFoxUtil.isEmpty(scopes)) { // TODO ttl 计算规则优化 - long ttl = SaOAuth2Manager.getConfig().getAccessTokenTimeout(); + long ttl = SaOAuth2Manager.getServerConfig().getAccessTokenTimeout(); // long ttl = checkClientModel(clientId).getAccessTokenTimeout(); String value = SaOAuth2Manager.getDataConverter().convertScopeListToString(scopes); getSaTokenDao().set(splicingGrantScopeKey(clientId, loginId), value, ttl); diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/loader/SaOAuth2DataLoader.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/loader/SaOAuth2DataLoader.java index df6dece3..c8f93ef6 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/loader/SaOAuth2DataLoader.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/loader/SaOAuth2DataLoader.java @@ -60,7 +60,7 @@ public interface SaOAuth2DataLoader { * @return 此账号在此Client下的openid */ default String getOpenid(String clientId, Object loginId) { - return SaSecureUtil.md5(SaOAuth2Manager.getConfig().getOpenidDigestPrefix() + "_" + clientId + "_" + loginId); + return SaSecureUtil.md5(SaOAuth2Manager.getServerConfig().getOpenidDigestPrefix() + "_" + clientId + "_" + loginId); } } diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/loader/SaClientModel.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/loader/SaClientModel.java index e6ae542d..46e64344 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/loader/SaClientModel.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/loader/SaClientModel.java @@ -75,7 +75,7 @@ public class SaClientModel implements Serializable { public SaClientModel() { - SaOAuth2ServerConfig config = SaOAuth2Manager.getConfig(); + SaOAuth2ServerConfig config = SaOAuth2Manager.getServerConfig(); this.isNewRefresh = config.getIsNewRefresh(); this.accessTokenTimeout = config.getAccessTokenTimeout(); this.refreshTokenTimeout = config.getRefreshTokenTimeout(); diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/oidc/IdTokenModel.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/oidc/IdTokenModel.java new file mode 100644 index 00000000..7e9783e0 --- /dev/null +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/model/oidc/IdTokenModel.java @@ -0,0 +1,90 @@ +/* + * Copyright 2020-2099 sa-token.cc + * + * Licensed 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 cn.dev33.satoken.oauth2.data.model.oidc; + +import java.io.Serializable; +import java.util.Map; + +/** + * OIDC IdToken Model + * + *
参考: + *
IDToken + *
StandardClaims + * + * @author click33 + * @since 1.23.0 + */ +public class IdTokenModel implements Serializable { + + private static final long serialVersionUID = -6541180061782004705L; + + /** + * 必填:发行者标识符,例如:https://server.example.com + */ + public String iss; + + /** + * 必填:用户标识符,用户id,例如:10001 + */ + public Object sub; + + /** + * 必填:客户端标识符,clientId,例如:s6BhdRkqt3 + */ + public String aud; + + /** + * 必填:令牌到期时间,10位时间戳,例如:1723341795 + */ + public long exp; + + /** + * 必填:签发此令牌的时间,10位时间戳,例如:1723339995 + */ + public long iat; + + /** + * 用户认证时间,10位时间戳,例如:1723339988 + */ + public long authTime; + + /** + * 随机数,客户端提供,防止重放攻击,例如:e9a3f4d9 + */ + public String nonce; + + /** + * 身份验证上下文类引用 + */ + public String acr; + + /** + * 身份验证方法参考 + */ + public String amr; + + /** + * 授权方 - 签发 ID 令牌的一方,如果存在,它必须包含此方的 OAuth 2.0 客户端 ID。 + */ + public String azp; + + /** + * 扩展数据 + */ + public Map extraData; + +} diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2Exception.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2Exception.java index 62803e27..c1b96496 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2Exception.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/exception/SaOAuth2Exception.java @@ -29,7 +29,15 @@ public class SaOAuth2Exception extends SaTokenException { * 序列化版本号 */ private static final long serialVersionUID = 6806129545290130114L; - + + /** + * 一个异常:代表OAuth2认证流程错误 + * @param cause 根异常原因 + */ + public SaOAuth2Exception(Throwable cause) { + super(cause); + } + /** * 一个异常:代表OAuth2认证流程错误 * @param message 异常描述 diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/PasswordGrantTypeHandler.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/PasswordGrantTypeHandler.java index 1609e355..e5615e83 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/PasswordGrantTypeHandler.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/granttype/handler/PasswordGrantTypeHandler.java @@ -70,7 +70,7 @@ public class PasswordGrantTypeHandler implements SaOAuth2GrantTypeHandlerInterfa * @param password / */ public void loginByUsernamePassword(String username, String password) { - SaOAuth2Manager.getConfig().doLoginHandle.apply(username, password); + SaOAuth2Manager.getServerConfig().doLoginHandle.apply(username, password); } } \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/processor/SaOAuth2ServerProcessor.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/processor/SaOAuth2ServerProcessor.java index ed8cc38b..ce05abc9 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/processor/SaOAuth2ServerProcessor.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/processor/SaOAuth2ServerProcessor.java @@ -113,7 +113,7 @@ public class SaOAuth2ServerProcessor { // 获取变量 SaRequest req = SaHolder.getRequest(); SaResponse res = SaHolder.getResponse(); - SaOAuth2ServerConfig cfg = SaOAuth2Manager.getConfig(); + SaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig(); SaOAuth2DataGenerate dataGenerate = SaOAuth2Manager.getDataGenerate(); SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate(); String responseType = req.getParamNotNull(Param.response_type); @@ -218,7 +218,7 @@ public class SaOAuth2ServerProcessor { public Object doLogin() { // 获取变量 SaRequest req = SaHolder.getRequest(); - SaOAuth2ServerConfig cfg = SaOAuth2Manager.getConfig(); + SaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig(); return cfg.doLoginHandle.apply(req.getParam(Param.name), req.getParam(Param.pwd)); } @@ -285,7 +285,7 @@ public class SaOAuth2ServerProcessor { public Object clientToken() { // 获取变量 SaRequest req = SaHolder.getRequest(); - SaOAuth2ServerConfig cfg = SaOAuth2Manager.getConfig(); + SaOAuth2ServerConfig cfg = SaOAuth2Manager.getServerConfig(); SaOAuth2Template oauth2Template = SaOAuth2Manager.getTemplate(); String grantType = req.getParamNotNull(Param.grant_type); diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/OidcScopeHandler.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/OidcScopeHandler.java index 7adb42d6..97c70d1d 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/OidcScopeHandler.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/scope/handler/OidcScopeHandler.java @@ -15,9 +15,25 @@ */ package cn.dev33.satoken.oauth2.scope.handler; +import cn.dev33.satoken.SaManager; +import cn.dev33.satoken.context.SaHolder; +import cn.dev33.satoken.context.model.SaRequest; +import cn.dev33.satoken.jwt.SaJwtUtil; +import cn.dev33.satoken.jwt.error.SaJwtErrorCode; +import cn.dev33.satoken.jwt.exception.SaJwtException; +import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.data.model.AccessTokenModel; import cn.dev33.satoken.oauth2.data.model.ClientTokenModel; +import cn.dev33.satoken.oauth2.data.model.oidc.IdTokenModel; +import cn.dev33.satoken.oauth2.data.model.request.ClientIdAndSecretModel; +import cn.dev33.satoken.oauth2.exception.SaOAuth2Exception; import cn.dev33.satoken.oauth2.scope.CommonScope; +import cn.dev33.satoken.util.SaFoxUtil; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.LinkedHashMap; +import java.util.Map; /** * id_token 权限处理器:在 AccessToken 扩展参数中追加 id_token 字段 @@ -33,7 +49,31 @@ public class OidcScopeHandler implements SaOAuth2ScopeHandlerInterface { @Override public void workAccessToken(AccessTokenModel at) { - // TODO 追加参数 id_token + SaRequest req = SaHolder.getRequest(); + ClientIdAndSecretModel client = SaOAuth2Manager.getDataResolver().readClientIdAndSecret(req); + + // 基础参数 + IdTokenModel idToken = new IdTokenModel(); + idToken.iss = getIss(); + idToken.sub = at.loginId; + idToken.aud = client.clientId; + idToken.iat = System.currentTimeMillis() / 1000; + idToken.exp = idToken.iat + SaOAuth2Manager.getServerConfig().getOidc().getIdTokenTimeout(); + idToken.authTime = SaOAuth2Manager.getStpLogic().getSessionByLoginId(at.loginId).getCreateTime() / 1000; + idToken.nonce = getNonce(); + idToken.acr = null; + idToken.amr = null; + idToken.azp = client.clientId; + + // 额外参数 + idToken.extraData = new LinkedHashMap<>(); + idToken = workExtraData(idToken); + + // 构建 jwtIdToken + String jwtIdToken = generateJwtIdToken(idToken); + + // 放入 AccessTokenModel + at.extraData.put("id_token", jwtIdToken); } @Override @@ -41,4 +81,84 @@ public class OidcScopeHandler implements SaOAuth2ScopeHandlerInterface { } + /** + * 获取 iss + * @return / + */ + public String getIss() { + String urlString = SaHolder.getRequest().getUrl(); + try { + URL url = new URL(urlString); + String iss = url.getProtocol() + "://" + url.getHost(); + if(url.getPort() != -1) { + iss += ":" + url.getPort(); + } + return iss; + } catch (MalformedURLException e) { + throw new SaOAuth2Exception(e); + } + } + + /** + * 获取 nonce + * @return / + */ + public String getNonce() { + String nonce = SaHolder.getRequest().getParam("nonce"); + if(SaFoxUtil.isEmpty(nonce)) { + nonce = SaFoxUtil.getRandomString(32); + } + SaManager.getSaSignTemplate().checkNonce(nonce); + return nonce; + } + + /** + * 加工 IdTokenModel + * @return / + */ + public IdTokenModel workExtraData(IdTokenModel idToken) { + // + return idToken; + } + + /** + * 将 IdTokenModel 转化为 Map 数据 + * @return / + */ + public Map convertIdTokenToMap(IdTokenModel idToken) { + // 基础参数 + Map map = new LinkedHashMap<>(); + map.put("iss", idToken.iss); + map.put("sub", idToken.sub); + map.put("aud", idToken.aud); + map.put("exp", idToken.exp); + map.put("iat", idToken.iat); + map.put("auth_time", idToken.authTime); + map.put("nonce", idToken.nonce); + map.put("acr", idToken.acr); + map.put("amr", idToken.amr); + map.put("azp", idToken.azp); + + // 移除 null 值 + idToken.extraData.entrySet().removeIf(entry -> entry.getValue() == null); + + // 扩展参数 + map.putAll(idToken.extraData); + + // 返回 + return map; + } + + /** + * 生成 jwt 格式的 id_token + * @param idToken / + * @return / + */ + public String generateJwtIdToken(IdTokenModel idToken) { + Map dataMap = convertIdTokenToMap(idToken); + String keyt = SaOAuth2Manager.getStpLogic().getConfigOrGlobal().getJwtSecretKey(); + SaJwtException.throwByNull(keyt, "请配置jwt秘钥", SaJwtErrorCode.CODE_30205); + return SaJwtUtil.createToken(dataMap, keyt); + } + } \ No newline at end of file diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/strategy/SaOAuth2Strategy.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/strategy/SaOAuth2Strategy.java index dbfda655..939b9ff9 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/strategy/SaOAuth2Strategy.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/strategy/SaOAuth2Strategy.java @@ -170,7 +170,7 @@ public final class SaOAuth2Strategy { } // 看看全局是否开启了此 grantType - SaOAuth2ServerConfig config = SaOAuth2Manager.getConfig(); + SaOAuth2ServerConfig config = SaOAuth2Manager.getServerConfig(); if(grantType.equals(GrantType.authorization_code) && !config.getEnableAuthorizationCode() ) { throw new SaOAuth2Exception("系统未开放的 grant_type: " + grantType); } diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java index cbfeedbb..ea5d33ca 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java @@ -401,7 +401,7 @@ public class SaOAuth2Template { * @return / */ public List getHigherScopeList() { - String higherScope = SaOAuth2Manager.getConfig().getHigherScope(); + String higherScope = SaOAuth2Manager.getServerConfig().getHigherScope(); return SaOAuth2Manager.getDataConverter().convertScopeStringToList(higherScope); } @@ -410,7 +410,7 @@ public class SaOAuth2Template { * @return / */ public List getLowerScopeList() { - String lowerScope = SaOAuth2Manager.getConfig().getLowerScope(); + String lowerScope = SaOAuth2Manager.getServerConfig().getLowerScope(); return SaOAuth2Manager.getDataConverter().convertScopeStringToList(lowerScope); } diff --git a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/oauth2/SaOAuth2AutoConfigure.java b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/oauth2/SaOAuth2AutoConfigure.java index dab18067..d0d7ee45 100644 --- a/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/oauth2/SaOAuth2AutoConfigure.java +++ b/sa-token-starter/sa-token-solon-plugin/src/main/java/cn/dev33/satoken/solon/oauth2/SaOAuth2AutoConfigure.java @@ -18,7 +18,6 @@ package cn.dev33.satoken.solon.oauth2; import cn.dev33.satoken.oauth2.SaOAuth2Manager; import cn.dev33.satoken.oauth2.config.SaOAuth2ServerConfig; import cn.dev33.satoken.oauth2.template.SaOAuth2Template; -import cn.dev33.satoken.oauth2.template.SaOAuth2Util; import org.noear.solon.annotation.Bean; import org.noear.solon.annotation.Condition; import org.noear.solon.annotation.Configuration; @@ -36,11 +35,11 @@ public class SaOAuth2AutoConfigure { @Bean public void init(AppContext appContext) throws Throwable { appContext.subBeansOfType(SaOAuth2Template.class, bean -> { - SaOAuth2Util.saOAuth2Template = bean; + SaOAuth2Manager.setTemplate(bean); }); appContext.subBeansOfType(SaOAuth2ServerConfig.class, bean -> { - SaOAuth2Manager.setConfig(bean); + SaOAuth2Manager.setServerConfig(bean); }); } @@ -48,7 +47,7 @@ public class SaOAuth2AutoConfigure { * 获取 OAuth2配置Bean */ @Bean - public SaOAuth2ServerConfig getConfig(@Inject(value = "${sa-token.oauth2}", required = false) SaOAuth2ServerConfig oAuth2Config) { + public SaOAuth2ServerConfig getConfig(@Inject(value = "${sa-token.oauth2-server}", required = false) SaOAuth2ServerConfig oAuth2Config) { return oAuth2Config; } } \ No newline at end of file diff --git a/sa-token-starter/sa-token-spring-boot-autoconfig/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanInject.java b/sa-token-starter/sa-token-spring-boot-autoconfig/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanInject.java index 092d7956..03748859 100644 --- a/sa-token-starter/sa-token-spring-boot-autoconfig/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanInject.java +++ b/sa-token-starter/sa-token-spring-boot-autoconfig/src/main/java/cn/dev33/satoken/spring/oauth2/SaOAuth2BeanInject.java @@ -53,7 +53,7 @@ public class SaOAuth2BeanInject { */ @Autowired(required = false) public void setSaOAuth2Config(SaOAuth2ServerConfig saOAuth2Config) { - SaOAuth2Manager.setConfig(saOAuth2Config); + SaOAuth2Manager.setServerConfig(saOAuth2Config); } /**