Sa-Token 自定义 Scope 权限与处理器:让 OAuth2 access_token 携带 userinfo 的两种实战方案
发布时间:2026/9/14 5:13:49来源:尧图网络
Sa-Token 自定义 Scope 权限与处理器让 OAuth2 access_token 携带 userinfo 的两种实战方案【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架让鉴权变得简单、优雅—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token导读在 OAuth2 授权流程中第三方 oauth2-client 仅拿到access_token往往不够还需要用户昵称、头像、性别等资料。本文基于 Sa-Token 的sa-token-oauth2插件系统讲解两种让access_token获得更多信息的方案自定义接口模式client 拿 token 后二次请求换取 userinfo与自定义权限处理器模式签发 token 时直接把 userinfo 追加进返回体并深入剖析最终权限处理器的底层机制。读完本文你将能够自主扩展任意 scope 及其数据加工逻辑并理解SaOAuth2ScopeHandlerInterface在 Token 生成链路中的真实调用位置。本文主文档为 oauth2-custom-scope.md全部实现均可在仓库源码中逐一验证。1、需求场景access_token 之外client 还需要什么一般情况下对于第三方 oauth2-client 来讲仅仅拿到用户的access_token是不够的还需要拿到更多的信息比如用户昵称、头像等资料。sa-token-oauth2提供两种模式让access_token可以得到更多信息模式实现思路优点代价自定义接口模式在 oauth2-server 端开放一个资料查询接口oauth2-client 得到access_token后再次调用这个接口来获取userinfo信息实现简单、职责清晰与标准 OAuth2 资源端模型一致client 多一次网络请求自定义权限处理器模式自定义一个ScopeHandler直接在返回access_token时追加字段将userinfo信息和access_token一并返回到 oauth2-client少一次网络请求client 一次性拿到全部数据需要在 Token 生成期做数据加工方案相对进阶下面分别展开两种模式的具体落地步骤。2、模式一自定义接口模式该模式是典型的资源端思路认证中心开放一个受 scope 保护的资料接口client 用access_token换取userinfo。2.1、新建查询接口在 oauth2-server 新建接口查询指定access_token代表的userId其userinfo// 获取 userinfo 信息昵称、头像、性别等等 RequestMapping(/oauth2/userinfo) public SaResult userinfo() { // 获取 Access-Token 对应的账号id String accessToken SaOAuth2Manager.getDataResolver().readAccessToken(SaHolder.getRequest()); Object loginId SaOAuth2Util.getLoginIdByAccessToken(accessToken); System.out.println(-------- 此Access-Token对应的账号id: loginId); // 校验 Access-Token 是否具有权限: userinfo SaOAuth2Util.checkAccessTokenScope(accessToken, userinfo); // 模拟账号信息 真实环境需要查询数据库获取信息 MapString, Object map new LinkedHashMap(); // map.put(userId, loginId); 一般原则下oauth2-server 不能把 userId 返回给 oauth2-client map.put(nickname, 林小林); map.put(avatar, http://xxx.com/1.jpg); map.put(age, 18); map.put(sex, 男); map.put(address, 山东省 青岛市 城阳区); return SaResult.ok().setMap(map); }代码说明均有源码可查SaOAuth2Manager.getDataResolver().readAccessToken(SaHolder.getRequest())用于从请求参数或请求头Authorization: Bearer xxx中解析出 access_tokenSaOAuth2Util.getLoginIdByAccessToken(accessToken)与checkAccessTokenScope(accessToken, userinfo)为静态方法底层委托给 SaOAuth2Template.java 的checkAccessTokenScope()/getLoginIdByAccessToken()前者会遍历校验AccessTokenModel.scopes是否包含指定 scope不满足时抛出SaOAuth2AccessTokenScopeException后者在checkAccessToken()通过后直接返回loginId该接口的完整实现同样存在于仓库 Demo 中见 SaOAuth2ResourcesController.java。注释强调一般原则下oauth2-server不能把userId返回给 oauth2-client防止真实账号体系泄露如需标识用户应使用openid/unionid等派生 ID。2.2、申请 code 时指定权限oauth2-client 申请code时一定需要加上userinfo权限否则后续无法通过 scope 校验http://sa-oauth-server.com:8000/oauth2/authorize ?response_typecode client_id1001 redirect_urihttp://sa-oauth-client.com:8002/ scopeuserinfo2.3、code 换 access_token访问上述链接后得到code授权码然后我们拿着code换access_tokenhttp://sa-oauth-server.com:8000/oauth2/token ?grant_typeauthorization_code client_id1001 client_secretaaaa-bbbb-cccc-dddd-eeee code${code}2.4、access_token 取 userinfo使用返回的access_token再次访问接口/oauth2/userinfohttp://sa-oauth-server.com:8000/oauth2/userinfo?access_token${access_token}返回以下结果{ code: 200, msg: ok, data: null, nickname: 林小林, avatar: http://xxx.com/1.jpg, age: 18, sex: 男, address: 山东省 青岛市 城阳区 }至此client 成功拿到 userinfo。提示在接口模式中除了手动调用SaOAuth2Util.checkAccessTokenScope(...)校验 scope还可以使用注解方式例如 Demo 中的 TestController.java 里SaCheckAccessToken(scope userinfo)或SaCheckAccessToken(scope {openid, userinfo})即携带有效 access_token 且具备指定 scope 才可访问。3、模式二自定义权限处理器模式该模式把 userinfo 的组装逻辑前移到Token 签发阶段只要签发的access_token具备userinfoscope数据就会被自动追加进返回结果。3.1、新建权限处理器在 oauth2-server 新建UserinfoScopeHandler.java实现SaOAuth2ScopeHandlerInterface接口/** * 自定义 userinfo scope 处理器 */ Component public class UserinfoScopeHandler implements SaOAuth2ScopeHandlerInterface { // 指示当前处理器所要处理的 scope Override public String getHandlerScope() { return userinfo; } // 当构建的 AccessToken 具有此权限时所需要执行的方法 Override public void workAccessToken(AccessTokenModel at) { System.out.println(--------- userinfo 权限加工 AccessTokenModel --------- ); // 模拟账号信息 真实环境需要查询数据库获取信息 MapString, Object map new LinkedHashMapString, Object(); map.put(userId, 10008); map.put(nickname, shengzhang_); map.put(avatar, http://xxx.com/1.jpg); map.put(age, 18); map.put(sex, 男); map.put(address, 山东省 青岛市 城阳区); at.extraData.putAll(map); } // 当构建的 ClientToken 具有此权限时所需要执行的方法 Override public void workClientToken(ClientTokenModel ct) { } // 当使用 RefreshToken 刷新 AccessToken 时是否重新执行 workAccessToken 构建方法 // 在一些实时性较高的数据中需要指定为 true Override public boolean refreshAccessTokenIsWork() { return true; } }如上所述所有写入到extraData中的数据都将追加返回到 oauth2-client 端。该示例在 Demo 工程中以注释代码形式完整保留见 UserinfoScopeHandler.java。3.2、接口方法逐个解读从 SaOAuth2ScopeHandlerInterface.java 的源码可见该接口是所有 OAuth2 权限处理器的父接口自定义 Scope 处理器必须实现它共四个方法方法作用说明String getHandlerScope()声明本处理器负责的 scope 字符串框架据此把 scope 与处理器建立映射void workAccessToken(AccessTokenModel at)当构建的 AccessToken 具有此权限时执行在此向at.extraData写入自定义数据void workClientToken(ClientTokenModel ct)当构建的 ClientToken 具有此权限时执行处理客户端令牌场景如 client_credentials 模式boolean refreshAccessTokenIsWork()使用 RefreshToken 刷新 AccessToken 时是否重新执行workAccessToken默认false即刷新时不重跑对实时性较高的数据如登录态、动态签名可重写为true3.3、申请 code 时指定权限与接口模式一致oauth2-client 申请code时一定需要加上userinfo权限http://sa-oauth-server.com:8000/oauth2/authorize ?response_typecode client_id1001 redirect_urihttp://sa-oauth-client.com:8002/ scopeuserinfo3.4、code 换 access_token访问上述链接后得到code授权码然后我们拿着code换access_tokenhttp://sa-oauth-server.com:8000/oauth2/token ?grant_typeauthorization_code client_id1001 client_secretaaaa-bbbb-cccc-dddd-eeee code${code}返回结果如下{ code: 200, msg: ok, data: null, token_type: Bearer, access_token: LQ24xI0hX25vIzvciHPA0PNsnGCweSFM1Bzl8783li07VAXpw8sEfn9xsta2, refresh_token: rKB8mby1Mw8yZXHbWzliHx6lmatcLcULLw5C5cUMBhMMRx72DFg5u0owZgrA, expires_in: 7199, refresh_expires_in: 2591999, client_id: 1001, scope: openid,userid,userinfo, userinfo: { userId: 10008, nickname: shengzhang_, avatar: http://xxx.com/1.jpg, age: 18, sex: 男, address: 山东省 青岛市 城阳区 } }注意响应体中的userinfo: {...}对象它不是平铺字段而是一个独立的数据块Demo 中处理器用at.extraData.put(userinfo, map)组装而文档主示例用putAll(map)平铺追加两种写法效果略有差异可视业务需要选择。拿到 userinfo。3.5、模式对比一次请求 vs 两次请求相比于自定义接口模式自定义权限处理器模式可以少一次网络请求让 oauth2-client 端提前拿到userinfo信息。二者的取舍在于接口模式更符合认证端 / 资源端分离的标准架构接口可独立扩展、按需查询适合数据量大或需要二次加工的场景处理器模式在签发 Token 时即完成数据组装client 端一次拿到全部信息适合字段固定、实时性要求不高的场景实时性要求高时请把refreshAccessTokenIsWork()返回true。4、底层原理ScopeHandler 何时被调用为了让处理器模式不止于会写代码还需要理解它的触发时机。从源码可以梳理出完整的调用链1注册SaOAuth2Strategy在构造时通过registerDefaultScopeHandler()注册了 4 个内置处理器——openid、unionid、userid、oidc见 SaOAuth2Strategy.java并维护一个MapString, SaOAuth2ScopeHandlerInterface scopeHandlerMap。开发者自定义的Component处理器由registerScopeHandler(handler)按getHandlerScope()放入该 Map该 Map 以LinkedHashMap实现处理器按注册顺序执行。2调度三个策略函数是核心枢纽workAccessTokenByScopeL100-L113遍历AccessTokenModel.scopes逐个取出对应 handler 执行workAccessToken(at)refreshAccessTokenWorkByScopeL118-L131刷新场景下只执行refreshAccessTokenIsWork()返回true的处理器workClientTokenByScopeL136-L149对 ClientTokenModel 执行同样的加工。3触发点这三处策略函数在 SaOAuth2DataGenerateDefaultImpl.java 中被调用generateAccessToken(String code)L95——授权码模式换 token 时执行workAccessTokenByScopegenerateAccessToken(RequestAuthModel ra, boolean isCreateRt, ...)L176——隐藏式 / 密码式签发时执行workAccessTokenByScoperefreshAccessToken(String refreshToken)L143——刷新 token 时执行refreshAccessTokenWorkByScope。也就是说只要 scope 里声明了userinfoToken 生成的那一刻处理器就会被自动触发并把数据写入AccessTokenModel.extraData该字段定义见 AccessTokenModel.java最终随响应序列化返回给 client。4内置处理器参考openidOpenIdScopeHandler.java 写入openid、useridUserIdScopeHandler.java 写入userid、oidcOidcScopeHandler.java 构建id_token且refreshAccessTokenIsWork()返回true。自定义userinfo处理器的写法与它们完全同构可以直接对照学习。Scope 常量定义集中在 CommonScope.java。5、最终权限处理器Finally Work Scope Handler5.1、概念与特性当一个自定义权限处理器监听的 scope 字符串为_FINALLY_WORK_SCOPE时则代表这个权限处理器为最终权限处理器。它会在所有权限处理器工作完成之后执行一次即使 oauth2-client 端没有申请任何 scope最终权限处理器也会固定执行。该常量定义在 SaOAuth2Consts.javapublic static final String _FINALLY_WORK_SCOPE _FINALLY_WORK_SCOPE;5.2、示例代码/** * 最终权限处理器在所有权限处理器工作完成之后执行此权限处理器 */ Component public class FinallyWorkScopeHandler implements SaOAuth2ScopeHandlerInterface { Override public String getHandlerScope() { return SaOAuth2Consts._FINALLY_WORK_SCOPE; } Override public void workAccessToken(AccessTokenModel at) { // 在所有权限处理器工作完成之后执行此处方法加工 AccessToken // System.out.println(123); } Override public void workClientToken(ClientTokenModel ct) { // System.out.println(456); } }典型使用场景包括在多个 scope 处理器分别写入字段后统一对extraData做后置整理、补充全局字段、或者做数据脱敏等收尾工作。5.3、源码佐证它为什么永远最后执行回到 SaOAuth2Strategy.java 的三个策略函数其执行顺序都是先遍历at.scopes逐个执行对应 scope 的处理器无 scope 时跳过循环再从scopeHandlerMap中取出_FINALLY_WORK_SCOPE对应的处理器若存在则固定执行一次。因此无论 client 是否申请了 scope最终处理器都兜底执行且刷新场景下refreshAccessTokenWorkByScope最终处理器也遵循refreshAccessTokenIsWork()的开关约定只有返回true才会在刷新时重跑。5.4、测试用例印证仓库测试 SaOAuth2StrategyTest.java 提供了直接的行为验证workAccessTokenByScope_finallyWorkScope注册一个_FINALLY_WORK_SCOPE处理器后即便 Token 只带useridscope最终处理器也会执行 1 次并向extraData写入finallytruerefreshAccessTokenWorkByScope断言内置openid处理器refreshAccessTokenIsWork()为false、oidc处理器为true刷新后 openid 不重跑而 id_token 会被重新生成refreshAccessTokenWorkByScope_finallySkipWhenFalse当最终处理器的refreshAccessTokenIsWork()为false时刷新流程不会执行它。这从侧面说明了refreshAccessTokenIsWork()的真实语义默认不重跑按需开启。6、实践要点scope 签约与配置要让自定义 scope 真正可用还需要两个前提6.1、client 必须签约该 scope在 oauth2-server 的数据加载层需要为每个client_id声明其允许申请的 scope 列表。Demo 中的 SaClientMockDao.java 通过链式 API 完成签约.addContractScopes(openid, unionid, userid, userinfo, oidc) // 所有签约的权限client 申请scopeuserinfo时框架会通过checkClientSecretAndScope(...)校验该 scope 是否在签约范围内未签约的 scope 无法通过授权。6.2、scope 等级higher / lower配置在 oauth2-server 的application.yml中还可以把 scope 划分为高级 / 低级权限用于授权确认页展示区分参见 application.ymlsa-token: oauth2-server: # 定义哪些 scope 是高级权限多个用逗号隔开 # higher-scope: openid,userid # 定义哪些 scope 是低级权限多个用逗号隔开 # lower-scope: userinfo7、总结围绕让access_token携带更多用户信息这一需求sa-token-oauth2提供了两条清晰的实现路径自定义接口模式认证中心开放/oauth2/userinfo资料接口用SaOAuth2Util.getLoginIdByAccessTokencheckAccessTokenScope做身份与权限双重校验client 持access_token二次请求换取资料——多一次网络请求但架构更标准自定义权限处理器模式实现SaOAuth2ScopeHandlerInterface并声明getHandlerScope() userinfo在workAccessToken(AccessTokenModel at)中向at.extraData写入数据Token 签发时由SaOAuth2Strategy.workAccessTokenByScope自动调度并随响应返回——少一次网络请求client 一次性拿齐数据最终权限处理器scope 标识为SaOAuth2Consts._FINALLY_WORK_SCOPE的处理器无条件在最后执行一次适合对多 scope 结果做统一后处理refreshAccessTokenIsWork()控制刷新 Token 时是否重跑默认不重跑。两条路径、一个收尾处理器配合源码中的调度实现SaOAuth2Strategy、触发实现SaOAuth2DataGenerateDefaultImpl与行为测试SaOAuth2StrategyTest即可完整掌握 Sa-Token OAuth2 的 scope 扩展机制为接入第三方登录、开放平台、用户资料服务提供即插即用的扩展能力。【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架让鉴权变得简单、优雅—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网