• No8.【spring-cloud-alibaba】基于OAuth2,新增加手机号验证码登录模式(不包含发短信,还没找到合适的短信发送平台)


    PigUserDetailsService  代码地址与接口文档看总目录:【学习笔记】记录冷冷-pig项目的学习过程,大概包括Authorization Server、springcloud、Mybatis Plus~~~_清晨敲代码的博客-CSDN博客


    终于结束从零搭建springcloud的部分了,目前也仅仅是学习了最最基本的逻辑,同时包含了开发系统的一些基本的逻辑。接下来就按照 pig 文档将其余基本的内容再熟悉一下,看一遍和写一遍真的不一样呐~~~

    那接下来就一小模块一小模块的学习啦,加油吧少年!

    本文及以后的文章还是基于前面的No6系列文章开发的,可以看之前文章顶部的内容总结,简单了解详情~

    目录

    A1.手机号验证码登录模式

    B1.步骤

    B2.编码

    B3.测试


    A1.手机号验证码登录模式

    B1.步骤

    首先,需要提供一个根据手机号码获取验证码的接口,这个接口写在 pig-upms-biz 里面就行。而且上一篇在 pig-gateway 网关已经有了校验验证码过滤器,所以也不需要编写验证码校验了,用一个就行。

    然后在 pig-auth 模块里面继承一套基于 OAuth2ResourceOwnerBaseAuthenticationXXXX 的 converter、token、provider 类,是用于 OAuth2 用户认证的,然后在修改一下用户认证里面的密码校验逻辑,对于短信验证登录模式是不校验密码的~

    B2.编码

    1.在 pig-upms-biz 模块里面添加根据手机号码获取验证码的接口,其中判断手机号码是否存在账号,并且判断是否有未过期校验码,最终将以手机号码为key,验证码为value保存到redis里面,并返回。【返回前要调用发送短信验证码逻辑发送短信~,这里我就不加了】;

    2.然后上面的controller类中增加一个根据手机号码获取用户信息的方法。

    【用于 UserDetailsService  里面获取用户信息】

    3.在 pig-upms-api 模块里面的 RemoteUserService 接口中新建一个远程调用接口方法,用来远程调用上面的方法;

    4.在 pig-commin-security 里面新建一个实现 PigUserDetailsService 接口的类,用来根据手机号码拿到用户信息,其中会用到上面的远程接口方法;并且将他添加到 spring 容器中;

    5.修改  PigDaoAuthenticationProvider#retrieveUser() 方法,原来默认就是拿到排序最高的,现在需要根据 grantType 判断具体用哪一个 UserDetailsService 来获取用户信息。并且一定要修改密码验证方法,在里面加入判断,如果是短信登录模式模式就不需要校验密码,因为短信登录模式没有密码嘛~

    6.在 pig-auth 模块里面继承一套基于 OAuth2ResourceOwnerBaseAuthenticationXXXX 的 converter、token、provider 类,完全按照密码模式登录的编写就好;

    7.修改 AuthorizationServerConfiguration 类,将短信验证码模式的 converter 和 provider 添加到配置里面;

    8.在数据库中找到 sys_oauth_client_details 表,给使用的客户端账号 authorized_grant_types 里面加上短信验证登录的标识,只有有该登录模式的标识才能够使用该登录模式。


    1. //1.在 pig-upms-biz 模块里面添加根据手机号码获取验证码的接口,其中判断手机号码是否存在账号,并且判断是否有未过期校验码,最终将以手机号码为key,验证码为value保存到redis里面,并返回。【返回前要调用发送短信验证码逻辑发送短信~,这里我就不加了】;
    2. @RestController
    3. @AllArgsConstructor
    4. @RequestMapping("/app")
    5. public class AppController {
    6. private final AppService appService;
    7. /**
    8. * @Description: 根据手机号码获取验证码【注意生产环境记得将返回的code去掉~】
    9. * @param: * @param mobile
    10. * @return: com.pig4cloud.pig.common.core.util.R
    11. **/
    12. @Inner(value = false)
    13. @GetMapping("/{mobile}")
    14. public R sendSmsCode(@PathVariable String mobile) {
    15. return appService.sendSmsCode(mobile);
    16. }
    17. }
    18. //因为用不到其他的 mapper ,所以就不用继承 mps 自带的 service 了
    19. @Slf4j
    20. @Service
    21. @RequiredArgsConstructor
    22. public class AppServiceImpl implements AppService {
    23. private final RedisTemplate redisTemplate;
    24. private final SysUserMapper userMapper;
    25. @Override
    26. public R sendSmsCode(String mobile) {
    27. //根据手机号码获取用户信息
    28. List userList = userMapper.selectList(Wrappers.query().lambda().eq(SysUser::getPhone, mobile));
    29. //判断该手机号码是否有注册的用户,没有就直接返回
    30. if (CollUtil.isEmpty(userList)) {
    31. log.info("手机号未注册:{}", mobile);
    32. return R.ok(Boolean.FALSE, "手机号未注册:{"+mobile+"}");
    33. }
    34. //根据手机号码从 redis 里面获取 code
    35. Object codeObj = redisTemplate.opsForValue().get(CacheConstants.DEFAULT_CODE_KEY + mobile);
    36. //判断该手机号码是否有未失效的验证码,没有就直接返回
    37. if (codeObj != null) {
    38. log.info("手机号验证码未过期:{},{}", mobile, codeObj);
    39. return R.ok(Boolean.FALSE, "请勿频繁获取验证码");
    40. }
    41. //在这里生成 code
    42. String code = RandomUtil.randomNumbers(Integer.parseInt(SecurityConstants.CODE_SIZE));
    43. log.info("手机号生成验证码成功:{},{}", mobile, code);
    44. //将手机号码为key,验证码为value保存到redis里面
    45. redisTemplate.opsForValue()
    46. .set(CacheConstants.DEFAULT_CODE_KEY + mobile, code, SecurityConstants.CODE_TIME, TimeUnit.SECONDS);
    47. // todo 记得调用短信通道发送
    48. return R.ok(Boolean.TRUE, code);
    49. }
    50. }
    1. //2.然后上面的controller类中增加一个根据手机号码获取用户信息的方法。
    2. @RestController
    3. @AllArgsConstructor
    4. @RequestMapping("/app")
    5. public class AppController {
    6. private final AppService appService;
    7. private final SysUserService userService;
    8. /**
    9. * @Description: 获取指定用户全部信息
    10. * @param: * @param phone
    11. * @return: com.pig4cloud.pig.common.core.util.R
    12. **/
    13. @Inner
    14. @GetMapping("/info/{phone}")
    15. public R infoByMobile(@PathVariable String phone) {
    16. SysUser user = userService.getOne(Wrappers.query().lambda().eq(SysUser::getPhone, phone));
    17. if (user == null) {
    18. return R.failed("用户信息为空");
    19. }
    20. return R.ok(userService.getUserInfo(user));
    21. }
    22. }
    1. //3.在 pig-upms-api 模块里面的 RemoteUserService 接口中新建一个远程调用接口方法,用来远程调用上面的方法;
    2. @FeignClient(contextId = "remoteUserService", value = ServiceNameConstants.UMPS_SERVICE)
    3. public interface RemoteUserService {
    4. /**
    5. * @Description: 通过手机号码查询用户、角色信息
    6. * @param: * @param phone
    7. * @param from
    8. * @return: com.pig4cloud.pig.common.core.util.R
    9. **/
    10. @GetMapping("/app/info/{phone}")
    11. R infoByMobile(@PathVariable("phone") String phone, @RequestHeader(SecurityConstants.FROM) String from);
    12. }
    1. //4.在 pig-commin-security 里面新建一个实现 PigUserDetailsService 接口的类,用来根据手机号码拿到用户信息,其中会用到上面的远程接口方法;并且将他添加到 spring 容器中;
    2. @Slf4j
    3. @RequiredArgsConstructor
    4. public class PigAppUserDetailsServiceImpl implements PigUserDetailsService {
    5. private final RemoteUserService remoteUserService;
    6. /**
    7. * @Description: 手机号登录
    8. * @param: * @param username
    9. * @return: org.springframework.security.core.userdetails.UserDetails
    10. **/
    11. @Override
    12. public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {
    13. R result = remoteUserService.infoByMobile(phone, SecurityConstants.FROM_SECRET_VALUE);
    14. UserDetails userDetails = getUserDetails(result);
    15. return userDetails;
    16. }
    17. }
    18. //在 /resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 里面加上 PigAppUserDetailsServiceImpl 类
    19. com.pig4cloud.pig.common.security.service.PigAppUserDetailsServiceImpl
    1. //5.修改  PigDaoAuthenticationProvider#retrieveUser() 方法,原来默认就是拿到排序最高的,现在需要根据 grantType 判断具体用哪一个 UserDetailsService 来获取用户信息。并且一定要修改密码验证方法,在里面加入判断,如果是短信登录模式模式就不需要校验密码,因为短信登录模式没有密码嘛~
    2. //密码模式登录的 service 用的是父类提供的,并且他的 Order 是最小的,因此不会影响其他模式的
    3. @Slf4j
    4. @RequiredArgsConstructor
    5. public class PigAppUserDetailsServiceImpl implements PigUserDetailsService {
    6. /**
    7. * @Description: 是否支持此客户端校验
    8. * @param: * @param grantType
    9. * @return: boolean
    10. **/
    11. @Override
    12. public boolean support(String grantType) {
    13. return SecurityConstants.APP.equals(grantType);
    14. }
    15. }
    16. //修改此类的两个方法
    17. public class PigDaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    18. @Override
    19. protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    20. // 短息验证码模式不用校验密码
    21. String grantType = Optional.ofNullable(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest())
    22. .get().getParameter(OAuth2ParameterNames.GRANT_TYPE);
    23. if (StrUtil.equals(SecurityConstants.APP, grantType)) {
    24. return;
    25. }
    26. if (authentication.getCredentials() == null) {
    27. this.logger.debug("Failed to authenticate since no credentials provided");
    28. throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
    29. } else {
    30. String presentedPassword = authentication.getCredentials().toString();
    31. if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
    32. this.logger.debug("Failed to authenticate since password does not match stored value");
    33. throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
    34. }
    35. }
    36. }
    37. @SneakyThrows
    38. @Override
    39. protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    40. //为计时攻击的防御做准备
    41. prepareTimingAttackProtection();
    42. //拿到当前请求 request
    43. HttpServletRequest request = Optional.ofNullable(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest())
    44. .orElseThrow((Supplier) () -> new InternalAuthenticationServiceException("web request is empty"));
    45. //在 request 里面拿到 grant_type
    46. Map paramMap = ServletUtil.getParamMap(request);
    47. String grantType = paramMap.get(OAuth2ParameterNames.GRANT_TYPE);
    48. //从容器中获取到 UserDetailsService bean
    49. Map userDetailsServiceMap = SpringUtil
    50. .getBeansOfType(PigUserDetailsService.class);
    51. Optional optional = userDetailsServiceMap.values().stream()
    52. //过滤掉不是当前登录模式的 service
    53. .filter(service -> service.support(grantType))
    54. .max(Comparator.comparingInt(Ordered::getOrder));
    55. try {
    56. UserDetails loadedUser = optional.get().loadUserByUsername(username);
    57. if (loadedUser == null) {
    58. throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
    59. } else {
    60. return loadedUser;
    61. }
    62. } catch (UsernameNotFoundException var4) {
    63. //缓解计时攻击
    64. this.mitigateAgainstTimingAttack(authentication);
    65. throw var4;
    66. } catch (InternalAuthenticationServiceException var5) {
    67. throw var5;
    68. } catch (Exception var6) {
    69. throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
    70. }
    71. }
    72. }
    1. //6.在 pig-auth 模块里面继承一套基于 OAuth2ResourceOwnerBaseAuthenticationXXXX 的 converter、token、provider 类,完全按照密码模式登录的编写就好;
    2. public class OAuth2ResourceOwnerSmsAuthenticationToken extends OAuth2ResourceOwnerBaseAuthenticationToken {
    3. public OAuth2ResourceOwnerSmsAuthenticationToken(AuthorizationGrantType authorizationGrantType, Authentication clientPrincipal, Set scopes, Map additionalParameters) {
    4. super(authorizationGrantType, clientPrincipal, scopes, additionalParameters);
    5. }
    6. }
    7. public class OAuth2ResourceOwnerSmsAuthenticationConverter extends OAuth2ResourceOwnerBaseAuthenticationConverter {
    8. /**
    9. * @Description: 是否支持此convert
    10. * @param: * @param grantType
    11. * @return: boolean
    12. **/
    13. @Override
    14. public boolean support(String grantType) {
    15. return SecurityConstants.APP.equals(grantType);
    16. }
    17. @Override
    18. public OAuth2ResourceOwnerBaseAuthenticationToken buildToken(Authentication clientPrincipal, Set requestedScopes, Map additionalParameters) {
    19. return new OAuth2ResourceOwnerSmsAuthenticationToken(new AuthorizationGrantType(SecurityConstants.APP), clientPrincipal, requestedScopes, additionalParameters);
    20. }
    21. /**
    22. * @Description: 校验扩展参数 密码模式密码必须不为空
    23. * @param: * @param request
    24. * @return: void
    25. **/
    26. @Override
    27. public void checkParams(HttpServletRequest request) {
    28. MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request);
    29. //从请求中拿到 mobile 属性的值
    30. String phone = parameters.getFirst(SecurityConstants.SMS_PARAMETER_NAME);
    31. //防止有多个 mobile 属性
    32. if (!StringUtils.hasText(phone) || parameters.get(SecurityConstants.SMS_PARAMETER_NAME).size() != 1) {
    33. OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, SecurityConstants.SMS_PARAMETER_NAME,
    34. OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
    35. }
    36. }
    37. }
    38. @Slf4j
    39. public class OAuth2ResourceOwnerSmsAuthenticationProvider extends OAuth2ResourceOwnerBaseAuthenticationProvider {
    40. public OAuth2ResourceOwnerSmsAuthenticationProvider(AuthenticationManager authenticationManager, OAuth2AuthorizationService oAuth2AuthorizationService, OAuth2TokenGenerator oAuth2TokenGenerator) {
    41. super(authenticationManager, oAuth2AuthorizationService, oAuth2TokenGenerator);
    42. }
    43. @Override
    44. public boolean supports(Class authentication) {
    45. boolean supports = OAuth2ResourceOwnerSmsAuthenticationToken.class.isAssignableFrom(authentication);
    46. log.debug("supports authentication=" + authentication + " returning " + supports);
    47. return supports;
    48. }
    49. @Override
    50. public void checkClient(RegisteredClient registeredClient) {
    51. assert registeredClient != null;
    52. //检查当前登录的客户端是否支持此模式的登录
    53. if (!registeredClient.getAuthorizationGrantTypes().contains(new AuthorizationGrantType(SecurityConstants.APP))) {
    54. throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
    55. }
    56. }
    57. @Override
    58. public UsernamePasswordAuthenticationToken buildUserAuthenToken(Map reqParameters) {
    59. //从请求中拿到 mobile 属性的值
    60. String phone = (String) reqParameters.get(SecurityConstants.SMS_PARAMETER_NAME);
    61. //创建未认证的 token
    62. return new UsernamePasswordAuthenticationToken(phone, null);
    63. }
    64. }
    1. //7.修改 AuthorizationServerConfiguration 类,将短信验证码模式的 converter 和 provider 添加到配置里面;
    2. @EnableWebSecurity(debug = true) //这个注解会触发创建 HttpSecurity bean ~
    3. @RequiredArgsConstructor
    4. public class AuthorizationServerConfiguration {
    5. /**
    6. * @Description: request -> xToken 注入请求转换器
    7. * 1、授权码模式(暂无)
    8. * 2、隐藏式(暂无)
    9. * 3、密码式(自定义的)
    10. * 4、客户端凭证(暂无)
    11. * @param
    12. * @Return: org.springframework.security.web.authentication.AuthenticationConverter
    13. */
    14. public AuthenticationConverter accessTokenRequestConverter(){
    15. //new一个token转换器委托器,其中包含自定义密码模式认证转换器和刷新令牌认证转换器
    16. return new DelegatingAuthenticationConverter(Arrays.asList(
    17. // ——自定义密码模式登录
    18. new OAuth2ResourceOwnerPasswordAuthenticationConverter(),
    19. // ——自定义短信验证码模式登录
    20. new OAuth2ResourceOwnerSmsAuthenticationConverter(),
    21. // 访问令牌请求用于OAuth 2.0刷新令牌授权 ——刷新token
    22. new OAuth2RefreshTokenAuthenticationConverter()
    23. //有需要到就要添加上
    24. // // 访问令牌请求用于OAuth 2.0授权码授权 ——授权码模式获取token
    25. // new OAuth2AuthorizationCodeAuthenticationConverter(),
    26. // // 授权请求(或同意)用于OAuth 2.0授权代码授权 ——授权码模式获取code
    27. // new OAuth2AuthorizationCodeRequestAuthenticationConverter()
    28. ));
    29. }
    30. /**
    31. * @Description: 注入所有自定义认证授权需要的 provider 对象
    32. * 1. 密码模式
    33. * 2. 短信登录 (暂无)
    34. * @param http
    35. * @Return: void
    36. */
    37. public void addCustomOAuth2GrantAuthenticationProvider(HttpSecurity http){
    38. //从shareObject中获取到授权管理业务类(主要负责管理已认证的授权信息)
    39. OAuth2AuthorizationService oAuth2AuthorizationService = http.getSharedObject(OAuth2AuthorizationService.class);
    40. //从shareObject中获取到认证管理类
    41. AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
    42. //new一个自定义处理密码模式的授权提供方,其中重点需要注入token生成器
    43. OAuth2ResourceOwnerPasswordAuthenticationProvider oAuth2ResourceOwnerPasswordAuthenticationProvider =
    44. new OAuth2ResourceOwnerPasswordAuthenticationProvider(authenticationManager, oAuth2AuthorizationService, oAuth2TokenGenerator());
    45. //new一个自定义处理短信验证码模式的授权提供方,其中重点需要注入token生成器
    46. OAuth2ResourceOwnerSmsAuthenticationProvider oAuth2ResourceOwnerSmsAuthenticationProvider =
    47. new OAuth2ResourceOwnerSmsAuthenticationProvider(authenticationManager, oAuth2AuthorizationService, oAuth2TokenGenerator());
    48. // 将自定义处理密码模式的授权提供方添加到安全配置中
    49. http.authenticationProvider(new PigDaoAuthenticationProvider());
    50. // 将自定义用户认证提供方添加到安全配置中
    51. http.authenticationProvider(oAuth2ResourceOwnerPasswordAuthenticationProvider);
    52. // 将自定义用户认证提供方添加到安全配置中
    53. http.authenticationProvider(oAuth2ResourceOwnerSmsAuthenticationProvider);
    54. }
    55. }
    //8.在数据库中找到 sys_oauth_client_details 表,给使用的客户端账号 authorized_grant_types 里面加上短信验证登录的标识,只有有该登录模式的标识才能够使用该登录模式。

     

     

    B3.测试

    先测试获取短信验证码接口成功~

    注意,记得修改右上角的环境!

    在ApiFox里创建一个短信验证码登录的接口,使用有此登录模式的客户端账号,进行登录

     

     

  • 相关阅读:
    视觉答题的方法、数据集和评价指标综述
    Linux下一键安装Python3&更改镜像源&虚拟环境管理技巧
    DAP数据加工流程梳理
    jvm-类加载
    ‘==‘与‘=‘并非胖与瘦一样容易分辨
    微信小程序开发系列(十八)·wxml语法·声明和绑定数据
    3线硬件SPI驱动 HX8347 TFT屏
    小谈设计模式(27)—享元模式
    windows安装多个版本jdk
    qt 文字滚动
  • 原文地址:https://blog.csdn.net/vaevaevae233/article/details/127766304