From b1180a219d651fc1406062060bc0b78dcfa86854 Mon Sep 17 00:00:00 2001 From: click33 <2393584716@qq.com> Date: Mon, 26 Aug 2024 18:50:38 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=9F=BA=E4=BA=8E=E5=86=85?= =?UTF-8?q?=E5=AD=98=E5=BD=A2=E5=BC=8F=E7=9A=84=20client=20=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sa-token-doc/_sidebar.md | 1 + sa-token-doc/oauth2/oauth2-data-loader.md | 208 ++++++++++++++++++ sa-token-doc/oauth2/oauth2-server.md | 37 +++- .../oauth2/config/SaOAuth2ServerConfig.java | 39 +++- .../data/loader/SaOAuth2DataLoader.java | 3 +- 5 files changed, 279 insertions(+), 9 deletions(-) create mode 100644 sa-token-doc/oauth2/oauth2-data-loader.md diff --git a/sa-token-doc/_sidebar.md b/sa-token-doc/_sidebar.md index b926c996..8cdb52ad 100644 --- a/sa-token-doc/_sidebar.md +++ b/sa-token-doc/_sidebar.md @@ -55,6 +55,7 @@ - [OAuth2.0简述](/oauth2/readme) - [OAuth2-Server搭建](/oauth2/oauth2-server) - [OAuth2-Server端开放 API 接口](/oauth2/oauth2-apidoc) + - [自定义数据加载器](/oauth2/oauth2-data-loader) - [配置 client 域名校验 ](/oauth2/oauth2-check-domain) - [自定义 Scope 权限及处理器](/oauth2/oauth2-custom-scope) - [为 Scope 划分等级](/oauth2/oauth2-scope-level) diff --git a/sa-token-doc/oauth2/oauth2-data-loader.md b/sa-token-doc/oauth2/oauth2-data-loader.md new file mode 100644 index 00000000..92d6055e --- /dev/null +++ b/sa-token-doc/oauth2/oauth2-data-loader.md @@ -0,0 +1,208 @@ +# OAuth2-自定义数据加载器 + + + +### 1、基于内存的数据加载 + +在之前搭建 OAuth2-Server 示例中,我们演示了 client 信息配置方案: +``` java +// Sa-Token OAuth2 定制化配置 +@Autowired +public void configOAuth2Server(SaOAuth2ServerConfig oauth2Server) { + + // 添加 client + oauth2Server.addClient( + new SaClientModel() + .setClientId("1001") // client id + .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") // client 秘钥 + .addAllowRedirectUris("*") // 所有允许授权的 url + .addContractScopes("openid", "userid", "userinfo") // 所有签约的权限 + .addAllowGrantTypes( // 所有允许的授权模式 + GrantType.authorization_code, // 授权码式 + GrantType.implicit, // 隐式式 + GrantType.refresh_token, // 刷新令牌 + GrantType.password, // 密码式 + GrantType.client_credentials, // 客户端模式 + ) + ) + + // 可以添加更多 client 信息,只要保持 clientId 唯一就行了 + // oauth2Server.addClient(...) + +} +``` + +你也可以在 `application.yml` 配置中 `client` 信息: + + + +``` yaml +# sa-token配置 +sa-token: + # OAuth2.0 配置 + oauth2-server: + # client 列表 + clients: + # 客户端1 + 1001: + # 客户端id + client-id: 1001 + # 客户端秘钥 + client-secret: aaaa-bbbb-cccc-dddd-eeee + # 所有允许授权的 url + allow-redirect-uris: + - http://sa-oauth-client.com:8002 + - http://sa-oauth-client.com:8002/* + # 所有签约的权限 + contract-scopes: + - openid + - userid + - userinfo + # 所有允许的授权模式 + allow-grant-types: + - authorization_code + - implicit + - refresh_token + - password + - client_credentials + # 客户端2 + 1002: + # 客户端id + client-id: 1002 + # 更多配置 ... +``` + +``` properties +########### 客户端1 +# 客户端id +sa-token.oauth2-server.clients.1001.client-id=1001 +# 客户端秘钥 +sa-token.oauth2-server.clients.1001.client-secret=aaaa-bbbb-cccc-dddd-eeee +# 所有允许授权的 url +sa-token.oauth2-server.clients.1001.allow-redirect-uris[0]=http://sa-oauth-client.com:8002 +sa-token.oauth2-server.clients.1001.allow-redirect-uris[1]=http://sa-oauth-client.com:8002/* +# 所有签约的权限 +sa-token.oauth2-server.clients.1001.contract-scopes[0]=openid +sa-token.oauth2-server.clients.1001.contract-scopes[1]=userid +sa-token.oauth2-server.clients.1001.contract-scopes[2]=userinfo +# 所有允许的授权模式 +sa-token.oauth2-server.clients.1001.allow-grant-types[0]=authorization_code +sa-token.oauth2-server.clients.1001.allow-grant-types[1]=implicit +sa-token.oauth2-server.clients.1001.allow-grant-types[2]=refresh_token +sa-token.oauth2-server.clients.1001.allow-grant-types[3]=password +sa-token.oauth2-server.clients.1001.allow-grant-types[4]=client_credentials + +########### 客户端2 +sa-token.oauth2-server.clients.1002.client-id=1002 +sa-token.oauth2-server.clients.1002.client-secret=... +``` + + + +这两种方案都是基于内存形式的 client 信息配置,只适合简单的测试,一般真实项目的 client 信息都是保存在数据库中的,下面演示一下如何在数据库中动态获取 client 信息 + + +### 2、基于数据库的数据加载 + +你只需要自定义数据加载器:新建 `SaOAuth2DataLoaderImpl` 实现 `SaOAuth2DataLoader` 接口。 + + +``` java +/** + * Sa-Token OAuth2:自定义数据加载器 + */ +@Component +public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { + + // 根据 clientId 获取 Client 信息 + @Override + public SaClientModel getClientModel(String clientId) { + // 此为模拟数据,真实环境需要从数据库查询 + if("1001".equals(clientId)) { + return new SaClientModel() + .setClientId("1001") // client id + .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") // client 秘钥 + .addAllowRedirectUris("*") // 所有允许授权的 url + .addContractScopes("openid", "userid", "userinfo") // 所有签约的权限 + .addAllowGrantTypes( // 所有允许的授权模式 + GrantType.authorization_code, // 授权码式 + GrantType.implicit, // 隐式式 + GrantType.refresh_token, // 刷新令牌 + GrantType.password, // 密码式 + GrantType.client_credentials // 客户端模式 + ) + ; + } + return null; + } + + // 根据 clientId 和 loginId 获取 openid + @Override + public String getOpenid(String clientId, Object loginId) { + // 此处使用框架默认算法生成 openid,真实环境建议改为从数据库查询 + return SaOAuth2DataLoader.super.getOpenid(clientId, loginId); + } + +} +``` + +此种形式更加灵活,后续文档将默认按照此种形式来展示示例。 + + +### 3、自定义 openid 生成算法 + +openid 是用户在某一 client 下的唯一标识,其有如下特点: + +- 一个用户在同一个 client 下,openid 是固定的,每次请求都会返回相同的值。 +- 一个用户在不同的 client 下,openid 是不同的,会返回不同的值。 + +oauth2-client 在每次授权时可根据返回的 openid 值来确定用户身份。 + +框架默认的 openid 生成算法为: +``` java +md5(prefix + "_" + clientId + "_" + loginId); +``` + +其中的 prefix 前缀默认值为:`openid_default_digest_prefix`,你可以通过以下方式配置: + + + +``` yaml +# sa-token配置 +sa-token: + oauth2-server: + # 默认 openid 生成算法中使用的摘要前缀 + openid-digest-prefix: xxxxxx +``` + +``` properties +# 默认 openid 生成算法中使用的摘要前缀 +sa-token.oauth2-server.openid-digest-prefix=xxxxxx +``` + + +正常来讲,openid 算法需要保证: + +1. 单个 clientId 下同一 loginId 生成的 `openid` 一致。[必须] +2. 多个 clientId 下同一 loginId 生成的 `openid` 不一致。[非常建议] +3. 客户端无法通过 clientId + loginId 推测 `openid` 值。[建议] +4. 客户端无法通过 clientId + loginId + openid 推测该 loginId 在其它 clientId 下的 `openid` 值。[建议] +5. oauth2-server 自身由 `openid` 可以反查出对应的 clientId 和 loginId。[根据业务需求而定是否满足] + +框架内置的算法,可以满足 1和2,如果自定义了 `sa-token.oauth2-server.openid-digest-prefix` 配置,可以满足3。 + +如果自定义配置的 prefix 长度较短,或比较简单呈现规律性,则有客户端根据 clientId + loginId + openid 穷举爆破出 `prefix` 的风险, +从而获得提前计算彩虹表来推测出其它 clientId、loginId 对应 openid 值的能力。 + +如果自定义的 prefix 前缀比较复杂,让客户端无法爆破,则可以满足4。但依然无法满足5。 + +所以 openid 算法的最优解,应该是 oauth2-server 采用随机字符串作为 openid,然后自建数据库表来维护其映射关系,这样可以同时满足12345。 + +表结构参考如下: + +- id:数据id,主键。 +- client_id:应用id。 +- user_id:用户账号id。 +- openid:对应的 openid 值,随机字符串。 +- create_time:数据创建时间。 +- xxx:其它需要扩展的字段。 diff --git a/sa-token-doc/oauth2/oauth2-server.md b/sa-token-doc/oauth2/oauth2-server.md index ab385de2..66d0e2a7 100644 --- a/sa-token-doc/oauth2/oauth2-server.md +++ b/sa-token-doc/oauth2/oauth2-server.md @@ -59,6 +59,7 @@ implementation 'org.apache.commons:commons-pool2' ### 3、开放服务 + - -2、新建`SaOAuth2ServerController` +1、新建`SaOAuth2ServerController` ``` java /** * Sa-Token OAuth2 Server端 控制器 @@ -122,6 +123,25 @@ public class SaOAuth2ServerController { @Autowired public void setSaOAuth2Config(SaOAuth2Config oauth2Server) { + // 添加 client 信息 + oauth2Server.addClient( + new SaClientModel() + .setClientId("1001") // client id + .setClientSecret("aaaa-bbbb-cccc-dddd-eeee") // client 秘钥 + .addAllowRedirectUris("*") // 所有允许授权的 url + .addContractScopes("openid", "userid", "userinfo") // 所有签约的权限 + .addAllowGrantTypes( // 所有允许的授权模式 + GrantType.authorization_code, // 授权码式 + GrantType.implicit, // 隐式式 + GrantType.refresh_token, // 刷新令牌 + GrantType.password, // 密码式 + GrantType.client_credentials, // 客户端模式 + ) + ); + + // 可以添加更多 client 信息,只要保持 clientId 唯一就行了 + // oauth2Server.addClient(...) + // 配置:未登录时返回的View oauth2Server.notLoginView = () -> { String msg = "当前会话在OAuth-Server端尚未登录,请先访问" @@ -154,12 +174,15 @@ public class SaOAuth2ServerController { return res; }; } - + } ``` -注意:在 `doLoginHandle` 函数里如果要获取 name, pwd 以外的参数,可通过 `SaHolder.getRequest().getParam("xxx")` 来获取。 +注意: +- 在 `doLoginHandle` 函数里如果要获取 name, pwd 以外的参数,可通过 `SaHolder.getRequest().getParam("xxx")` 来获取。 +- 你可以在 [框架配置](/use/config?id=SaClientModel属性定义) 了解有关 `SaClientModel` 对象所有属性的详细定义。 -3、全局异常处理 + +2、全局异常处理 ``` java @RestControllerAdvice public class GlobalExceptionHandler { @@ -171,7 +194,7 @@ public class GlobalExceptionHandler { } ``` -4、创建启动类: +3、创建启动类: ``` java /** * 启动:Sa-OAuth2 Server端 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 abdbc03d..a01b3c8c 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 @@ -16,12 +16,15 @@ package cn.dev33.satoken.oauth2.config; import cn.dev33.satoken.oauth2.consts.SaOAuth2Consts; +import cn.dev33.satoken.oauth2.data.model.loader.SaClientModel; import cn.dev33.satoken.oauth2.function.SaOAuth2ConfirmViewFunction; import cn.dev33.satoken.oauth2.function.SaOAuth2DoLoginHandleFunction; import cn.dev33.satoken.oauth2.function.SaOAuth2NotLoginViewFunction; import cn.dev33.satoken.util.SaResult; import java.io.Serializable; +import java.util.LinkedHashMap; +import java.util.Map; /** * Sa-Token OAuth2 Server 端 配置类 Model @@ -78,12 +81,14 @@ public class SaOAuth2ServerConfig implements Serializable { /** 是否在返回值中隐藏默认的状态字段 (code、msg、data) */ public Boolean hideStatusField = false; - /** * oidc 相关配置 */ SaOAuth2OidcConfig oidc = new SaOAuth2OidcConfig(); + /** client 列表 */ + public Map clients = new LinkedHashMap<>(); + /** * @return enableCode */ @@ -349,6 +354,23 @@ public class SaOAuth2ServerConfig implements Serializable { return this; } + /** + * 获取 client 列表 + * @return / + */ + public Map getClients() { + return clients; + } + + /** + * 写入 client 列表 + * @return / + */ + public SaOAuth2ServerConfig setClients(Map clients) { + this.clients = clients; + return this; + } + // -------------------- SaOAuth2Handle 所有回调函数 -------------------- @@ -388,4 +410,19 @@ public class SaOAuth2ServerConfig implements Serializable { ", oidc='" + oidc + '}'; } + + + /** + * 注册 client + * @return / + */ + public SaOAuth2ServerConfig addClient(SaClientModel client) { + if(this.clients == null) { + this.clients = new LinkedHashMap<>(); + } + this.clients.put(client.getClientId(), client); + return this; + } + + } 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 aa8815a7..635f07ad 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 @@ -36,7 +36,8 @@ public interface SaOAuth2DataLoader { * @return ClientModel */ default SaClientModel getClientModel(String clientId) { - return null; + // 默认从内存配置中读取数据 + return SaOAuth2Manager.getServerConfig().getClients().get(clientId); } /**