Spring Security OAuth2 微服务认证中心自定义授权模式扩展以及常见登录认证场景下的应用实战

本文源码地址
后端:https://gitee.com/youlaitech/youlai-mall/tree/v2.0.1
前端:https://gitee.com/youlaiorg/mall-admin/tree/v2.0.1
移动端: https://gitee.com/youlaiorg/mall-app

图片无法显示
掘金:https://juejin.cn/post/7020632761123733517
CSDN: https://blog.csdn.net/u013737132/article/details/125608714

一. 前言

【APP 移动端】Spring Security OAuth2 手机短信验证码模式 【微信小程序】Spring Security OAuth2 微信授权模式
【管理系统】Spring Security OAuth2 密码模式 【管理系统】Spring Security OAuth2 验证码模式

Spring Security OAuth2 默认实现的四种授权模式在实际的应用场景中往往满足不了预期,如以下需求:

  1. 授权对象分多个用户体系,例如系统用户和会员用户;
  2. 在密码授权模式的基础上加个验证码校验;
  3. 基于 Spring Security OAuth2 实现手机和短信验证码登录;
  4. 基于 Spring Security OAuth2 实现微信小程序授权登录。

相信你会遇到但不仅限上面的场景,网上也有很多对 Spring Security OAuth2 授权模式扩展的相关文章,但多少有不全面和实现复杂的通病,一度会让你觉得 Spring Security OAuth2 很难, Spring 在实现核心功能基础上同时还提供了很多的扩展点,Spring Security OAuth2 亦是如此,相信这篇文章会帮助消除它很难的误解。

本篇将以实战为主,原理为辅的方式,本着全面最少改动的原则去对 Spring Security OAuth2 授权模式的扩展,本篇涉及内容如下:

  1. Spring Cloud Gateway 微服务网关WebFlux整合谷歌验证码 Kaptcha
  2. SpringBoot 整合阿里云SMS短信服务;
  3. Spring Security OAuth2 认证授权模式底层源码分析;
  4. Spring Security OAuth2 扩展验证码授权模式;
  5. Spring Security OAuth2 扩展手机短信验证码授权模式;
  6. Spring Security OAuth2 扩展微信授权模式;
  7. Spring Security OAuth2 多用户体系刷新模式;
  8. vue-element-admin 后台管理前端登录接入验证码授权模式
  9. uni-app 微信小程序登录接入微信授权模式
  10. uni-app H5、移动端手机验证码登录接入手机短信验证码授权模式

🔊 先做个很重要的声明吧,本篇文章涉及所有的代码地址:

项目名称 码云(Gitee) GitHub
微服务后台 youlai-mall youlai-mall
管理前端 mall-admin-web mall-admin-web
微信小程序/H5/Android/IOS mall-app mall-app

因为涉及的内容很多,文章中做不到把所有的代码完全贴出来,但是放心源码全部在线的,同样文档也是

往期系列文章

微服务

  1. Spring Cloud实战 | 第一篇:Windows搭建Nacos服务
  2. Spring Cloud实战 | 第二篇:Spring Cloud整合Nacos实现注册中心
  3. Spring Cloud实战 | 第三篇:Spring Cloud整合Nacos实现配置中心
  4. Spring Cloud实战 | 第四篇:Spring Cloud整合Gateway实现API网关
  5. Spring Cloud实战 | 第五篇:Spring Cloud整合OpenFeign实现微服务之间的调用
  6. Spring Cloud实战 | 第六篇:Spring Cloud Gateway+Spring Security OAuth2+JWT实现微服务统一认证授权
  7. Spring Cloud实战 | 最七篇:Spring Cloud Gateway+Spring Security OAuth2集成统一认证授权平台下实现注销使JWT失效方案
  8. Spring Cloud实战 | 最八篇:Spring Cloud +Spring Security OAuth2+ Vue前后端分离模式下无感知刷新实现JWT续期
  9. Spring Cloud实战 | 最九篇:Spring Security OAuth2认证服务器统一认证自定义异常处理
  10. Spring Cloud实战 | 第十篇 :Spring Cloud + Nacos整合Seata 1.4.1最新版本实现微服务架构中的分布式事务,进阶之路必须要迈过的槛
  11. Spring Cloud实战 | 第十一篇 :Spring Cloud Gateway网关实现对RESTful接口权限和按钮权限细粒度控制
  12. Spring Cloud实战 | 第十二篇:Sentinel+Nacos实现流控、熔断降级,赋能拥有降级功能的Feign新技能熔断,做到熔断降级双剑合璧
  13. Spring Cloud实战 | 总结篇:Spring Cloud Gateway + Spring Security OAuth2 + JWT 实现微服务统一认证授权和鉴权

中间件

  1. SpringBoot 整合 Elastic Stack 最新版本(7.14.1)分布式日志解决方案,开源微服务全栈项目【有来商城】的日志落地实践

管理系统前端

  1. vue-element-admin实战 | 第一篇: 移除mock接入微服务接口,搭建SpringCloud+Vue前后端分离管理平台
  2. vue-element-admin实战 | 第二篇: 最小改动接入后台实现根据权限动态加载菜单

微信小程序

  1. vue+uni-app商城实战 | 第一篇:从0到1快捷开发一个商城微信小程序,无缝接入Spring Cloud OAuth2实现一键授权登录

应用部署

  1. Docker实战 | 第一篇:Linux 安装 Docker

  2. Docker实战 | 第二篇:Docker部署nacos-server:1.4.0

  3. Docker实战 | 第三篇:IDEA 集成 Docker 插件实现一键远程部署 SpringBoot 应用,无需三方依赖,开源微服务全栈有来商城线上部署方式

  4. Docker实战 | 第四篇:Docker安装Nginx,实现基于vue-element-admin框架构建的项目线上部署

二. 验证码授权模式

1. 原理

验证码授权模式是在密码模式基础添加个验证码校验,如果你有 不管功夫怎样,能打赢你的就是好功夫 这样的心态完全可以使用过滤器实现,但如果想不开的话那就试下扩展吧。

因为是基于密码授权模式的扩展,就先了解密码授权模式的流程吧。因为其他几种授权模式和密码模式实现原理都是一样,弄明白密码授权模式之后其他授权模式包括如何去扩展都是轻车熟路。

密码模式流程: 根据请求参数 grant_type 的值 password 匹配到授权者 ResourceOwnerPasswordTokenGraner ,授权者委托给认证提供者管理器 ProviderManager,根据 token 类型匹配到提供者 DaoAuthenticationProviderProvider 从数据库获取用户认证信息和客户端请求传值的用户信息进行认证密码判读,验证通过之后返回token给客户端。

下面密码授权模式时序图贴出关键类和方法,断点走几遍流程就应该知道流程。

验证码授权模式时序图如下,仔细比对下和密码授权模式的区别。

比较可知两者的区别基本就是授权者 Granter 的区别,后续的 Provider 获取用户认证信息和密码判断完全一致,具体新增的验证码模式授权者 CaptchaTokenGranter 和密码模式的授权者 ResourceOwnerPasswordTokenGraner 区别在于前者的 getOAuth2Authentication() 方法获取认证信息添加了校验验证码的逻辑,具体的代码实现在实战里交待。

2. 实战

验证码授权模式涉及Spring Security OAuth2扩展验证码授权模式、后台生成验证码和前端登录加入验证码三部分,涉及到前后端的东西,针对自己需要选择关注点即可。

2.1 验证码授权模式扩展

从原理得知只需重写 Granter 为其添加校验验证码的能力,所以复制密码模式的授权者 ResourceOwnerPasswordTokenGranter 然后重名为 CaptchaTokenGranter,稍加改动成为验证码模式的授权者。

CaptchaTokenGranter
/**
 * 验证码授权模式 授权者
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 * @date 2021/9/25
 */
public class CaptchaTokenGranter extends AbstractTokenGranter {

    /**
     * 声明授权者 CaptchaTokenGranter 支持授权模式 captcha
     * 根据接口传值 grant_type = captcha 的值匹配到此授权者
     * 匹配逻辑详见下面的两个方法
     *
     * @see org.springframework.security.oauth2.provider.CompositeTokenGranter#grant(String, TokenRequest)
     * @see org.springframework.security.oauth2.provider.token.AbstractTokenGranter#grant(String, TokenRequest)
     */
    private static final String GRANT_TYPE = "captcha";
    private final AuthenticationManager authenticationManager;
    private StringRedisTemplate redisTemplate;

    public CaptchaTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService,
                               OAuth2RequestFactory requestFactory, AuthenticationManager authenticationManager,
                               StringRedisTemplate redisTemplate
    ) {
        super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.authenticationManager = authenticationManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

        Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());

        // 验证码校验逻辑
        String validateCode = parameters.get("validateCode");
        String uuid = parameters.get("uuid");

        Assert.isTrue(StrUtil.isNotBlank(validateCode), "验证码不能为空");
        String validateCodeKey = AuthConstants.VALIDATE_CODE_PREFIX + uuid;
        
        // 从缓存取出正确的验证码和用户输入的验证码比对
        String correctValidateCode = redisTemplate.opsForValue().get(validateCodeKey);
        if (!validateCode.equals(correctValidateCode)) {
            throw new BizException("验证码不正确");
        } else {
            redisTemplate.delete(validateCodeKey);
        }

        String username = parameters.get("username");
        String password = parameters.get("password");

        // 移除后续无用参数
        parameters.remove("password");
        parameters.remove("validateCode");
        parameters.remove("uuid");

        // 和密码模式一样的逻辑
        Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);

        try {
            userAuth = this.authenticationManager.authenticate(userAuth);
        } catch (AccountStatusException var8) {
            throw new InvalidGrantException(var8.getMessage());
        } catch (BadCredentialsException var9) {
            throw new InvalidGrantException(var9.getMessage());
        }

        if (userAuth != null && userAuth.isAuthenticated()) {
            OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
            return new OAuth2Authentication(storedOAuth2Request, userAuth);
        } else {
            throw new InvalidGrantException("Could not authenticate user: " + username);
        }
    }
}

上面相对密码模式的授权者做了两处改动,总结如下:

  1. 修改 GRANT_TYPE 的值 passwordcaptcha;
  2. getOAuth2Authentication() 方法添加验证码校验逻辑。
AuthorizationServerConfig

在 AuthorizationServerConfig 配置类重写 TokenGranter 让其支持新增的验证码模式授权者 CaptchaTokenGranter

到此,Spring Security OAuth2 扩展验证码授权大功告成!!!

怎么样,简不简单?相信你有可能心存怀疑,那先做个测试吧。

管理前端的客户端ID是 mall-admin-web ,在测试之前,先赋予客户端支持验证码模式。

在登录界面输入错误的验证码和正确的验证码各一次看下效果,是不是能达到预期的效果,还有验证码如何生成和前端如何传值放在后文说。

2.2 Spring WebFlux 整合验证码 Kaptcha

验证码生成的功能主要是生成一个随机码将其缓存redis,返回redis的key标识(一般是uuid)和随机码的图片给前端。因为没有任何业务逻辑,故这里直接放在网关,除了利用 WebFlux 性能优势之外还能减少一次转发。youlai-gateway 验证码相关代码结构图如下:

CaptchaHandler
@Component
@RequiredArgsConstructor
public class CaptchaHandler implements HandlerFunction<ServerResponse> {

    private final Producer producer;
    private final StringRedisTemplate redisTemplate;

    @Override
    public Mono<ServerResponse> handle(ServerRequest serverRequest) {
        // 生成验证码
        String capText = producer.createText();
        String capStr = capText.substring(0, capText.lastIndexOf("@"));
        String code = capText.substring(capText.lastIndexOf("@") + 1);
        BufferedImage image = producer.createImage(capStr);
        // 缓存验证码至Redis
        String uuid = IdUtil.simpleUUID();
        redisTemplate.opsForValue().set(AuthConstants.VALIDATE_CODE_PREFIX + uuid, code, 60, TimeUnit.SECONDS);
        // 转换流信息写出
        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
        try {
            ImageIO.write(image, "jpg", os);
        } catch (IOException e) {
            return Mono.error(e);
        }

        java.util.Map resultMap = new HashMap<String, String>();
        resultMap.put("uuid", uuid);
        resultMap.put("img", Base64.encode(os.toByteArray()));

        return ServerResponse.status(HttpStatus.OK).body(BodyInserters.fromValue(Result.success(resultMap)));
    }
}
CaptchaConfig

属性 kaptcha.textproducer.impl 需要指定你自己项目文本生成器 KaptchaTextCreator 的类路径

// 验证码文本生成器 
properties.setProperty("kaptcha.textproducer.impl", "com.youlai.gateway.kaptcha.KaptchaTextCreator");
CaptchaRouter
@Configuration
public class CaptchaRouter {

    @Bean
    public RouterFunction<ServerResponse> routeFunction(CaptchaHandler captchaHandler) {
        return RouterFunctions
                .route(RequestPredicates.GET("/captcha")
                        .and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), captchaHandler::handle);
    }
}
验证码测试

修改 Nacos 网关配置文件 youlai-gateway.yaml 白名单添加请求路径 /captcha

访问 http://localhost:9999/captcha 如下:

2.3 前端登录接入验证码模式

登录页面

登录表单添加验证码,完整代码地址:mall-admin-web

src/views/login/index.vue

 <el-form-item prop="validateCode">
    <span class="svg-container">
       <svg-icon icon-class="validCode"/>
     </span>
   <el-input
     v-model="loginForm.validateCode"
     auto-complete="off"
     placeholder="请输入验证码"
     style="width: 65%"
     @keyup.enter.native="handleLogin"
   />
   <div class="validate-code">
     <img :src="captchaUrl" @click="getValidateCode" height="38px"/>
   </div>
 </el-form-item>

返回的图片是base64 加密后的字符串,所以添加前缀 data:image/gif;base64,

// 获取验证码
getValidateCode() {
  getCaptcha().then(response => {
	const {img, uuid} = response.data
	this.captchaUrl = "data:image/gif;base64," + img
	this.loginForm.uuid = uuid;
  })
}
接口请求

src/store/modules/user.js 设置请求参数

login({commit}, userInfo) {
  const {username, password, validateCode, uuid} = userInfo
  return new Promise((resolve, reject) => {
    login({  
      username: username,
      password: password,
      grant_type: 'captcha', // 授权模式指定为 captcha 验证码模式,原先为 password 密码模式
      uuid: uuid, // 从Redis获取正确验证码的标识
      validateCode: validateCode // 验证码
    }).then(response => {
      const {access_token, refresh_token, token_type} = response.data
      const token = token_type + " " + access_token
      commit('SET_TOKEN', token)
      setToken(token)
      setRefreshToken(refresh_token)
      resolve()
    }).catch(error => {
      reject(error)
    })
  })

src/api/user.js 请求API设置请求头部

export function login(params) {
  return request({
    url: '/youlai-auth/oauth/token',
    method: 'post',
    params: params,
    headers: {
      'Authorization': 'Basic bWFsbC1hZG1pbi13ZWI6MTIzNDU2' // OAuth2客户端信息Base64加密,明文:mall-admin-web:123456
    }
  })
}

三. 手机短信验证码授权模式

1. 原理

手机短信验证码模式时序图如下,变动的角色还是用绿色背景标识。可以看到扩展是对授权者 Granter 和认证提供者 Provider 做切入口。

手机短信验证码授权流程: 流程基本上和密码模式一致,根据 grant_type 匹配授权者 SmsCodeTokenGranter , 委托给 ProviderManager 进行认证,根据 SmsCodeAuthenticationToken的匹配认证提供者 SmsCodeAuthenticationProvider 进行短信验证码校验。

2. 实战

2.1 手机短信验证码授权模式扩展

SmsCodeTokenGranter
/**
 * 手机验证码授权者
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 * @date 2021/9/25
 */
public class SmsCodeTokenGranter extends AbstractTokenGranter {

    /**
     * 声明授权者 CaptchaTokenGranter 支持授权模式 sms_code
     * 根据接口传值 grant_type = sms_code 的值匹配到此授权者
     * 匹配逻辑详见下面的两个方法
     *
     * @see org.springframework.security.oauth2.provider.CompositeTokenGranter#grant(String, TokenRequest)
     * @see org.springframework.security.oauth2.provider.token.AbstractTokenGranter#grant(String, TokenRequest)
     */
    private static final String GRANT_TYPE = "sms_code";
    private final AuthenticationManager authenticationManager;

    public SmsCodeTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService,
                               OAuth2RequestFactory requestFactory, AuthenticationManager authenticationManager
    ) {
        super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

        Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());

        String mobile = parameters.get("mobile"); // 手机号
        String code = parameters.get("code"); // 短信验证码

        parameters.remove("code");

        Authentication userAuth = new SmsCodeAuthenticationToken(mobile, code);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);

        try {
            userAuth = this.authenticationManager.authenticate(userAuth);
        } catch (AccountStatusException var8) {
            throw new InvalidGrantException(var8.getMessage());
        } catch (BadCredentialsException var9) {
            throw new InvalidGrantException(var9.getMessage());
        }

        if (userAuth != null && userAuth.isAuthenticated()) {
            OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
            return new OAuth2Authentication(storedOAuth2Request, userAuth);
        } else {
            throw new InvalidGrantException("Could not authenticate user: " + mobile);
        }
    }
}

SmsCodeAuthenticationProvider
/**
 * 短信验证码认证授权提供者
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 * @date 2021/9/25
 */
@Data
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;
    private MemberFeignClient memberFeignClient;
    private StringRedisTemplate redisTemplate;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
        String mobile = (String) authenticationToken.getPrincipal();
        String code = (String) authenticationToken.getCredentials();

        String codeKey = AuthConstants.SMS_CODE_PREFIX + mobile;
        String correctCode = redisTemplate.opsForValue().get(codeKey);
        // 验证码比对
        if (StrUtil.isBlank(correctCode) || !code.equals(correctCode)) {
            throw new BizException("验证码不正确");
        } else {
            redisTemplate.delete(codeKey);
        }
        UserDetails userDetails = ((MemberUserDetailsServiceImpl) userDetailsService).loadUserByMobile(mobile);
        WechatAuthenticationToken result = new WechatAuthenticationToken(userDetails, new HashSet<>());
        result.setDetails(authentication.getDetails());
        return result;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }
}
AuthorizationServerConfig

在认证中心配置把 SmsCodeTokenGranter 添加到认证器的授权类型的集合中去。

2.2 阿里云免费短信申请

访问 https://free.aliyun.com/product/cloudcommunication-free-trial?spm=5176.10695662.1128094.7.2a6b4bee30xtJx 申请阿里云免费短信试用

添加签名,等待审核通过

签名审核通过之后就可以创建 AccessKey 访问密钥

添加模板, 国内消息 → 模板管理 → 添加模板

签名审核通过后得到 AccessKey 和 模板审核通过得到模板CODE,接下来就可以进行项目整合了。

2.3 SpringBoot 整合阿里云 SMS 短信

SpringBoot 整合 SMS 网上教程很多,这里不画蛇添足,接下来简单说下 youlai-mall 整合阿里云 SMS 短信。完整源码

按惯例把短信封装成一个公共模块以便给其他需要短信的应用模块引用。

youlai-auth 引入 common-sms 依赖

<dependencies> 
    <dependency>
        <groupId>com.youlai</groupId>
        <artifactId>common-sms</artifactId>
    </dependency>
</dependencies>

其中 AliyunSmsProperties 需要的属性需要配置在 Nacos 的配置中心文件 youlai-auth.yaml

# 阿里云短信配置
aliyun:
  sms:
    accessKeyId: LTAI5tSxxxxxxNcD6diBJLyR
    accessKeySecret: SoOWRqpjtSxxxxxxM8QZ2PZiMTJOVC
    domain: dysmsapi.aliyuncs.com 
    regionId: cn-shanghai
    templateCode: SMS_225xxx770
    signName: 有来技术

发送短信验证码接口

@Api(tags = "短信验证码")
@RestController
@RequestMapping("/sms-code")
@RequiredArgsConstructor
public class SmsCodeController {

    private final AliyunSmsService aliyunSmsService;

    @ApiOperation(value = "发送短信验证码")
    @ApiImplicitParam(name = "phoneNumber", example = "17621590365", value = "手机号", required = true)
    @PostMapping
    public Result sendSmsCode(String phoneNumber)  {
        boolean result = aliyunSmsService.sendSmsCode(phoneNumber);
        return Result.judge(result);
    }
}

2.4 移动端接入短信验证码授权模式

有来移动端 mall-app 使用 uni-app 跨平台应用的前端框架。因为一直以来有来商城都是以微信小程序的一个端呈现,所以 uni-app 的强大之处没法体现。借着这次给 mall-app 扩展手机短信验证码的授权模式的机会,为 H5、Android和IOS 添加手机短信验证码的登录界面。

先看下 mall-app 登录界面 在H5/Android/IOS 和 微信小程序的不同呈现效果。

H5/Android/IOS 登录界面 微信小程序 登录界面

登录页面 /pages/login/login.vue 在不同的平台有不同的呈现实现原理是通过 #ifdef MP #ifndef MP 条件编译指令实现的,其中 #ifdef MP 是在小程序平台编译生效,而 #ifdef MP 是非小程序平台编译生效。

在开发编译时,当在 HBuilderX 工具栏点击运行选择不同的平台会有不同的页面呈现。

  1. 运行 → 运行到内置浏览器 → 手机短信验证码登录界面;
  2. 运行 → 运行到小程序模拟器 → 微信开发者工具 → 小程序授权登录界面;

说到接入 Spring Security OAuth2 扩展的手机短信验证码,重要的还是看如何传参。在 mall-app/api/user.js 代码:

// H5/Android/IOS 手机短信验证码登录
// #ifndef MP
export function login( mobile,code) {
	return request({
		url: '/youlai-auth/oauth/token',
		method: 'post',
		params: {
			mobile: mobile,
			code: code,
			grant_type: 'sms_code'
		},
		headers: {
			'Authorization': 'Basic bWFsbC1hcHA6MTIzNDU2' // 客户端信息Base64加密,明文:mall-app:123456
		}
	})
}
// #endif

赋予mall-app 客户端支持 sms_code 模式

3. 测试

到此H5/Android/IOS移动端接入 Spring Security OAuth2 扩展的手机短信验证码授权模式已经完成。接下来扩展的授权模式是针对当下最火的微信小程序移动端的授权登录。

四. 微信授权模式

1. 原理

微信小程序登录授权流程图如下,我们所扮演的角色是 开发者服务器,主要的工作是接收小程序端的 code 从微信服务器获取 openidsession_key 后在开发者服务器生成会话(token)返回给小程序,后续小程序携带token和开发者服务器进行交互,也就没有微信服务器啥事了。

Spring Security OAuth2 微信授权扩展和上面的手机短信验证码原理一样,添加授权者 WechatTokenGranter 构建 WechatAuthenticationToken , 匹配到认证提供者 WechatAuthenticationProvider ,在其 authenticate 方法完成认证授权逻辑。

2. 实战

2.1 微信授权模式扩展

WechatTokenGranter

WechatTokenGranter 微信授权者接收 codeencryptedDataiv 构建 WechatAuthenticationToken

/**
 *  微信授权者
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 * @date 2021/9/25
 */
public class WechatTokenGranter extends AbstractTokenGranter {

    /**
     * 声明授权者 CaptchaTokenGranter 支持授权模式 wechat
     * 根据接口传值 grant_type = wechat 的值匹配到此授权者
     * 匹配逻辑详见下面的两个方法
     *
     * @see org.springframework.security.oauth2.provider.CompositeTokenGranter#grant(String, TokenRequest)
     * @see org.springframework.security.oauth2.provider.token.AbstractTokenGranter#grant(String, TokenRequest)
     */
    private static final String GRANT_TYPE = "wechat";
    private final AuthenticationManager authenticationManager;

    public WechatTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, AuthenticationManager authenticationManager) {
        super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

        Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());
        String code = parameters.get("code");
        String encryptedData = parameters.get("encryptedData");
        String iv = parameters.get("iv");

        parameters.remove("code");
        parameters.remove("encryptedData");
        parameters.remove("iv");

        Authentication userAuth = new WechatAuthenticationToken(code, encryptedData,iv); // 未认证状态
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);

        try {
            userAuth = this.authenticationManager.authenticate(userAuth); // 认证中
        } catch (Exception e) {
            throw new InvalidGrantException(e.getMessage());
        }

        if (userAuth != null && userAuth.isAuthenticated()) { // 认证成功
            OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
            return new OAuth2Authentication(storedOAuth2Request, userAuth);
        } else { // 认证失败
            throw new InvalidGrantException("Could not authenticate code: " + code);
        }
    }
}
WechatAuthenticationProvider

最终在微信认证提供者的 authenticate() 方法里完成认证逻辑,成功返回token。

/**
 * 微信认证提供者
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 * @date 2021/9/25
 */
@Data
public class WechatAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;
    private WxMaService wxMaService;
    private MemberFeignClient memberFeignClient;

    /**
     * 微信认证
     *
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        WechatAuthenticationToken authenticationToken = (WechatAuthenticationToken) authentication;
        String code = (String) authenticationToken.getPrincipal();

        WxMaJscode2SessionResult sessionInfo = null;
        try {
            sessionInfo = wxMaService.getUserService().getSessionInfo(code);
        } catch (WxErrorException e) {
            e.printStackTrace();
        }
        String openid = sessionInfo.getOpenid();
        Result<MemberAuthDTO> memberAuthResult = memberFeignClient.loadUserByOpenId(openid);
        // 微信用户不存在,注册成为新会员
        if (memberAuthResult != null && ResultCode.USER_NOT_EXIST.getCode().equals(memberAuthResult.getCode())) {

            String sessionKey = sessionInfo.getSessionKey();
            String encryptedData = authenticationToken.getEncryptedData();
            String iv = authenticationToken.getIv();
            // 解密 encryptedData 获取用户信息
            WxMaUserInfo userInfo = wxMaService.getUserService().getUserInfo(sessionKey, encryptedData, iv);

            UmsMember member = new UmsMember();
            BeanUtil.copyProperties(userInfo, member);
            member.setOpenid(openid);
            member.setStatus(GlobalConstants.STATUS_YES);
            memberFeignClient.add(member);
        }
        UserDetails userDetails = ((MemberUserDetailsServiceImpl) userDetailsService).loadUserByOpenId(openid);
        WechatAuthenticationToken result = new WechatAuthenticationToken(userDetails, new HashSet<>());
        result.setDetails(authentication.getDetails());
        return result;
    }


    @Override
    public boolean supports(Class<?> authentication) {
        return WechatAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

2.2 微信小程序接入微信授权模式

同样是在 mall-app 的接口文件中 /api/user.js,先让我们看下小程序端如何传值?

// 小程序授权登录
// #ifdef MP
export function login(code, encryptedData,iv) {
	return request({
		url: '/youlai-auth/oauth/token',
		method: 'post',
		params: {
			code: code,
			encryptedData: encryptedData,
			iv:iv,
			grant_type: 'wechat'
		},
		headers: {
			'Authorization': 'Basic bWFsbC13ZWFwcDoxMjM0NTY=' // 客户端信息Base64加密,明文:mall-weapp:123456
		}
	})
}
// #endif

设置 OAuth2 客户端支持 wechat 授权模式

3. 测试

到此微信授权扩展完成,实际业务场景常用的3种授权模式也就告一段落。

但是如果你对 Spring Security OAuth2 有些了解的话,你会有疑问这些扩展的模式对应的刷新模式需不需要做什么调整呢?

如果扩展只是针对一种用户体系以及一种认证方式(用户名/手机号/openid)的话,比如验证码 模式的扩展,就不需要对刷新模式做调整。

但是如果是多用户体系或者多种认证方式,youlai-mall 就是多用户体系以及多种认证方式,这时你必须做些调整来适配,不过改动不大,具体为什么调整和如何调整下文细说。

五. 多用户体系刷新模式

1. 原理

刷新模式 时序图如下,相较于密码模式还只是 GranterProvider的变动。

着重说一下刷新模式的认证提供者 PreAuthenticatedAuthenticationProvider ,其 authenticate() 认证方法只做用户状态校验,check() 方法调用 AccountStatusUserDetailsChecker#check(UserDetails)。

注意 下this.preAuthenticatedUserDetailsService.loadUserDetails((PreAuthenticatedAuthenticationToken)authentication);preAuthenticatedUserDetailsService 用户服务。

在没有进行授权模式扩展的时候,是下面这样设置的

然后在 AuthorizationServerEndpointsConfigurer#addUserDetailsService(DefaultTokenServices,UserDetailsService) 构造 PreAuthenticatedAuthenticationProvider 里设置了 UserDetailService用户服务。

这样在多用户体系认证下问题可想而知,用户分别有系统用户和会员用户,这里固定成一个用户服务肯定是行不通的,扩展授权模式创建 Provider 时可以指定具体的用户服务 UserDetailService,就如下面这样:

你可以为每个授权模式扩展新增对应的刷新模式,但是这样的话比较麻烦,本文的实现方案核心图的是简单有效,所以这里使用的另一种方案,重新设置PreAuthenticatedAuthenticationProvider 的 preAuthenticatedUserDetailsService 属性,让其有判断选择用户体系和认证方式的能力。

2. 实战

首先我们清楚一个 OAuth2 客户端基本对应的是一个用户体系,比如 youlai-mall 项目的客户端和用户体系对应关系如下表:

OAuth2 客户端名称 OAuth2 客户端ID 用户体系
管理系统 mall-admin-web 系统用户
H5/Android/IOS 移动端 mall-app 商城会员
小程序端 mall-weapp 商城会员

那就有一个很简单有效的思路,可以在系统内部维护一个如上表的映射关系 Map,然后根据传递的客户端ID去选择用户体系。

就这?当然不是,还有个点你必须要考虑到,举个例子虽然移动端的用户体系是会员用户,但是它可能有多种认证方式呀,比如可以同时支持手机短信验证码和用户名密码甚至更多的认证方式。

而 Spring Security OAuth2 默认的 UserDetailsService 接口只有一个 loadUserByUsername() 方法,很显然是做不到会员体系支持多种认证方式的。

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

所以需要在 UserDetailsService 的实现类新增认证方式,然后在运行时将 UserDetailsService 转为具体的实现类,具体可看下有来项目的 MemberUserDetailsServiceImpl 的实现,同时支持手机号和三方标识 openid 获取用户认证信息,即两种不同的认证方式。

/**
 * 商城会员用户认证服务
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 */
@Service("memberUserDetailsService")
@RequiredArgsConstructor
public class MemberUserDetailsServiceImpl implements UserDetailsService {

    private final MemberFeignClient memberFeignClient;

    @Override
    public UserDetails loadUserByUsername(String username) {
        return null;
    }

    /**
     * 手机号码认证方式
     *
     * @param mobile
     * @return
     */
    public UserDetails loadUserByMobile(String mobile) {
        MemberUserDetails userDetails = null;
        Result<MemberAuthDTO> result = memberFeignClient.loadUserByMobile(mobile);
        if (Result.isSuccess(result)) {
            MemberAuthDTO member = result.getData();
            if (null != member) {
                userDetails = new MemberUserDetails(member);
                userDetails.setAuthenticationMethod(AuthenticationMethodEnum.MOBILE.getValue());   // 认证方式:OpenId
            }
        }
        if (userDetails == null) {
            throw new UsernameNotFoundException(ResultCode.USER_NOT_EXIST.getMsg());
        } else if (!userDetails.isEnabled()) {
            throw new DisabledException("该账户已被禁用!");
        } else if (!userDetails.isAccountNonLocked()) {
            throw new LockedException("该账号已被锁定!");
        } else if (!userDetails.isAccountNonExpired()) {
            throw new AccountExpiredException("该账号已过期!");
        }
        return userDetails;
    }


    /**
     * openid 认证方式
     *
     * @param openId
     * @return
     */
    public UserDetails loadUserByOpenId(String openId) {
        MemberUserDetails userDetails = null;
        Result<MemberAuthDTO> result = memberFeignClient.loadUserByOpenId(openId);
        if (Result.isSuccess(result)) {
            MemberAuthDTO member = result.getData();
            if (null != member) {
                userDetails = new MemberUserDetails(member);
                userDetails.setAuthenticationMethod(AuthenticationMethodEnum.OPENID.getValue());   // 认证方式:OpenId
            }
        }
        if (userDetails == null) {
            throw new UsernameNotFoundException(ResultCode.USER_NOT_EXIST.getMsg());
        } else if (!userDetails.isEnabled()) {
            throw new DisabledException("该账户已被禁用!");
        } else if (!userDetails.isAccountNonLocked()) {
            throw new LockedException("该账号已被锁定!");
        } else if (!userDetails.isAccountNonExpired()) {
            throw new AccountExpiredException("该账号已过期!");
        }
        return userDetails;
    }
}

新增的 PreAuthenticatedUserDetailsService 可根据客户端和认证方式选择UserDetailService 和方法获取用户信息 UserDetail

/**
 * 刷新token再次认证 UserDetailsService
 *
 * @author <a href="mailto:xianrui0365@163.com">xianrui</a>
 * @date 2021/10/2
 */
@NoArgsConstructor
public class PreAuthenticatedUserDetailsService<T extends Authentication> implements AuthenticationUserDetailsService<T>, InitializingBean {

    /**
     * 客户端ID和用户服务 UserDetailService 的映射
     *
     * @see com.youlai.auth.security.config.AuthorizationServerConfig#tokenServices(AuthorizationServerEndpointsConfigurer)
     */
    private Map<String, UserDetailsService> userDetailsServiceMap;

    public PreAuthenticatedUserDetailsService(Map<String, UserDetailsService> userDetailsServiceMap) {
        Assert.notNull(userDetailsServiceMap, "userDetailsService cannot be null.");
        this.userDetailsServiceMap = userDetailsServiceMap;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(this.userDetailsServiceMap, "UserDetailsService must be set");
    }

    /**
     * 重写PreAuthenticatedAuthenticationProvider 的 preAuthenticatedUserDetailsService 属性,可根据客户端和认证方式选择用户服务 UserDetailService 获取用户信息 UserDetail
     *
     * @param authentication
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserDetails(T authentication) throws UsernameNotFoundException {
        String clientId = RequestUtils.getOAuth2ClientId();
        // 获取认证方式,默认是用户名 username
        AuthenticationMethodEnum authenticationMethodEnum = AuthenticationMethodEnum.getByValue(RequestUtils.getAuthenticationMethod());
        UserDetailsService userDetailsService = userDetailsServiceMap.get(clientId);
        if (clientId.equals(SecurityConstants.APP_CLIENT_ID)) {
            // 移动端的用户体系是会员,认证方式是通过手机号 mobile 认证
            MemberUserDetailsServiceImpl memberUserDetailsService = (MemberUserDetailsServiceImpl) userDetailsService;
            switch (authenticationMethodEnum) {
                case MOBILE:
                    return memberUserDetailsService.loadUserByMobile(authentication.getName());
                default:
                    return memberUserDetailsService.loadUserByUsername(authentication.getName());
            }
        } else if (clientId.equals(SecurityConstants.WEAPP_CLIENT_ID)) {
            // 小程序的用户体系是会员,认证方式是通过微信三方标识 openid 认证
            MemberUserDetailsServiceImpl memberUserDetailsService = (MemberUserDetailsServiceImpl) userDetailsService;
            switch (authenticationMethodEnum) {
                case OPENID:
                    return memberUserDetailsService.loadUserByOpenId(authentication.getName());
                default:
                    return memberUserDetailsService.loadUserByUsername(authentication.getName());
            }
        } else if (clientId.equals(SecurityConstants.ADMIN_CLIENT_ID)) {
            // 管理系统的用户体系是系统用户,认证方式通过用户名 username 认证
            switch (authenticationMethodEnum) {
                default:
                    return userDetailsService.loadUserByUsername(authentication.getName());
            }
        } else {
            return userDetailsService.loadUserByUsername(authentication.getName());
        }
    }
}

AuthorizationServerConfig 配置重新设置 PreAuthenticatedAuthenticationProvider 的 preAuthenticatedUserDetailsService 属性值

    /**
     * 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        // Token增强
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
        tokenEnhancers.add(tokenEnhancer());
        tokenEnhancers.add(jwtAccessTokenConverter());
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);

        // 获取原有默认授权模式(授权码模式、密码模式、客户端模式、简化模式)的授权者
        List<TokenGranter> granterList = new ArrayList<>(Arrays.asList(endpoints.getTokenGranter()));

        // 添加验证码授权模式授权者
        granterList.add(new CaptchaTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
                endpoints.getOAuth2RequestFactory(), authenticationManager, stringRedisTemplate
        ));

        // 添加手机短信验证码授权模式的授权者
        granterList.add(new SmsCodeTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
                endpoints.getOAuth2RequestFactory(), authenticationManager
        ));

        // 添加微信授权模式的授权者
        granterList.add(new WechatTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
                endpoints.getOAuth2RequestFactory(), authenticationManager
        ));

        CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(granterList);
        endpoints
                .authenticationManager(authenticationManager)
                .accessTokenConverter(jwtAccessTokenConverter())
                .tokenEnhancer(tokenEnhancerChain)
                .tokenGranter(compositeTokenGranter)
                /** refresh token有两种使用方式:重复使用(true)、非重复使用(false),默认为true
                 *  1 重复使用:access token过期刷新时, refresh token过期时间未改变,仍以初次生成的时间为准
                 *  2 非重复使用:access token过期刷新时, refresh token过期时间延续,在refresh token有效期内刷新便永不失效达到无需再次登录的目的
                 */
                .reuseRefreshTokens(true)
                .tokenServices(tokenServices(endpoints))
        ;
    }


    public DefaultTokenServices tokenServices(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
        tokenEnhancers.add(tokenEnhancer());
        tokenEnhancers.add(jwtAccessTokenConverter());
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);

        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(endpoints.getTokenStore());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setClientDetailsService(clientDetailsService);
        tokenServices.setTokenEnhancer(tokenEnhancerChain);

        // 多用户体系下,刷新token再次认证客户端ID和 UserDetailService 的映射Map
        Map<String, UserDetailsService> clientUserDetailsServiceMap = new HashMap<>();
        clientUserDetailsServiceMap.put(SecurityConstants.ADMIN_CLIENT_ID, sysUserDetailsService); // 管理系统客户端
        clientUserDetailsServiceMap.put(SecurityConstants.APP_CLIENT_ID, memberUserDetailsService); // Android/IOS/H5 移动客户端
        clientUserDetailsServiceMap.put(SecurityConstants.WEAPP_CLIENT_ID, memberUserDetailsService); // 微信小程序客户端

        // 重新设置PreAuthenticatedAuthenticationProvider#preAuthenticatedUserDetailsService 能够根据客户端ID和认证方式区分用户体系获取认证用户信息
        PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
        provider.setPreAuthenticatedUserDetailsService(new PreAuthenticatedUserDetailsService<>(clientUserDetailsServiceMap));
        tokenServices.setAuthenticationManager(new ProviderManager(Arrays.asList(provider)));
        return tokenServices;
    }

核心代码基本都在上面,在完成以上的调整之后刷新模式就可以了,接下来对新扩展的授权模式对应的刷新模式进行逐一测试。

3. 测试

3.1 Postman 导入 cURL 操作说明

下面所有的测试都会把 cURL 贴出来,至于为什么强调这个?原来以为我把用 Postman 测试 Spring Security OAuth2 获取 token 的完整请求截图放入项目说明文档 README.md 这样就不会再有人问登录接口 403 报错,但事实反馈确实自己挺失望,以致于后来再有这样的问题基本上选择沉默了,希望大家换位思考理解下。所以这次想到的方案是把接口信息以 cURL 的形式贴出来,然后直接导入 Postman 测试。

下面是有来项目获取 token 的 cURL

curl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?username=admin&password=123456&grant_type=password' \
--header 'Authorization: Basic bWFsbC1hZG1pbi13ZWI6MTIzNDU2'

进入 Postman 选择 File → Import → Raw text 把上面的 cURL 导入

3.2 密码模式测试

密码模式的测试使用的客户端信息, 客户端ID:客户端密钥: mall-admin-web:123456 ----- Base64在线编码 → bWFsbC1hZG1pbi13ZWI6MTIzNDU2

如果你要更改客户端,请在下方接口的请求头 Authorization 更换客户端信息即可,不然会报 403 提示,因为你的客户端信息不正确认证不成功禁止访问。

有些人会问现在有来项目没有自定义客户端认证异常的处理,其实在我之前的文章有提供解决方案 https://www.cnblogs.com/haoxianrui/p/14028366.html#3-客户端认证异常,有需要的可以根据文章调整。至于为什么项目中没有使用方案,首先觉得实现比较复杂,如果你有好的解决方案欢迎提出,另外这种客户端信息错误作为一个开发人员来说你是完全可以规避的。

获取token
curl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?username=admin&password=123456&grant_type=password' \
--header 'Authorization: Basic bWFsbC1hZG1pbi13ZWI6MTIzNDU2'

刷新token

refresh_token 需要替换,在第一步获取 token 返回的 refresh_token

curl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?refresh_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiJmYzdiOGNhZi1iNmI4LTRlZTEtOGE4OC0yYzdmZTcxNTA0YjEiLCJleHAiOjE2MzQ0NDg5NDIsInVzZXJJZCI6MiwiYXV0aG9yaXRpZXMiOlsiQURNSU4iXSwianRpIjoiOGU3ZWE5MjAtOGQ0Ni00NmFlLWI3ODYtZTc3ZjAxY2Y5ZjIyIiwiY2xpZW50X2lkIjoibWFsbC1hZG1pbi13ZWIiLCJ1c2VybmFtZSI6ImFkbWluIn0.I_9uLpr7WUeb-JNSBr17Ya59qP3a8EFSps3MwqpTS-mlDldx-HDsJM41Pl11-b_99_yhl_h-FRhIYpGaOqP4p7428z_LQmlpBrebx9TVcSk_gVbDPjN3Q2glxaupvCGmAuRNWby0Aam-On2wO8RkKKhH0arI2nf4rseu18WN0-cqxJuYn10hyQ-T7n5n3zjnx92nMyqESWqfPqsy8_eie-can4113PBHhnqs9QI1SQ-1Z_AtZLgAb1FzaV2JuTqqbPlVULM-uaQnIoe0zNq5R-TYoUJ2cQNkP4YOR4e9TP26iSPLNlcsg59TFHi0UhrZiZqvS3i5nUkqV0jpzvYVrg&grant_type=refresh_token' \
--header 'Authorization: Basic bWFsbC1hZG1pbi13ZWI6MTIzNDU2' 

3.3 验证码模式测试

验证码模式的测试使用客户端的信息, 客户端ID:客户端密钥: mall-admin-web:123456 ----- Base64在线编码 → bWFsbC1hZG1pbi13ZWI6MTIzNDU2

获取token
curl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?username=admin&password=123456&grant_type=captcha&uuid=11add22b38e74a57bade0bf628a70645&validateCode=1' \
--header 'Authorization: Basic bWFsbC1hZG1pbi13ZWI6MTIzNDU2

刷新token
curl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?refresh_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiJiMTU5ZGU2Ni1iYmY5LTRmOWEtYTg1MC1kMjk1MDJiYTNjY2IiLCJleHAiOjE2MzQ0NjQxNjUsInVzZXJJZCI6MiwiYXV0aG9yaXRpZXMiOlsiQURNSU4iXSwianRpIjoiN2MwNDk2YzgtMTRjMC00MWJhLTk2OTUtYTk2ZGYwODQ1NGMxIiwiY2xpZW50X2lkIjoibWFsbC1hZG1pbi13ZWIiLCJ1c2VybmFtZSI6ImFkbWluIn0.j3n1FrMEIRkb_-3YhoDdPA4qBofzjD4y6HWdhCRdIjWU3D1La9ee_guhdeEEL49sfdHQSek_T4funyUCegTCdxfowzh3JghtCXFyRdxSWxjgJalgSIGVcOSEePxADwf2biHB3m6WzpOT9FxEdBavT7mfdQRjfc276uL7zzi5blKc4pUzX9l1AvReMP7azT_6soBNi-nid5maUCpMx_w9AVUvjVl4L7QMCO22zEogs2SlpMpggAITMv3QKYYTZ3vzxL2oNR_r-9qXqN7W6DxGqQc1gIqXADX1oqsXzD4AaAtLqOslP8FM6HiOzzZVd1kmv1cPHzVzabx6vYUZFA1PMg&grant_type=refresh_token' \
--header 'Authorization: Basic bWFsbC1hZG1pbi13ZWI6MTIzNDU2'

3.4 手机短信验证码测试

手机短信验证码模式测试使用的客户端的信息, 客户端ID:客户端密钥: mall-app:123456 ----- Base64在线编码 → bWFsbC1hZG1pbi13ZWI6MTIzNDU2

获取 token
curl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?mobile=17621590365&code=666666&grant_type=sms_code' \
--header 'Authorization: Basic bWFsbC1hcHA6MTIzNDU2'

刷新token
curl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?refresh_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRoZW50aWNhdGlvbk1ldGhvZCI6Im1vYmlsZSIsInVzZXJfbmFtZSI6IjE3NjIxNTkwMzY1Iiwic2NvcGUiOlsiYWxsIl0sImF0aSI6IjBlZGMyZjI0LWFiNWUtNDkxYy1iYjAyLTdlOWJkN2U5M2Y0MiIsImV4cCI6MTYzNDQ2NTEzMCwidXNlcklkIjo1OSwianRpIjoiZjcyMWZhZjAtZTczMS00MmUxLTgxYjAtMjg4NDEwZjQzODA0IiwiY2xpZW50X2lkIjoibWFsbC1hcHAiLCJ1c2VybmFtZSI6IjE3NjIxNTkwMzY1In0.RdtJiNhk3OheoUcpUtM9JBgwLfSt1k3FhEvgMYeDSFwf28TeS_SF2LY7vzOrbJfYQZuaMzvMfoSljeDuQoBr38Ebh2LogbZClaDY72TO9P88DAW-1l2Rjm1XYFMEzCZYweDehT2tJU6eOwN8GZ40dzcCnqjZwgCKgoIdJksxMB6n96Kfmxw_Z3TUny5j2mdDZB79bwWci86jev6y-RUTjbZWRu1vH4MVJ0hCOCRARoem1jlkW6nnkzhE84OasDI9RCg5jsA_ZNs3x-rFNnRY7T5gQOAOwPvJKVcXww35BGYZGHCHqQb6QEbxul6Pg1rLjFU6YgsSO1Xq_cWVOt0Nvg&grant_type=refresh_token' \
--header 'Authorization: Basic bWFsbC1hcHA6MTIzNDU2'

3.5 微信授权模式测试

微信授权模式测试使用的客户端的信息, 客户端ID:客户端密钥: mall-weapp:123456 ----- Base64在线编码 → bWFsbC13ZWFwcDoxMjM0NTY=

获取token
curl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?code=063hEOFa1N1dWB0XpRIa1WvNw74hEOF-&encryptedData=1qmFeCKbTxZyCdzctu37sX+jOnM9dZG9lKyD3v6FhA5sCEtDwaF/wqyVR70QVrqt7bGVH+Kb+PBsFJlBXUdjnFGlrwmPqgNusI4f5eA8SvZgopvmlzJhXwe+OjLCQooeGnSkcnUrUuMA/G4ZYWFeljaHhxJq/75APWs4HyLANfbeLp50qI9xrRJVUXlTqdqJ0ub38ZxWVvWZMqY8FaskAiZpxzrF30eXu93BCpDavRCVzlSfv6LFJmmvEGVOKr4Wap9ND82N3sDMyArRsdhdhmoWIYBbRs+iLbKcS4WyOhpmaQr4fhhOuxO+zSAa7W+eNmCH2Id6Pgpvhl6ureNNzEb0cQLoksP6oakPmv/yEiw5fnW6Oi9jJbxzlMyORN3/atHgBl6zLIgS9UMhFE+42Vp5B3L8jLly4+B4NpNgol+khXoh+ycUXSRPV4bUuriv&iv=j+brWSrqRW+d4lAjRWW4RA==&grant_type=wechat' \
--header 'Authorization: Basic bWFsbC13ZWFwcDoxMjM0NTY='

刷新token

六. 总结

本篇基于 Spring Security OAuth2 扩展了实际开发常用的 验证码模式手机短信验证码模式微信授权模式并分别应用至有来商城的管理前端移动应用端和微信小程序端,同时稍调整刷新模式使其能够适配扩展的几种模式以及多用户体系。通过授权模式的扩展揭露 Spring Security OAuth2 的认证流程和底层原理,相信对流程和原理有个清晰的思路之后,不同的认证需求都可以做到得心应手。最后还是感叹下 Spring 框架的魅力,就是你能感受到它在功能的实现的基础上会给你留个扩展的入口,而不是让你想着去改它的源码去实现。最后希望大家都能收获些东西吧,虽然咱这也不图啥,写这些说实话对自己提升也不大,但毕竟是花了半个多月时间写的这篇文章,算是自己的一份心血,也不希望白费了。

七. 联系信息

有兴趣进交流群的同学加我微信(haoxianrui),备注 有来 即可,纯属学习交流群,无任何利益。另外如果有兴趣加入开源项目 youlai-mall 开发的欢迎私信我,或者能给项目提交PR的我联系您。

posted @ 2021-10-18 18:33  有来技术  阅读(19256)  评论(23编辑  收藏  举报