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);
}
/**