万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

2022年05月15日 阅读数:5
这篇文章主要向大家介绍万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

一. 前言

【APP 移动端】Spring Security OAuth2 手机短信验证码模式html

【微信小程序】Spring Security OAuth2 微信受权模式前端

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

【管理系统】Spring Security OAuth2 密码模式vue

【管理系统】Spring Security OAuth2 验证码模式java

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

Spring Security OAuth2 默认实现的四种受权模式在实际的应用场景中每每知足不了预期,如如下需求:web

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

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

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

  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 H五、移动端手机验证码登陆接入手机短信验证码受权模式

先作个很重要的声明吧,本篇文章涉及全部的代码地址:数据库

项目名称小程序

码云(Gitee)后端

GitHub

微服务后台

youlai-mall

youlai-mall

管理前端

mall-admin-web

mall-admin-web

微信小程序/H5/Android/IOS

mall-app

mall-app

由于涉及的内容不少,文章中作不到把全部的代码彻底贴出来,可是放心源码所有在线的,一样文档也是

二. 验证码受权模式

1. 原理

验证码受权模式是在密码模式基础添加个验证码校验,若是你有 无论功夫怎样,能打赢你的就是好功夫 这样的心态彻底可使用过滤器实现,但若是想不开的话那就试下扩展吧。

由于是基于密码受权模式的扩展,就先了解密码受权模式的流程吧。由于其余几种受权模式和密码模式实现原理都是同样,弄明白密码受权模式以后其余受权模式包括如何去扩展都是轻车熟路。

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

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

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

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

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

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

2. 实战

验证码受权模式涉及Spring Security OAuth2扩展验证码受权模式、后台生成验证码和前端登陆加入验证码三部分,涉及到先后端的东西,针对本身须要选择关注点便可。

2.1 验证码受权模式扩展

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

CaptchaTokenGranter

复制代码1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677JAVA/**
 * 验证码受权模式 受权者
 *
 * @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 的值 password 为 captcha;
  2. getOAuth2Authentication() 方法添加验证码校验逻辑。

AuthorizationServerConfig

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

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

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

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

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

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

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

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

2.2 Spring WebFlux 整合验证码Kaptcha

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

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

CaptchaHandler

复制代码1234567891011121314151617181920212223242526272829303132JAVA@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 的类路径

复制代码12JAVA// 验证码文本生成器 
properties.setProperty("kaptcha.textproducer.impl", "com.youlai.gateway.kaptcha.KaptchaTextCreator");

CaptchaRouter

复制代码12345678910JAVA@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 以下:

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

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

登陆页面

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

src/views/login/index.vue

复制代码123456789101112131415HTML <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,

复制代码12345678JAVASCRIPT// 获取验证码
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 设置请求参数

复制代码1234567891011121314151617181920JAVASCRIPTlogin({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设置请求头部

复制代码12345678910JAVASCRIPTexport 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 作切入口。

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

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

2. 实战

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

SmsCodeTokenGranter

复制代码1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556JAVA/**
 * 手机验证码受权者
 *
 * @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

复制代码123456789101112131415161718192021222324252627282930313233343536373839JAVA/**
 * 短信验证码认证受权提供者
 *
 * @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 添加到认证器的受权类型的集合中去。

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

2.2 阿里云免费短信申请

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

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

添加签名,等待审核经过

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

签名审核经过以后就能够建立 AccessKey 访问密钥

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

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

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

签名审核经过后获得 AccessKey 和 模板审核经过获得模板CODE,接下来就能够进行项目整合了。

2.3 SpringBoot 整合阿里云 SMS 短信

SpringBoot 整合 SMS 网上教程不少,这里不多此一举,接下来简单说下 youlai-mall 整合阿里云 SMS 短信。完整源码

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

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

youlai-auth 引入 common-sms 依赖

复制代码123456XML<dependencies> 
    <dependency>
        <groupId>com.youlai</groupId>
        <artifactId>common-sms</artifactId>
    </dependency>
</dependencies>

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

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

发送短信验证码接口

复制代码12345678910111213141516JAVA@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 扩展手机短信验证码的受权模式的机会,为 H五、Android和IOS 添加手机短信验证码的登陆界面。

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

H5/Android/IOS 登陆界面

微信小程序 登陆界面

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

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

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

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

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

复制代码1234567891011121314151617JAVASCRIPT// 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 模式

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

3. 测试

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

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

四. 微信受权模式

1. 原理

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

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

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

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

2. 实战

2.1 微信受权模式扩展

WechatTokenGranter

WechatTokenGranter 微信受权者接收 code 、encryptedData 、iv 构建 WechatAuthenticationToken

复制代码1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253JAVA/**
 *  微信受权者
 *
 * @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。

复制代码123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960JAVA/**
 * 微信认证提供者
 *
 * @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,先让咱们看下小程序端如何传值?

复制代码123456789101112131415161718JAVASCRIPT// 小程序受权登陆
// #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 受权模式

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

3. 测试

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

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

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

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

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

五. 多用户体系刷新模式

1. 原理

刷新模式 时序图以下,相较于密码模式还只是 Granter 和 Provider的变更。

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

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

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

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

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

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

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

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

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

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

你能够为每一个受权模式扩展新增对应的刷新模式,可是这样的话比较麻烦,本文的实现方案核心图的是简单有效,因此这里使用的另外一种方案,从新设置
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() 方法,很显然是作不到会员体系支持多种认证方式的。

复制代码123JAVApublic interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

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

复制代码12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273JAVA/**
 * 商城会员用户认证服务
 *
 * @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

复制代码1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768JAVA/**
 * 刷新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 属性值

复制代码1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071JAVA    /**
     * 配置受权(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

复制代码12SHELLcurl --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 导入

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

3.2 密码模式测试

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

若是你要更改客户端,请在下方接口的请求头 Authorization 更换客户端信息便可,否则会报 403 提示,由于你的客户端信息不正确认证不成功禁止访问。

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

获取token

复制代码12SHELLcurl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?username=admin&password=123456&grant_type=password' \
--header 'Authorization: Basic bWFsbC1hZG1pbi13ZWI6MTIzNDU2'

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

刷新token

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

复制代码12SHELLcurl --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' 

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

3.3 验证码模式测试

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

获取token

复制代码12SHELLcurl --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

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

刷新token

复制代码12SHELLcurl --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'

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

3.4 手机短信验证码测试

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

获取 token

复制代码12SHELLcurl --location --request POST 'http://localhost:9999/youlai-auth/oauth/token?mobile=17621590365&code=666666&grant_type=sms_code' \
--header 'Authorization: Basic bWFsbC1hcHA6MTIzNDU2'

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

刷新token

复制代码12SHELLcurl --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'

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

3.5 微信受权模式测试

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

获取token

复制代码12SHELLcurl --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='

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

刷新token

万字长文一套搞定Spring Security OAuth2受权模式扩展以及应用实战

六. 总结

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


若是以为本文对你有帮助,能够转发关注支持一下~