新增 OIDC 协议实现

This commit is contained in:
click33
2024-08-24 00:20:17 +08:00
parent 419ca3797c
commit a7a3e8c14f
26 changed files with 576 additions and 34 deletions

View File

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

View File

@@ -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 +
'}';
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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<String> 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);

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -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
*
* <br/> 参考:
* <br/> <a href="https://openid.net/specs/openid-connect-core-1_0.html#IDToken">IDToken</a>
* <br/> <a href="https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">StandardClaims</a>
*
* @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<String, Object> extraData;
}

View File

@@ -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 异常描述

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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<String, Object> convertIdTokenToMap(IdTokenModel idToken) {
// 基础参数
Map<String, Object> 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<String, Object> dataMap = convertIdTokenToMap(idToken);
String keyt = SaOAuth2Manager.getStpLogic().getConfigOrGlobal().getJwtSecretKey();
SaJwtException.throwByNull(keyt, "请配置jwt秘钥", SaJwtErrorCode.CODE_30205);
return SaJwtUtil.createToken(dataMap, keyt);
}
}

View File

@@ -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);
}

View File

@@ -401,7 +401,7 @@ public class SaOAuth2Template {
* @return /
*/
public List<String> 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<String> getLowerScopeList() {
String lowerScope = SaOAuth2Manager.getConfig().getLowerScope();
String lowerScope = SaOAuth2Manager.getServerConfig().getLowerScope();
return SaOAuth2Manager.getDataConverter().convertScopeStringToList(lowerScope);
}