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

@RestController
@AllArgsConstructor
@RequestMapping("/app")
public class AppController {

    private final AppService appService;
    
    /**
     * @Description: 根据手机号码获取验证码【注意生产环境记得将返回的code去掉~】
     * @param:  * @param mobile
     * @return: com.pig4cloud.pig.common.core.util.R<java.lang.Boolean>
     **/
    @Inner(value = false)
    @GetMapping("/{mobile}")
    public R<Boolean> sendSmsCode(@PathVariable String mobile) {
        return appService.sendSmsCode(mobile);
    }

}

//因为用不到其他的 mapper ,所以就不用继承 mps 自带的 service 了
@Slf4j
@Service
@RequiredArgsConstructor
public class AppServiceImpl implements AppService {

    private final RedisTemplate redisTemplate;

    private final SysUserMapper userMapper;

    @Override
    public R<Boolean> sendSmsCode(String mobile) {
        //根据手机号码获取用户信息
        List<SysUser> userList = userMapper.selectList(Wrappers.<SysUser>query().lambda().eq(SysUser::getPhone, mobile));
        //判断该手机号码是否有注册的用户,没有就直接返回
        if (CollUtil.isEmpty(userList)) {
            log.info("手机号未注册:{}", mobile);
            return R.ok(Boolean.FALSE, "手机号未注册:{"+mobile+"}");
        }

        //根据手机号码从 redis 里面获取 code
        Object codeObj = redisTemplate.opsForValue().get(CacheConstants.DEFAULT_CODE_KEY + mobile);
        //判断该手机号码是否有未失效的验证码,没有就直接返回
        if (codeObj != null) {
            log.info("手机号验证码未过期:{},{}", mobile, codeObj);
            return R.ok(Boolean.FALSE, "请勿频繁获取验证码");
        }

        //在这里生成 code
        String code = RandomUtil.randomNumbers(Integer.parseInt(SecurityConstants.CODE_SIZE));
        log.info("手机号生成验证码成功:{},{}", mobile, code);

        //将手机号码为key,验证码为value保存到redis里面
        redisTemplate.opsForValue()
                .set(CacheConstants.DEFAULT_CODE_KEY + mobile, code, SecurityConstants.CODE_TIME, TimeUnit.SECONDS);

        // todo 记得调用短信通道发送
        return R.ok(Boolean.TRUE, code);
    }
}
//2.然后上面的controller类中增加一个根据手机号码获取用户信息的方法。

@RestController
@AllArgsConstructor
@RequestMapping("/app")
public class AppController {

    private final AppService appService;

    private final SysUserService userService;

    /**
     * @Description: 获取指定用户全部信息
     * @param:  * @param phone
     * @return: com.pig4cloud.pig.common.core.util.R<com.pig4cloud.pig.admin.api.vo.UserInfoVO>
     **/
    @Inner
    @GetMapping("/info/{phone}")
    public R<UserInfoVO> infoByMobile(@PathVariable String phone) {
        SysUser user = userService.getOne(Wrappers.<SysUser>query().lambda().eq(SysUser::getPhone, phone));
        if (user == null) {
            return R.failed("用户信息为空");
        }
        return R.ok(userService.getUserInfo(user));
    }

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

@FeignClient(contextId = "remoteUserService", value = ServiceNameConstants.UMPS_SERVICE)
public interface RemoteUserService {

    /**
     * @Description: 通过手机号码查询用户、角色信息
     * @param:  * @param phone
     * @param from
     * @return: com.pig4cloud.pig.common.core.util.R<UserInfo>
     **/
    @GetMapping("/app/info/{phone}")
    R<UserInfoVO> infoByMobile(@PathVariable("phone") String phone, @RequestHeader(SecurityConstants.FROM) String from);


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

@Slf4j
@RequiredArgsConstructor
public class PigAppUserDetailsServiceImpl implements PigUserDetailsService {

    private final RemoteUserService remoteUserService;

    /**
     * @Description: 手机号登录
     * @param:  * @param username
     * @return: org.springframework.security.core.userdetails.UserDetails
     **/
    @Override
    public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {

        R<UserInfoVO> result = remoteUserService.infoByMobile(phone, SecurityConstants.FROM_SECRET_VALUE);

        UserDetails userDetails = getUserDetails(result);

        return userDetails;
    }
}

//在 /resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 里面加上 PigAppUserDetailsServiceImpl 类

com.pig4cloud.pig.common.security.service.PigAppUserDetailsServiceImpl
//5.修改  PigDaoAuthenticationProvider#retrieveUser() 方法,原来默认就是拿到排序最高的,现在需要根据 grantType 判断具体用哪一个 UserDetailsService 来获取用户信息。并且一定要修改密码验证方法,在里面加入判断,如果是短信登录模式模式就不需要校验密码,因为短信登录模式没有密码嘛~
//密码模式登录的 service 用的是父类提供的,并且他的 Order 是最小的,因此不会影响其他模式的

@Slf4j
@RequiredArgsConstructor
public class PigAppUserDetailsServiceImpl implements PigUserDetailsService {

    /**
     * @Description: 是否支持此客户端校验
     * @param:  * @param grantType
     * @return: boolean
     **/
    @Override
    public boolean support(String grantType) {
        return SecurityConstants.APP.equals(grantType);
    }
}


//修改此类的两个方法
public class PigDaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        // 短息验证码模式不用校验密码
        String grantType = Optional.ofNullable(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest())
                .get().getParameter(OAuth2ParameterNames.GRANT_TYPE);
        if (StrUtil.equals(SecurityConstants.APP, grantType)) {
            return;
        }

        if (authentication.getCredentials() == null) {
            this.logger.debug("Failed to authenticate since no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                this.logger.debug("Failed to authenticate since password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }

    @SneakyThrows
    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        //为计时攻击的防御做准备
        prepareTimingAttackProtection();

        //拿到当前请求 request
        HttpServletRequest request = Optional.ofNullable(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest())
                .orElseThrow((Supplier<Throwable>) () -> new InternalAuthenticationServiceException("web request is empty"));
        //在 request 里面拿到 grant_type
        Map<String, String> paramMap = ServletUtil.getParamMap(request);
        String grantType = paramMap.get(OAuth2ParameterNames.GRANT_TYPE);

        //从容器中获取到 UserDetailsService bean
        Map<String, PigUserDetailsService> userDetailsServiceMap = SpringUtil
                .getBeansOfType(PigUserDetailsService.class);

        Optional<PigUserDetailsService> optional = userDetailsServiceMap.values().stream()
                //过滤掉不是当前登录模式的 service
                .filter(service -> service.support(grantType))
                .max(Comparator.comparingInt(Ordered::getOrder));
        try {

            UserDetails loadedUser = optional.get().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            //缓解计时攻击
            this.mitigateAgainstTimingAttack(authentication);
            throw var4;
        } catch (InternalAuthenticationServiceException var5) {
            throw var5;
        } catch (Exception var6) {
            throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
        }

    }

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

public class OAuth2ResourceOwnerSmsAuthenticationToken extends OAuth2ResourceOwnerBaseAuthenticationToken {

    public OAuth2ResourceOwnerSmsAuthenticationToken(AuthorizationGrantType authorizationGrantType, Authentication clientPrincipal, Set<String> scopes, Map<String, Object> additionalParameters) {
        super(authorizationGrantType, clientPrincipal, scopes, additionalParameters);
    }
}


public class OAuth2ResourceOwnerSmsAuthenticationConverter extends OAuth2ResourceOwnerBaseAuthenticationConverter {

    /**
     * @Description: 是否支持此convert
     * @param:  * @param grantType
     * @return: boolean
     **/
    @Override
    public boolean support(String grantType) {
        return SecurityConstants.APP.equals(grantType);
    }

    @Override
    public OAuth2ResourceOwnerBaseAuthenticationToken buildToken(Authentication clientPrincipal, Set requestedScopes, Map additionalParameters) {
        return new OAuth2ResourceOwnerSmsAuthenticationToken(new AuthorizationGrantType(SecurityConstants.APP), clientPrincipal, requestedScopes, additionalParameters);
    }

    /**
     * @Description: 校验扩展参数 密码模式密码必须不为空
     * @param:  * @param request
     * @return: void
     **/
    @Override
    public void checkParams(HttpServletRequest request) {
        MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
        //从请求中拿到 mobile 属性的值
        String phone = parameters.getFirst(SecurityConstants.SMS_PARAMETER_NAME);
        //防止有多个 mobile 属性
        if (!StringUtils.hasText(phone) || parameters.get(SecurityConstants.SMS_PARAMETER_NAME).size() != 1) {
            OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, SecurityConstants.SMS_PARAMETER_NAME,
                    OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
        }
    }

}


@Slf4j
public class OAuth2ResourceOwnerSmsAuthenticationProvider extends OAuth2ResourceOwnerBaseAuthenticationProvider<OAuth2ResourceOwnerSmsAuthenticationToken> {

    public OAuth2ResourceOwnerSmsAuthenticationProvider(AuthenticationManager authenticationManager, OAuth2AuthorizationService oAuth2AuthorizationService, OAuth2TokenGenerator<? extends OAuth2Token> oAuth2TokenGenerator) {
        super(authenticationManager, oAuth2AuthorizationService, oAuth2TokenGenerator);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        boolean supports =  OAuth2ResourceOwnerSmsAuthenticationToken.class.isAssignableFrom(authentication);
        log.debug("supports authentication=" + authentication + " returning " + supports);
        return supports;
    }

    @Override
    public void checkClient(RegisteredClient registeredClient) {
        assert registeredClient != null;
        //检查当前登录的客户端是否支持此模式的登录
        if (!registeredClient.getAuthorizationGrantTypes().contains(new AuthorizationGrantType(SecurityConstants.APP))) {
            throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
        }
    }

    @Override
    public UsernamePasswordAuthenticationToken buildUserAuthenToken(Map<String, Object> reqParameters) {
        //从请求中拿到 mobile 属性的值
        String phone = (String) reqParameters.get(SecurityConstants.SMS_PARAMETER_NAME);
        //创建未认证的 token
        return new UsernamePasswordAuthenticationToken(phone, null);
    }
}
//7.修改 AuthorizationServerConfiguration 类,将短信验证码模式的 converter 和 provider 添加到配置里面;

@EnableWebSecurity(debug = true) //这个注解会触发创建 HttpSecurity bean ~
@RequiredArgsConstructor
public class AuthorizationServerConfiguration {

    /**
     * @Description: request -> xToken 注入请求转换器
     * 		1、授权码模式(暂无)
     * 		2、隐藏式(暂无)
     * 		3、密码式(自定义的)
     * 		4、客户端凭证(暂无)
     * @param
     * @Return: org.springframework.security.web.authentication.AuthenticationConverter
     */
    public AuthenticationConverter accessTokenRequestConverter(){
        //new一个token转换器委托器,其中包含自定义密码模式认证转换器和刷新令牌认证转换器
        return new DelegatingAuthenticationConverter(Arrays.asList(
                //      ——自定义密码模式登录
                new OAuth2ResourceOwnerPasswordAuthenticationConverter(),
                //      ——自定义短信验证码模式登录
                new OAuth2ResourceOwnerSmsAuthenticationConverter(),
                // 访问令牌请求用于OAuth 2.0刷新令牌授权   ——刷新token
                new OAuth2RefreshTokenAuthenticationConverter()
                //有需要到就要添加上
//                // 访问令牌请求用于OAuth 2.0授权码授权   ——授权码模式获取token
//                new OAuth2AuthorizationCodeAuthenticationConverter(),
//                //  授权请求(或同意)用于OAuth 2.0授权代码授权   ——授权码模式获取code
//                new OAuth2AuthorizationCodeRequestAuthenticationConverter()
        ));
    }

    /**
     * @Description: 注入所有自定义认证授权需要的 provider 对象
     * 1. 密码模式 </br>
     * 2. 短信登录 (暂无)</br>
     * @param http
     * @Return: void
     */
    public void addCustomOAuth2GrantAuthenticationProvider(HttpSecurity http){
        //从shareObject中获取到授权管理业务类(主要负责管理已认证的授权信息)
        OAuth2AuthorizationService oAuth2AuthorizationService = http.getSharedObject(OAuth2AuthorizationService.class);
        //从shareObject中获取到认证管理类
        AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);

        //new一个自定义处理密码模式的授权提供方,其中重点需要注入token生成器
        OAuth2ResourceOwnerPasswordAuthenticationProvider oAuth2ResourceOwnerPasswordAuthenticationProvider =
                new OAuth2ResourceOwnerPasswordAuthenticationProvider(authenticationManager, oAuth2AuthorizationService, oAuth2TokenGenerator());
        //new一个自定义处理短信验证码模式的授权提供方,其中重点需要注入token生成器
        OAuth2ResourceOwnerSmsAuthenticationProvider oAuth2ResourceOwnerSmsAuthenticationProvider =
                new OAuth2ResourceOwnerSmsAuthenticationProvider(authenticationManager, oAuth2AuthorizationService, oAuth2TokenGenerator());


        // 将自定义处理密码模式的授权提供方添加到安全配置中
        http.authenticationProvider(new PigDaoAuthenticationProvider());
        // 将自定义用户认证提供方添加到安全配置中
        http.authenticationProvider(oAuth2ResourceOwnerPasswordAuthenticationProvider);
        // 将自定义用户认证提供方添加到安全配置中
        http.authenticationProvider(oAuth2ResourceOwnerSmsAuthenticationProvider);
    }

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

 

 

B3.测试

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

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

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

 

 

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值