diff --git a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml index f12e1b5b..4c661c3c 100644 --- a/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml +++ b/sa-token-demo/sa-token-demo-oauth2/sa-token-demo-oauth2-server/src/main/resources/application.yml @@ -4,7 +4,7 @@ server: # sa-token配置 sa-token: # token名称 (同时也是 Cookie 名称) - token-name: sa-token-oauth2-server + token-name: satoken # 是否打印操作日志 is-log: true # jwt 秘钥 diff --git a/sa-token-doc/oauth2/oauth2-questions.md b/sa-token-doc/oauth2/oauth2-questions.md index 0b92e069..4c7a4f33 100644 --- a/sa-token-doc/oauth2/oauth2-questions.md +++ b/sa-token-doc/oauth2/oauth2-questions.md @@ -30,8 +30,6 @@ OAuth2 集成常见问题整理 - - ### 问:Sa-Token-OAuth2 怎么集成多账号模式? 在 `configOAuth2Server` 里指定 oauth2 模块使用的 `StpLogic` 对象即可: @@ -45,4 +43,17 @@ public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) { // 指定 oauth2 模块使用的 `StpLogic` 对象 SaOAuth2Manager.setStpLogic(StpUserUtil.stpLogic); } -``` \ No newline at end of file +``` + + + + +### 问:授权码流程中 state 参数是干吗用的? + +state 参数用于验证授权码流程的发起端和接受端是否为同一个客户端,以防止OAuth-Server账号伪装攻击。 + +授权流程发起端必须保证: +- state 参数必须足够随机,不可被预测。 +- state 参数与授权码流程发起客户端 一 一 对 应,授权流程发起时创建的 state 必须与接受时返回的 state 值一致。 +- 安全起见,一个 state 参数只允许使用一次。 + 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 136612e8..f3110717 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 @@ -158,6 +158,17 @@ public interface SaOAuth2Dao { } } + /** + * 持久化:state + * @param state / + */ + default void saveState(String state) { + if( ! SaFoxUtil.isEmpty(state)) { + long ttl = SaOAuth2Manager.getServerConfig().getCodeTimeout(); + getSaTokenDao().set(splicingStateSaveKey(state), state, ttl); + } + } + // ------------------- delete数据 @@ -262,6 +273,14 @@ public interface SaOAuth2Dao { getSaTokenDao().delete(splicingGrantScopeKey(clientId, loginId)); } + /** + * 删除:state记录 + * @param state / + */ + default void deleteGrantScope(String state) { + getSaTokenDao().delete(splicingStateSaveKey(state)); + } + // ------------------- get 数据 @@ -372,6 +391,18 @@ public interface SaOAuth2Dao { return SaOAuth2Manager.getDataConverter().convertScopeStringToList(value); } + /** + * 获取:state + * @param state / + * @return / + */ + default String getState(String state) { + if(SaFoxUtil.isEmpty(state)) { + return null; + } + return getSaTokenDao().get(splicingStateSaveKey(state)); + } + // ------------------- 拼接key @@ -469,6 +500,15 @@ public interface SaOAuth2Dao { return getSaTokenConfig().getTokenName() + ":oauth2:grant-scope:" + clientId + ":" + loginId; } + /** + * 拼接key:state 参数持久化 + * @param state / + * @return key + */ + default String splicingStateSaveKey(String state) { + return getSaTokenConfig().getTokenName() + ":oauth2:state:" + state; + } + // -------- bean 对象代理 diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerate.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerate.java index 6fbee6a3..197dfde7 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerate.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerate.java @@ -91,4 +91,10 @@ public interface SaOAuth2DataGenerate { */ void revokeAccessToken(String accessToken); + /** + * 检查 state 是否被重复使用 + * @param state / + */ + void checkState(String state); + } diff --git a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerateDefaultImpl.java b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerateDefaultImpl.java index 0090a2db..266342bf 100644 --- a/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerateDefaultImpl.java +++ b/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/data/generate/SaOAuth2DataGenerateDefaultImpl.java @@ -245,6 +245,7 @@ public class SaOAuth2DataGenerateDefaultImpl implements SaOAuth2DataGenerate { public String buildRedirectUri(String redirectUri, String code, String state) { String url = SaFoxUtil.joinParam(redirectUri, SaOAuth2Consts.Param.code, code); if( ! SaFoxUtil.isEmpty(state)) { + checkState(state); url = SaFoxUtil.joinParam(url, SaOAuth2Consts.Param.state, state); } return url; @@ -261,6 +262,7 @@ public class SaOAuth2DataGenerateDefaultImpl implements SaOAuth2DataGenerate { public String buildImplicitRedirectUri(String redirectUri, String token, String state) { String url = SaFoxUtil.joinSharpParam(redirectUri, SaOAuth2Consts.Param.token, token); if( ! SaFoxUtil.isEmpty(state)) { + checkState(state); url = SaFoxUtil.joinSharpParam(url, SaOAuth2Consts.Param.state, state); } return url; @@ -291,5 +293,18 @@ public class SaOAuth2DataGenerateDefaultImpl implements SaOAuth2DataGenerate { dao.deleteRefreshTokenIndex(at.clientId, at.loginId); } + /** + * 检查 state 是否被重复使用 + * @param state / + */ + @Override + public void checkState(String state) { + String value = SaOAuth2Manager.getDao().getState(state); + if(SaFoxUtil.isNotEmpty(value)) { + throw new SaOAuth2Exception("多次请求的 state 不可重复: " + state); + } + SaOAuth2Manager.getDao().saveState(state); + } + }