• springboot 使用shiro集成阿里云短信验证码


    目录

    1.阿里云短信验证码服务

    2.发送短信验证码

    3.shiro配置多个realm

    4.验证短信验证码

    5.一些修改思路


    引言:短信验证码是通过发送验证码到手机的一种有效的验证码系统,主要用于验证用户手机的合法性及敏感操作的身份验证。在注册和修改密码时需要用到短信验证码校验手机号的功能。本文主要集成阿里云的短信验证码功能,进行功能实现。

    1.阿里云短信验证码服务

    首先,我们需要登录阿里云官网,进入控制台搜索短信验证码(sms)服务,进行开通。

    然后我们需要开通ACCESS KEY,进行api调用时需要进行填写,来验证用户的身份。

    然后我们需要进入访问控制,建立用户组及用户,并分配权限SMS给他:

    然后,我们可以进入短信服务,遵循的推荐顺序,进行具体功能的开通:

    首先,申请签名,然后是申请短信发送的模板,然后需要记住以上内容,方便在api中进行填写。

    2.发送短信验证码

    完成上述步骤后,我们就可以开始代码的填写。

    首先,在springboot项目中引入依赖:

    1. <dependency>
    2. <groupId>com.aliyungroupId>
    3. <artifactId>aliyun-java-sdk-coreartifactId>
    4. <version>4.3.3version>
    5. dependency>

    然后,编写业务类代码:

    1. @Service
    2. public class AuthServiceImpl extends ServiceImpl implements AuthService {
    3. @Override
    4. public boolean sendMessage(String phoneNum, Map code) {
    5. if(StringUtils.isEmpty(phoneNum)) {
    6. return false;
    7. }
    8. /**创建阿里云连接
    9. * @Param regionld 默认为default
    10. * @Param accessKeyId 你的accessKey id
    11. * @Param secret 你的accessKey秘钥
    12. */
    13. DefaultProfile profile = DefaultProfile.getProfile("default","accessKeyId", "secret");
    14. IAcsClient client = new DefaultAcsClient(profile);
    15. //发送请求
    16. CommonRequest request = new CommonRequest();
    17. //request.setProtocol(ProtocolType.HTTPS);
    18. request.setMethod(MethodType.POST);
    19. request.setDomain("dysmsapi.aliyuncs.com");
    20. request.setVersion("2017-05-25");
    21. request.setAction("SendSms");
    22. request.putQueryParameter("PhoneNumbers", phoneNum); //发送的手机号对象
    23. request.putQueryParameter("SignName", "阿里云短信测试"); //申请的签名名称
    24. request.putQueryParameter("TemplateCode", "SMS_154950909"); //申请的短信模板
    25. request.putQueryParameter("TemplateParam", JSONObject.toJSONString(code)); //验证码
    26. try {
    27. CommonResponse response = client.getCommonResponse(request);
    28. System.out.println(response.getData());
    29. return response.getHttpResponse().isSuccess();
    30. } catch (Exception e) {
    31. e.printStackTrace();
    32. }
    33. return false;
    34. }
    35. }

    上述代码根据阿里云官网的示例代码进行修改,进行手机验证码的发送。

    编写controller代码:

    1. //利用阿里云发送短信验证码
    2. @GetMapping("/send/{phone}")
    3. public CommonResult codeSend(@PathVariable("phone") String phone){
    4. String code = redisTemplate.opsForValue().get(phone);
    5. if(!StringUtils.isEmpty(code)){
    6. log.info("验证码存在,未过期");
    7. }
    8. //不存在,生成验证码
    9. code = UUID.randomUUID().toString().substring(0,4);
    10. HashMap map = new HashMap<>();
    11. map.put("code",code);
    12. //发送验证码
    13. boolean isSend = authService.sendMessage(phone,map);
    14. if(isSend){
    15. //存储验证码
    16. redisTemplate.opsForValue().set(phone,code, 300,TimeUnit.SECONDS);
    17. return CommonResult.success(null);
    18. }else {
    19. //失败
    20. return CommonResult.fail(null);
    21. }
    22. }
    23. 上述代码中,若发送成功,使用redis存储验证码(存储时间上述代码为5分钟,具体可以自己设定),方便后续的检验。

      3.shiro配置多个realm

      在手机验证码发送成功后,我们还需接收验证码进行登录验证,下面我们使用shiro集成多个realm实现。分开两个接口,一个是正常的用户名密码登录,可参考springboot集成shiro实现登录验证;另一个使用手机和验证码登录,如果手机已注册,将账号信息存入jwt token中,如果没有注册,生成临时账号,将信息存入jwt token。

      下面是多个realm的实现:

      (1)编写LoginToken,继承原始的UsernamePasswordToken,添加需要获取的参数及认证参数:

      1. public class LoginToken extends UsernamePasswordToken {
      2. //手机号
      3. private String phone;
      4. //定义登陆的类型是为了在后面的校验中 去选择使用哪一个realm
      5. private String loginType;
      6. public LoginToken(String phone, String loginType){
      7. this.phone=phone;
      8. this.loginType=loginType;
      9. }
      10. //认证规则,与realm中的认证对应
      11. @Override
      12. public Object getCredentials() {
      13. return phone;
      14. }
      15. public void setPhone(String phone) {
      16. this.phone = phone;
      17. }
      18. public String getPhone() {
      19. return phone;
      20. }
      21. public void setLoginType(String loginType) {
      22. this.loginType = loginType;
      23. }
      24. public String getLoginType() {
      25. return loginType;
      26. }
      27. }

      (2)编写多个realm的管理类,根据loginType指定使用的realm:

      1. public class LoginTypeModularRealmAuthenticator extends ModularRealmAuthenticator {
      2. //就是通过传入数据的类型 来选择使用哪一个Realm
      3. @Override
      4. protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
      5. //做Realm的一个校验
      6. assertRealmsConfigured();
      7. //获取前端传递过来的token
      8. LoginToken loginToken =(LoginToken)authenticationToken;
      9. //现在就可以获取这个登陆的类型了
      10. String loginType = loginToken.getLoginType(); // 登陆类型 1:User 2:Phone
      11. //获取所有的realms()
      12. Collection realms = getRealms();
      13. //登陆类型对应的所有realm全部获取到
      14. Collection typeRealms=new ArrayList<>();
      15. for (Realm realm:realms){
      16. //realm类型和现在登陆的类型做一个对比
      17. //注意loginType中的值需要和realm中的名称对应,如PhoneRealm对应的loginType为phone
      18. if(realm.getName().contains(loginType)){ //就能分开这两个realm
      19. typeRealms.add(realm);
      20. }
      21. }
      22. if(typeRealms.size()==1){
      23. return doSingleRealmAuthentication(typeRealms.iterator().next(), loginToken);
      24. }else{
      25. return doMultiRealmAuthentication(typeRealms, loginToken);
      26. }
      27. }
      28. }

      (3)编写shiro配置类,将bean注入,部分代码如下:

      1. //@Qualifier("userRealm") UserRealm userRealm
      2. @Bean
      3. public DefaultWebSecurityManager getDefaultWebSecurityManager(Collection realms) {
      4. DefaultWebSecurityManager SecurityManager = new DefaultWebSecurityManager();
      5. SecurityManager.setRealms(realms);
      6. return SecurityManager;
      7. }
      8. //该bean为username和password验证realm
      9. @Bean
      10. public UserRealm userRealm() {
      11. UserRealm userRealm = new UserRealm();
      12. //注册MD5加密
      13. userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
      14. return userRealm;
      15. }
      16. //该bean为phone验证realm
      17. @Bean
      18. public PhoneRealm phoneRealm() {
      19. return new PhoneRealm();
      20. }
      21. /**
      22. * 系统自带的Realm管理,主要针对多realm
      23. */
      24. @Bean
      25. public ModularRealmAuthenticator modularRealmAuthenticator() {
      26. //自己重写的ModularRealmAuthenticator
      27. LoginTypeModularRealmAuthenticator modularRealmAuthenticator = new LoginTypeModularRealmAuthenticator();
      28. modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
      29. return modularRealmAuthenticator;
      30. }

      (4)编写PhoneRealm:

      1. @Slf4j
      2. public class PhoneRealm extends AuthorizingRealm {
      3. //授权
      4. @Override
      5. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
      6. return null;
      7. }
      8. //认证
      9. @Override
      10. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
      11. LoginToken token = (LoginToken) authenticationToken;
      12. //用户名/密码认证
      13. String phone = token.getPhone();
      14. //参数2为token中定义的getCredentials()
      15. return new SimpleAuthenticationInfo(phone,phone,getName());
      16. }
      17. }

      上述代码只是一个手机登录的简单实现,如果需要,可在 doGetAuthenticationInfo 方法中进行扩展。

      4.验证短信验证码

      完成上述代码后,我们即可对登录的手机验证码进行验证登录。

      首先,封装一个手机登录的dto,分别对于username登录和phone登录:

      1. @Data
      2. @NoArgsConstructor
      3. @AllArgsConstructor
      4. public class LoginPhone implements Serializable {
      5. private static final long serialVersionUID = -5331320733431220933L;
      6. @NotBlank(message = "手机号不能为空") // 非空,message为错误的提示信息
      7. private String phone;
      8. @NotBlank(message = "验证码不能为空") // 非空
      9. @CodePatten // 密码自定义校验,密码必须含数字、大写字母、小写字母、特殊字符
      10. private String code;
      11. @NotBlank(message = "登录类型不能为空") // 非空,message为错误的提示信息
      12. private String loginType;
      13. }

       上述代码使用了自定义注解@CodePatten进行参数的简单校验,如果不熟悉,可以删去,不影响使用;下面是自定义校验的代码:

      1. @Documented
      2. @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
      3. @Retention(RUNTIME)
      4. @Repeatable(CodePatten.List.class)
      5. @Constraint(validatedBy = {CodePattenValidator.class})
      6. public @interface CodePatten {
      7. //校验失败返回信息
      8. String message() default "验证码长度错误";
      9. Class[] groups() default { };
      10. Classextends Payload>[] payload() default { };
      11. @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
      12. @Retention(RUNTIME)
      13. @Documented
      14. public @interface List {
      15. CodePatten[] value();
      16. }
      17. }
      1. import com.seven.springcloud.annotation.CodePatten;
      2. import org.apache.commons.lang3.StringUtils;
      3. import javax.validation.ConstraintValidator;
      4. import javax.validation.ConstraintValidatorContext;
      5. public class CodePattenValidator implements ConstraintValidator {
      6. @Override
      7. public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
      8. if (StringUtils.isBlank(value)) {
      9. return false;
      10. }
      11. return validateCode(value);
      12. }
      13. private boolean validateCode(String code) {
      14. return code.length() == 4;
      15. }
      16. }

       然后在控制类中编写验证短信的代码:

      1. @PostMapping("/login/phone")
      2. public CommonResult loginByPhoneNum(@RequestBody @Validated LoginPhone user){
      3. String phone=user.getPhone();
      4. String code=user.getCode();
      5. String loginType=user.getLoginType();
      6. if(!"phone".equals(loginType)){
      7. return new CommonResult<>(NormalResultEnum.SYSTEM_FAIL.getCode(), NormalResultEnum.SYSTEM_FAIL.getMessage());
      8. }
      9. if(!code.equals(redisTemplate.opsForValue().get(phone))){
      10. return new CommonResult<>(NormalResultEnum.FAIL.getCode(), "验证码错误");
      11. }
      12. //shiro验证
      13. Subject subject= SecurityUtils.getSubject();
      14. LoginToken token = new LoginToken(phone,loginType);
      15. try {
      16. subject.login(token);
      17. } catch (AuthenticationException e) {
      18. log.warn("用户登录异常:" + e.getMessage());
      19. return CommonResult.system_fail(null);
      20. }
      21. Map tokenMap = new HashMap<>();
      22. UserAccount account = authService.getOne(new QueryWrapper().eq("mobile",phone));
      23. if(account!=null){
      24. // 手机号已注册
      25. tokenMap = jwtTokenUtil
      26. .generateTokenAndRefreshToken(String.valueOf(account.getId()), account.getUsername(),
      27. //用户角色映射表中中查询用户角色
      28. rolesService.getOne(new QueryWrapper()
      29. .eq("username",account.getUsername())).getRoles());
      30. }else{
      31. // 手机号未注册,赋予user权限
      32. tokenMap = jwtTokenUtil
      33. .generateTokenAndRefreshToken(UUID.randomUUID().toString().substring(0,8), phone,"user");
      34. }
      35. return CommonResult.success(tokenMap);
      36. }
      37. 上述代码逻辑如下:首先判断登录类型是否为手机登录,否则不可调用该接口;然后根据手机号到redis中取对应的验证码,如果取不到或是不相同,则验证失败;然后使用shiro进行手机号的登录验证;登录成功后,将登录账号的信息存入token中,返回前端。

        (此处的用户权限管理使用RBAC模型实现,创建了单独的用户权限表,若不熟悉,可以将用户权限和用户账号存入同一张表,直接取出即可)

        至此,手机验证码代码实现结束,进行验证:

        发送验证码:

        使用手机号登录:

        登录成功,返回jwt token;

        查看jwt token中值,可以查得,该手机号未注册,为临时账号:

         验证码错误,则登录失败。

        5.一些修改思路

        代码中,用户名登录和手机登录可封装到同一个接口中,但LoginToken需要进行一些修改:

        1. public LoginToken(String username, String password, String loginType){
        2. super(username, password);
        3. this.loginType=loginType;
        4. }
        5. //认证规则,与realm中的认证对应
        6. @Override
        7. public Object getCredentials() {
        8. if(phone==null){
        9. return getPassword();
        10. }
        11. return phone;
        12. }

        controller中,根据业务需求,添加如下代码即可:

        1. //shiro验证
        2. Subject subject= SecurityUtils.getSubject();
        3. LoginToken token = new LoginToken(username,password,loginType);

        同时,最好是封装好一个同时包含phoen和username等信息的dto,方便进行参数的读取。

      38. 相关阅读:
        R语言使用plot函数可视化数据散点图,自定义设置xaxt参数移除X轴的刻度线
        Docker--harbor私有仓库部署与管理
        【Mysql】给查询记录增加序列号方法
        软件项目管理 7.1.项目进度基本概念
        反向迭代器------封装的力量
        Linux权限理解以及shell理解
        Centos部署openGauss6.0创新版本,丝滑的体验
        QT5.14.2 视频分帧:QT与FFmpeg的高效结合
        搜索优化剪枝策略
        第五十五周总结——十一月月底总结
      39. 原文地址:https://blog.csdn.net/tang_seven/article/details/128044824