Sa-Token OAuth2 自定义 grant_type 处理器实战:重写 password 认证与扩展手机号验证码授权

Sa-Token OAuth2 自定义 grant_type 处理器实战:重写 password 认证与扩展手机号验证码授权 Sa-Token OAuth2 自定义 grant_type 处理器实战重写 password 认证与扩展手机号验证码授权【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架让鉴权变得简单、优雅—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token在 Sa-Token 的 OAuth2.0 模块中/oauth2/token接口通过grant_type参数区分不同的令牌获取方式每种grant_type都由一个处理器Handler独立完成校验凭据 → 定位用户 → 签发 Access-Token的全流程。本文围绕自定义权限处理器这一主题讲清两件事如何重写内置的password认证模式处理器以接入真实账号体系以及如何从零添加一个全新的phone_code手机号 验证码授权类型并结合sa-token-oauth2插件源码剖析处理器注册、路由分发与校验机制读完即可在 OAuth2 Server 端落地任意自定义登录方式。一、内置 grant_type 与处理器体系OAuth2 协议的/oauth2/token接口定义了两种获取access_token的grant_type分别是authorization_code使用用户授权的授权码获取 access_tokenpassword使用用户提交的账号、密码来获取 access_token。你可以重写内置grant_type处理器或添加自定义grant_type处理器来支持更多的场景。从源码结构看框架实际内置了三个默认处理器均在 SaOAuth2Strategy.java 中注册到grantTypeHandlerMap内置 grant_type处理器类说明authorization_codeAuthorizationCodeGrantTypeHandler.java校验 code/clientSecret/redirect_uri 后换取 tokenpasswordPasswordGrantTypeHandler.java账号密码登录默认实现仅供开发测试refresh_tokenRefreshTokenGrantTypeHandler.java用 refresh_token 换新 access_token所有处理器统一实现父接口 SaOAuth2GrantTypeHandlerInterface.java该接口只定义了两个方法这就是自定义处理器的最小契约public interface SaOAuth2GrantTypeHandlerInterface { /** * 获取所要处理的 GrantType */ String getHandlerGrantType(); /** * 获取 AccessTokenModel 对象 */ AccessTokenModel getAccessToken(SaRequest req, String clientId, ListString scopes); }内置grant_type常量集中在 GrantType.javaauthorization_code、refresh_token、password、client_credentials、implicit。自定义类型如phone_code不需要也不能加入这个常量类直接返回字符串即可。请求是如何路由到处理器的/oauth2/token请求的分发逻辑位于 SaOAuth2Strategy.java 的grantTypeAuth函数其校验链为读取请求参数grant_type从grantTypeHandlerMap查找处理器找不到则抛异常错误码30126无效 grant_type对authorization_code与password两种特殊类型额外检查服务端全局开关SaOAuth2ServerConfig的enableAuthorizationCode/enablePassword未开启则抛系统未开放的 grant_type30126解析clientId/clientSecret与scope完成客户端身份与权限范围校验检查该应用的allowGrantTypes白名单不包含当前grant_type则抛应用未开放的 grant_type错误码30141——这是自定义模式必须配置应用授权类型的原因校验全部通过后调用grantTypeHandler.getAccessToken(req, clientId, scopes)由处理器真正签发令牌。理解这条链路后重写处理器与添加处理器的区别就清楚了前者只是替换某个已注册grant_type的处理逻辑后者则是在链路的第 1 步和第 4 步各新增一个登记项处理器 Bean 应用白名单。处理器 Bean 如何被框架发现处理器并不是手动调用注册方法的而是由各容器 Starter 在注入阶段自动完成。以 Spring Boot 侧为例SaOAuth2BeanInject.java 中定义了批量注入方法public void setSaOAuth2GrantTypeHandlerInterface(ListSaOAuth2GrantTypeHandlerInterface handlerList) { for (SaOAuth2GrantTypeHandlerInterface handler : handlerList) { SaOAuth2Strategy.instance.registerGrantTypeHandler(handler); } }即只要你的处理器类标注Component成为容器 Bean框架就会调用 SaOAuth2Strategy.java 的registerGrantTypeHandler将其放入grantTypeHandlerMap并在日志中输出自定义 GRANT_TYPE [xxx]。SolonSaOAuth2BeanInject.java与 LoveQQ 等框架的 Starter 也提供了同样的自动注册能力。二、重写 password 认证模式处理器按照官方文档搭建 OAuth2 Server 后直接测试password认证模式时控制台会得到警告警告信息当前 password 认证模式使用默认实现 (SaOAuth2Strategy.instance.doLoginHandle)仅供开发测试 正式项目请重写 PasswordGrantTypeHandler 处理器 loginByUsernamePassword 方法这段警告正是出自 PasswordGrantTypeHandler.java 的loginByUsernamePassword默认方法public PasswordAuthResult loginByUsernamePassword(String username, String password) { System.err.println(警告信息当前 password 认证模式使用默认实现 (SaOAuth2Strategy.instance.doLoginHandle)仅供开发测试); System.err.println(正式项目请重写 PasswordGrantTypeHandler 处理器 loginByUsernamePassword 方法); SaOAuth2Strategy.instance.doLoginHandle.apply(username, password); Object loginId StpUtil.getLoginIdDefaultNull(); return new PasswordAuthResult(loginId); }这是因为为方便测试框架内部直接将 password 认证请求转发到了SaOAuth2Strategy.instance.doLoginHandle来处理。getAccessToken主流程会读取username/password参数并调用该方法拿到PasswordAuthResult中的loginId后若loginId为null直接抛登录失败异常错误码30161否则构建RequestAuthModel调用SaOAuth2Manager.getDataGenerate().generateAccessToken(ra, true, atm - atm.grantType GrantType.password)签发令牌。因此在真正的项目中需要大家重写 password 认证模式处理器继承PasswordGrantTypeHandler并只覆盖账号校验方法/** * 自定义 Password Grant_Type 授权模式处理器认证过程 */ Component public class CustomPasswordGrantTypeHandler extends PasswordGrantTypeHandler { Override public PasswordAuthResult loginByUsernamePassword(String username, String password) { if(sa.equals(username) 123456.equals(password)) { long userId 10001; return new PasswordAuthResult(userId); } else { throw new SaOAuth2Exception(无效账号密码); } } }注意事项重写后返回new PasswordAuthResult(loginId)其中loginId必须与业务登录体系StpUtil使用的 loginId保持一致否则签发出的 access_token 将无法通过后续鉴权校验失败有两种表达方式抛出SaOAuth2Exception或返回loginId null的结果对象二者都会终止签发该类只需标注Component容器启动时即被自动注册覆盖内置的password处理器grantTypeHandlerMap以getHandlerGrantType()为 key同 key 注册会覆盖旧实现。demo 工程中保留了同样思路的参考实现可对照阅读CustomPasswordGrantTypeHandler.java。三、添加自定义 grant_type 处理器手机号 验证码假设有以下需求通过 手机号 验证码 登录返回access_token。整体分三步开放验证码发送接口、编写自定义处理器、为应用登记允许的授权类型。3.1 新增验证码发送接口首先在 oauth2-server 端开放一个接口为指定手机号发送验证码/** * 自定义手机登录接口 */ RestController public class PhoneLoginController { RequestMapping(/oauth2/sendPhoneCode) public SaResult sendCode(String phone) { String code SaFoxUtil.getRandomNumber(100000, 999999) ; SaManager.getSaTokenDao().set(phone_code: phone, code, 60 * 5); System.out.println(手机号 phone 验证码 code 已发送成功); return SaResult.ok(验证码发送成功); } }真实项目肯定是要对接短信服务商的此处仅做模拟代码将发送的验证码打印在控制台上。要点是验证码以phone_code:{手机号}为 key 存入 Sa-Token Dao默认内存、可替换为 Redis有效期 5 分钟60 * 5秒后续处理器正是从同一个存储中取出比对。3.2 自定义 phone_code 处理器在 oauth2-server 新建PhoneCodeGrantTypeHandler实现SaOAuth2GrantTypeHandlerInterface接口/** * 自定义 phone_code 授权模式处理器 */ Component public class PhoneCodeGrantTypeHandler implements SaOAuth2GrantTypeHandlerInterface { Override public String getHandlerGrantType() { return phone_code; } Override public AccessTokenModel getAccessToken(SaRequest req, String clientId, ListString scopes) { // 获取前端提交的参数 String phone req.getParamNotNull(phone); String code req.getParamNotNull(code); String realCode SaManager.getSaTokenDao().get(phone_code: phone); // 1、校验验证码是否正确 if(!code.equals(realCode)) { throw new SaOAuth2Exception(验证码错误); } // 2、校验通过删除验证码 SaManager.getSaTokenDao().delete(phone_code: phone); // 3、登录 long userId 10001; // 模拟 userId真实项目应该根据手机号从数据库查询 // 4、构建 ra 对象 RequestAuthModel ra new RequestAuthModel(); ra.clientId clientId; ra.loginId userId; ra.scopes scopes; // 5、生成 Access-Token AccessTokenModel at SaOAuth2Manager.getDataGenerate().generateAccessToken(ra, true, atm - atm.grantType phone_code); return at; } }结合源码逐项说明关键调用req.getParamNotNull(phone)从请求中读取参数缺失时直接抛异常适合自定义参数名内置常量里没有phone/code这类 OAuth2 标准之外的参数校验后立即delete验证码实现验证码一次性防止同一验证码被重复兑换令牌SaOAuth2DataGenerate.java 提供的重载generateAccessToken(RequestAuthModel ra, boolean isCreateRt, ConsumerAccessTokenModel appendWork)是所有处理器签发令牌的统一入口第一个参数raRequestAuthModel见 RequestAuthModel.java承载clientId、loginId、scopes三元组是生成 access_token 与 openid 的输入第二个参数isCreateRt表示是否同时签发 refresh_tokentrue即返回体中带有refresh_token与refresh_expires_in字段第三个参数appendWork是令牌落库前的追加回调示例中用它把令牌模型上的grantType字段写为phone_code便于后续区分令牌来源。对照内置处理器可以看到相同的代码骨架AuthorizationCodeGrantTypeHandler.java 与 PasswordGrantTypeHandler.java 都是取参 → 校验 → 组装 ra → 调用 dataGenerate 签发的结构自定义处理器只是把校验凭据这一步替换成了自己的业务逻辑。3.3 为应用添加允许的授权类型回到第一节的第 4 步校验即使处理器注册成功如果应用Client没有把新类型加入白名单请求仍会被30141拦截。因此在SaOAuth2DataLoader实现类中为 client 的允许授权类型增加自定义的phone_code// Sa-Token OAuth2自定义数据加载器 Component public class SaOAuth2DataLoaderImpl implements SaOAuth2DataLoader { Override public SaClientModel getClientModel(String clientId) { if(1001.equals(clientId)) { return new SaClientModel() .setClientId(1001) .setClientSecret(aaaa-bbbb-cccc-dddd-eeee) .addAllowRedirectUris(*) // 所有允许授权的 url .addContractScopes(openid, userid, userinfo) .addAllowGrantTypes( GrantType.authorization_code, GrantType.implicit, GrantType.refresh_token, GrantType.password, GrantType.client_credentials, phone_code // 重要代码自定义授权模式 手机号验证码登录 ) ; } return null; } // 其它代码 ... }addAllowGrantTypes的实现在 SaClientModel.java即向allowGrantTypes列表追加字符串内置常量与自定义字符串可以混用。demo 工程中对应的真实数据加载写法可参考 SaClientMockDao.java 与 SaOAuth2DataLoaderImpl.java其中getClientModel通过实现类委托给模拟 DaogetOpenid若使用框架默认算法可直接调用SaOAuth2DataLoader.super.getOpenid(clientId, loginId)。至此全部编码完成可以开始测试。四、测试步骤与结果验证步骤 1先发送验证码http://sa-oauth-server.com:8000/oauth2/sendPhoneCode?phone13144556677执行后后端控制台会打印手机号13144556677验证码xxxxxx已发送成功记下这个 6 位数字。步骤 2请求 token注意grant_type要填写我们自定义的phone_codecode的具体值可以在后端的控制台上看到http://sa-oauth-server.com:8000/oauth2/token ?grant_typephone_code client_id1001 client_secretaaaa-bbbb-cccc-dddd-eeee scopeopenid phone13144556677 code{value}返回结果参考如下{ code: 200, msg: ok, data: null, token_type: Bearer, access_token: pfxRz6KVacwvKNu4IHmDsCJs33kvvARs2z1lTch7stog8nRt6rfVLowtAZ0E, refresh_token: qcFD6Wo2qZidofXQtWF5oK5ML6ljHKufQ5SbouBxzGnHhnMjUG4VV0iXZhdE, expires_in: 7199, refresh_expires_in: 2591999, client_id: 1001, scope: openid, openid: ded91dc189a437dd1bac2274be167d50 }字段解读token_type固定为Bearerexpires_in默认 7200 秒与refresh_expires_in分别对应 access_token 与 refresh_token 的剩余有效期由服务端配置决定openid是应用 用户维度的唯一标识因scopeopenid而返回。由于处理器里isCreateRt true所以同时拿到了 refresh_token。相关错误码速查结合分发链路实现自定义处理器时最常遇到的错误码如下定义见 SaOAuth2ErrorCode.java错误码触发场景常见原因30126无效 grant_type / 系统未开放的 grant_type处理器未注册Bean 未生效或 password/authorization_code 全局开关未开30141应用未开放的 grant_typeSaClientModel的allowGrantTypes未添加该类型30161password 模式登录失败loginByUsernamePassword返回loginId null30111无效 refresh_tokenrefresh_token 不存在或已过期refresh_token 模式五、落地要点小结两种扩展方式的选型仅替换某内置模式的登录逻辑如 password 接数据库校验时继承对应内置处理器只覆盖校验方法代价最小引入全新凭据形式验证码、扫码、第三方 IdP 等时实现SaOAuth2GrantTypeHandlerInterface并实现两个方法即可。注册依赖容器自动装配处理器必须以 Spring/Solon 等容器的 Bean 形式存在Component各 Starter 的SaOAuth2BeanInject才会将其注册进SaOAuth2Strategy.instance.grantTypeHandlerMap纯手工调用registerGrantTypeHandler也可用于框架外的场景。白名单是第二道闸门处理器注册 ≠ 可用务必在SaOAuth2DataLoader中为每个使用方应用通过addAllowGrantTypes显式开放该grant_type。凭据的一次性与存储位置自定义凭据如验证码建议沿用SaManager.getSaTokenDao()存取这样在多实例部署下切换到 Redis 后凭据天然共享校验通过后立即删除实现一次性消费。loginId 必须对齐业务体系处理器中确定的loginId要与StpUtil.login体系一致签发的 access_token 才能在资源端通过SaCheckLogin等注解完成鉴权。demo 工程中 custom_grant_type 目录保留了CustomPasswordGrantTypeHandler、PhoneCodeGrantTypeHandler、PhoneLoginController三个完整参照文件当前处于注释状态取消注释即可作为对照示例运行可与本文代码逐行比对。【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架让鉴权变得简单、优雅—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考