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

news2025/7/18 0:35:31

目录

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

2.发送短信验证码

3.多个realm配置

4.验证短信验证码

5.一些拓展思路


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

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

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

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

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

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

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

2.发送短信验证码

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

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

        <!-- 阿里云服务依赖 -->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.3.3</version>
        </dependency>

然后,编写业务类代码:

@Service
public class AuthServiceImpl extends ServiceImpl<AuthDao, UserAccount> implements AuthService {
    @Override
    public boolean sendMessage(String phoneNum, Map<String, Object> code) {

        if(StringUtils.isEmpty(phoneNum)) {
            return false;
        }

        /**创建阿里云连接
         * @Param regionld 默认为default
         * @Param accessKeyId 你的accessKey id
         * @Param secret 你的accessKey秘钥
         */
        DefaultProfile profile = DefaultProfile.getProfile("default",
                "Q2AtKVxX1N3tOh3AWHHzXyx", "ZgmmX3vSlMF9GnxliXZrLxoD7053Hx");
        IAcsClient client = new DefaultAcsClient(profile);

        //发送请求
        CommonRequest request = new CommonRequest();
        //request.setProtocol(ProtocolType.HTTPS);
        request.setMethod(MethodType.POST);
        request.setDomain("dysmsapi.aliyuncs.com");
        request.setVersion("2017-05-25");
        request.setAction("SendSms");
        
        request.putQueryParameter("PhoneNumbers", phoneNum);           //发送的手机号对象
        request.putQueryParameter("SignName", "阿里云短信测试");    //申请的签名名称
        request.putQueryParameter("TemplateCode", "SMS_154950909"); //申请的短信模板
        request.putQueryParameter("TemplateParam", JSONObject.toJSONString(code));    //验证码

        try {
            CommonResponse response = client.getCommonResponse(request);
            System.out.println(response.getData());
            return response.getHttpResponse().isSuccess();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }
}

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

编写controller代码:

//利用阿里云发送短信验证码
    @GetMapping("/send/{phone}")
    public CommonResult<Object> codeSend(@PathVariable("phone") String phone){
        String code = redisTemplate.opsForValue().get(phone);
        if(!StringUtils.isEmpty(code)){
            log.info("验证码存在,未过期");
        }
        //不存在,生成验证码
        code = UUID.randomUUID().toString().substring(0,4);
        HashMap<String,Object> map = new HashMap<>();
        map.put("code",code);
        //发送验证码
        boolean isSend = authService.sendMessage(phone,map);
        if(isSend){
            //存储验证码
            redisTemplate.opsForValue().set(phone,code, 30,TimeUnit.SECONDS);
            return CommonResult.success(null);
        }else {
            //失败
            return CommonResult.fail(null);
        }
    }

上述代码中,若发送成功,使用redis存储验证码(存储时间上述代码为5分钟,具体可以自己设定),方便后续的检验。

3.多个realm配置

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

下面是多个realm的实现:

(1)编写LoginToken:

public class LoginToken extends UsernamePasswordToken {

    //手机号
    private String phone;
    //定义登陆的类型是为了在后面的校验中 去选择使用哪一个realm
    private String loginType;

    public LoginToken(String phone, String loginType){
        this.phone=phone;
        this.loginType=loginType;
    }

    //认证规则,与realm中的认证对应
    @Override
    public Object getCredentials() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getPhone() {
        return phone;
    }

    public void setLoginType(String loginType) {
        this.loginType = loginType;
    }

    public String getLoginType() {
        return loginType;
    }
}

(2)编写多个realm管理类:

public class LoginTypeModularRealmAuthenticator extends ModularRealmAuthenticator {

    //就是通过传入数据的类型  来选择使用哪一个Realm
    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        //做Realm的一个校验
        assertRealmsConfigured();
        //获取前端传递过来的token
        LoginToken loginToken =(LoginToken)authenticationToken;
        //现在就可以获取这个登陆的类型了
        String loginType = loginToken.getLoginType();  //  登陆类型   1:User   2:Phone
        //获取所有的realms()
        Collection<Realm> realms = getRealms();
        //登陆类型对应的所有realm全部获取到
        Collection<Realm> typeRealms=new ArrayList<>();
        for (Realm realm:realms){
            //realm类型和现在登陆的类型做一个对比
            //注意loginType中的值需要和realm中的名称对应,如PhoneRealm对应的loginType为phone
            if(realm.getName().contains(loginType)){   //就能分开这两个realm
                typeRealms.add(realm);
            }
        }

        if(typeRealms.size()==1){
            return doSingleRealmAuthentication(typeRealms.iterator().next(), loginToken);
        }else{
            return doMultiRealmAuthentication(typeRealms, loginToken);
        }
    }
}

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

    //@Qualifier("userRealm") UserRealm userRealm
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Collection<Realm> realms) {
        DefaultWebSecurityManager SecurityManager = new DefaultWebSecurityManager();
        SecurityManager.setRealms(realms);
        return SecurityManager;
    }

    //该bean为username和password验证realm
    @Bean
    public UserRealm userRealm() {
        UserRealm userRealm = new UserRealm();
        //注册MD5加密
        userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return userRealm;
    }

    //该bean为phone验证realm
    @Bean
    public PhoneRealm phoneRealm() {
        return new PhoneRealm();
    }

    /**
     * 系统自带的Realm管理,主要针对多realm
     */
    @Bean
    public ModularRealmAuthenticator modularRealmAuthenticator() {
        //自己重写的ModularRealmAuthenticator
        LoginTypeModularRealmAuthenticator modularRealmAuthenticator = new LoginTypeModularRealmAuthenticator();
        modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        return modularRealmAuthenticator;
    }

(4)编写PhoneRealm:

@Slf4j
public class PhoneRealm extends AuthorizingRealm {

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        LoginToken token = (LoginToken) authenticationToken;
        //用户名/密码认证
        String phone = token.getPhone();
        //参数2为token中定义的getCredentials()
        return  new SimpleAuthenticationInfo(phone,phone,getName());
    }
}

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

4.验证短信验证码

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

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

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginPhone implements Serializable {

    private static final long serialVersionUID = -5331320733431220933L;

    @NotBlank(message = "手机号不能为空")   // 非空,message为错误的提示信息
    private String phone;
    @NotBlank(message = "验证码不能为空")   // 非空
    @CodePatten // 密码自定义校验,密码必须含数字、大写字母、小写字母、特殊字符
    private String code;
    @NotBlank(message = "登录类型不能为空")   // 非空,message为错误的提示信息
    private String loginType;
}

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

@Documented
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Repeatable(CodePatten.List.class)
@Constraint(validatedBy = {CodePattenValidator.class})
public @interface CodePatten {

    //校验失败返回信息
    String message() default "验证码长度错误";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    @Documented
    public @interface List {
        CodePatten[] value();
    }
}
import com.seven.springcloud.annotation.CodePatten;
import org.apache.commons.lang3.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class CodePattenValidator implements ConstraintValidator<CodePatten,String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
        if (StringUtils.isBlank(value)) {
            return false;
        }
        return validateCode(value);
    }

    private boolean validateCode(String code) {
        return code.length() == 4;
    }
}

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

@PostMapping("/login/phone")
    public CommonResult<Object> loginByPhoneNum(@RequestBody @Validated LoginPhone user){

        String phone=user.getPhone();
        String code=user.getCode();
        String loginType=user.getLoginType();

        if(!"phone".equals(loginType)){
            return new CommonResult<>(NormalResultEnum.SYSTEM_FAIL.getCode(), NormalResultEnum.SYSTEM_FAIL.getMessage());
        }

        if(!code.equals(redisTemplate.opsForValue().get(phone))){
            return new CommonResult<>(NormalResultEnum.FAIL.getCode(), "验证码错误");
        }

        //shiro验证
        Subject subject= SecurityUtils.getSubject();
        LoginToken token = new LoginToken(phone,loginType);

        try {
            subject.login(token);
        } catch (AuthenticationException e) {
            log.warn("用户登录异常:" + e.getMessage());
            return CommonResult.system_fail(null);
        }

        Map<String, Object> tokenMap = new HashMap<>();
        UserAccount account = authService.getOne(new QueryWrapper<UserAccount>().eq("mobile",phone));
        if(account!=null){
            // 手机号已注册
            tokenMap = jwtTokenUtil
                    .generateTokenAndRefreshToken(String.valueOf(account.getId()), account.getUsername(), 
            //用户角色映射表中中查询用户角色
                            rolesService.getOne(new QueryWrapper<AccountRoles>()
                                    .eq("username",account.getUsername())).getRoles());
        }else{
            // 手机号未注册,赋予user权限
            tokenMap = jwtTokenUtil
                    .generateTokenAndRefreshToken(UUID.randomUUID().toString().substring(0,8), phone,"user");
        }
        return CommonResult.success(tokenMap);
    }

上述代码逻辑如下:首先判断登录类型是否为手机登录,否则不可调用该接口;然后根据手机号到redis中取对应的验证码,如果取不到或是不相同,则验证失败;然后使用shiro进行手机号的登录验证;登录成功后,将登录账号的信息存入token中,返回前端。

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

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

发送验证码:

使用手机号登录:

登录成功,返回jwt token;

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

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

5.一些拓展思路

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

    public LoginToken(String username, String password, String loginType){
        super(username, password);
        this.loginType=loginType;
    }

    //认证规则,与realm中的认证对应
    @Override
    public Object getCredentials() {
        if(phone==null){
            return getPassword();
        }
        return phone;
    }

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/36269.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

手摸手教会你在idea中配置Tomcat进行servlet/jsp开发(多图超详)

1. 下载安装idea&#xff0c;创建project&#xff0c;如果没有JDK可以通过idea指定文件夹并下载JDK。工程就是普通的Java工程&#xff0c;名字为webdemo 2.因为是Web项目&#xff0c;所以要对这个普通的项目进行WEB扶持^^&#xff0c;在项目名称webdemo上右键单间选择菜单项&qu…

Hive搭建

Hive系列第二章 第二章 Hive搭建 2.1 MySQL5.6安装 1、检查删除已有的 有就删除&#xff0c;没有就不用管。 rpm -qa | grep mysql rpm -e mysql-libs-5.1.73-8.el6_8.x86_64 --nodepsrpm -qa | grep mariadb rpm -e --nodeps mariadb-libs-5.5.56-2.el7.x86_642、删除mysql分…

Windows下Labelimg标注自己的数据集使用(Ubuntu18.04)Jetson AGX Orin进行YOLO5训练测试完整版教程

一、环境配置介绍 整个实现过程所涉及的文件目录&#xff0c;其中&#xff0c;自备表示自己需要准备的&#xff0c;生成表示无需自己准备。 使用yolov5时出现“assertionerror:no labels found in */*/*/JPEGImages.cache can not train without labels”问题 很多朋友都会遇…

SpringBoot+Vue项目医疗管理系统

文末获取源码 开发语言&#xff1a;Java 使用框架&#xff1a;spring boot 前端技术&#xff1a;JavaScript、Vue.js 、css3 开发工具&#xff1a;IDEA/MyEclipse/Eclipse、Visual Studio Code 数据库&#xff1a;MySQL 5.7/8.0 数据库管理工具&#xff1a;phpstudy/Navicat JD…

待办事项是什么意思,怎么用?

待办事项是什么意思&#xff0c;为什么要用&#xff1f;待办事项工具怎么设置&#xff1f;这里一文给你讲清&#xff01; 废话不多说&#xff0c;下面直接教你&#xff1a;梳理待办事项清单的方法&#xff0c;以及待办工具的操作实操步骤。想要快速提升工作效率的小伙伴&#…

第三十七篇 Vue中封装Swiper组件

在上一篇内容的结尾讲到了将swiper初始化从mounted生命周期转移到updated中来能够让swiper动起来&#xff0c;但同时是否会造其他的一个问题&#xff1f;什么问题呢&#xff1f;在每次data中的状态发什么改变&#xff0c;updated中的内容会又重新执行&#xff0c;这样会导致swi…

二维数组与二级指针是好朋友吗?

抛出问题 有一个二维数组&#xff0c;我想把它传给一个函数。于是我把函数接口定义出来了&#xff0c;如下&#xff1a; int array[2][3] {1,2,3,4,5,6}; void fun(int **array) {array[0][0] 5; } 当我试图直接把数组名传给函数时候&#xff0c;fun(array)编译会报错&…

C++ 中explicit的作用及用法

目录 Cexplicit&#xff08;官网的说法&#xff09; Cexplicit 清楚的说法&#xff08;建议英文不好的从这里开始食用哦&#xff09; Cexplicit使用的例子&#xff08;建议喜欢自己敲代码实验的从这里开始食用哦&#xff09; 总结 Cexplicit&#xff08;官网的说法&#xff…

神经网络和深度学习-梯度下降Gradient Descent(下)

梯度下降gradient descent 我们接着用数学公式来看一下梯度下降 首先是梯度 ∂cos⁡t∂ω\frac{\partial \cos t}{\partial \omega} ∂ω∂cost​ 然后我们对权重进行更新 ωω−α∂cos⁡t∂ω\omega\omega-\alpha \frac{\partial \cos t}{\partial \omega} ωω−α∂ω∂c…

python基础之容器类型公共方法

文章目录一、简介1.高级数据类型的公共方法&#xff1a;2.python的内置函数&#xff1a;3.python包含的内置函数&#xff1a;二、遍历字典的列表一、简介 1.高级数据类型的公共方法&#xff1a; 列表元组字典或者字符串都能够共同使用的方法&#xff1b; 2.python的内置函数…

【kafka】七、kafka数据可靠性保证

数据可靠性保证 为保证producer发送的数据&#xff0c;能可靠的发送到指定的topic&#xff0c;topic的每个partition收到producer发送的数据后&#xff0c;都需要向producer发送ack&#xff08;acknowledgement确认收到&#xff09;&#xff0c;如果producer收到ack&#xff0c…

基于AntBlazor的学生在线练习系统实现过程的简单总结

前言 疫情当前&#xff0c;作为一名教师&#xff0c;如何能让学生进行有效练习成了一个难题。如果使用 “问卷星”等线上调查问卷式的网站来做练习测试&#xff0c;是无法及时有效的进行&#xff0c;更谈不上对学生练习情况进行跟踪。鉴于以上现实情况和问题&#xff…

神经网络和深度学习-用pytorch实现线性回归

用pytorch实现线性回归 用pytorch的工具包来实现线性模型的训练过程 准备数据集 设计模型 构造损失函数和优化器&#xff08;使用pytorch API&#xff09; 训练过程&#xff1a;前馈、反馈、更新 准备数据 在PyTorch中&#xff0c;计算图是小批处理的&#xff0c;所以X和…

MATLAB | 世界杯来用MATLAB画个足球玩叭~

世界杯教你用MATLAB画个超逼真的足球&#xff0c; 需要准备Partial Differential Equation Toolbox工具箱&#xff0c;同时因为用到了polyshape类所以至少需要R2017b版本。 绘制讲解 数据来源及说明 我是真的不想写注释了太麻烦了&#xff0c;给大家讲一下我的思路希望能够看…

判断一个时间段是否经过了另一个时间段

场景&#xff1a; IOT设备存在离线与恢复时间记录&#xff0c;每一次离线和恢复记为一个周期即一条数据, 现在需要统计出在某个时段存在离线记录的数据,如果目前未恢复,没有恢复时间&#xff0c;恢复时间置为9999-01-01 00:00:00 原始数据如下(t_offline_record)&#xff1a;…

OPengl学习——初识opengl

文章目录1、网站罗列2、一些词语解析1.入门概念2.Opengl Vs DirectX3、QOpenGLWidget4、引用**OpenGL&#xff08;Open Graphics Library&#xff09;*是一个跨编程语言、跨平台的编程图形程序接口&#xff0c;它将计算机的资源抽象称为一个个OpenGL的对象&#xff0c;对这些资…

3.5、点对点协议 PPP

3.5、点对点协议 PPP 3.5.1、基本介绍 点对点协议 PPP(Point-to-Point Protocol) 是目前使用最广泛的点对点数据链路层协议。 用户计算机只有获取到 ISP 所分配的合法 IP 地址后&#xff0c;才能成为因特网上的主机 用户计算机与 ISP 进行通信时&#xff0c;所使用的数据链路…

【学习笔记21】JavaScript数组的基本方法

笔记首发 一、push&#xff1a;末位新增 语法: 数组.push(数据)作用: 向数组末尾添加数据返回值: 追加数据后, 数组最新的长度 var arr [10, 20, 30, 40];console.log(原始数组: , arr); ​var num arr.push(500);console.log(push新增后数组: , arr);console.log(push的返回…

进程的初识

目录预备知识 -> 操作系统操作系统的定义操作系统的定位进程进程的概念进程调度的过程进程的管理描述组织PCB描述进程的特征进程调度的相关属性进程的状态优先级上下文记账信息预备知识 -> 操作系统 操作系统的定义 操作系统是一个搞管理的软件 对上&#xff0c;要对硬…

为什么一定要申请专利呢

问题一&#xff1a;如何避免被判为非正常专利&#xff1f; 原本有个发明创造&#xff0c;想申请专利&#xff0c;却被列入非正常申请&#xff0c;甚至违法。以下五种情况一定要注意&#xff0c;千万不能碰。 1、同一单位或个人申请多件明显抄袭现有技术的专利&#xff1b; 2…