若依RuoYi整合短信验证码登录

news2024/12/27 14:04:34

背景:若依默认使用账号密码进行登录,但是咱们客户需要增加一个短信登录功能,即在不更改原有账号密码登录的基础上,整合短信验证码登录

一、自定义短信登录 token 验证

仿照 UsernamePasswordAuthenticationToken 类,编写短信登录 token 验证。

package com.ruoyi.framework.security.authentication;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;

import java.util.Collection;

/**
 * 自定义短信登录token验证
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    /**
     * 存储手机号码
     */
    private final Object principal;

    /**
     * 构建一个没有鉴权的构造函数
     */
    public SmsCodeAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }

    /**
     * 构建一个拥有鉴权的构造函数
     */
    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }
        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
    
}

二、编写 UserDetailsService 实现类

在用户信息库中查找出当前需要鉴权的用户,如果用户不存在,loadUserByUsername() 方法抛出异常;如果用户名存在,将用户信息和权限列表一起封装到 UserDetails 的类中。

package com.ruoyi.system.service.impl;

import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.enums.UserStatus;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysUserService;
import com.ruoyi.system.service.SysPermissionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

/**
 * 用户验证处理
 *
 * @author hjs
 */
@Service("userDetailsByPhonenumber")
public class UserDetailsByPhonenumberServiceImpl implements UserDetailsService {

    private static final Logger log = LoggerFactory.getLogger(UserDetailsByPhonenumberServiceImpl.class);

    @Autowired
    private ISysUserService userService;

    @Autowired
    private SysPermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String phoneNumber) throws UsernameNotFoundException {
        SysUser user = userService.selectUserByPhonenumber(phoneNumber);
        if (StringUtils.isNull(user)) {
            log.info("登录用户:{} 不存在.", phoneNumber);
            throw new ServiceException("登录用户:" + phoneNumber+ " 不存在");
        } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
            log.info("登录用户:{} 已被删除.", phoneNumber);
            throw new ServiceException("对不起,您的账号:" + phoneNumber+ " 已被删除");
        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
            log.info("登录用户:{} 已被停用.", phoneNumber);
            throw new ServiceException("对不起,您的账号:" + phoneNumber+ " 已停用");
        }
        return createLoginUser(user);
    }

    public UserDetails createLoginUser(SysUser user) {
        return new LoginUser(user, permissionService.getMenuPermission(user));
    }

}

三、自定义短信登录身份认证

在 Sping Security 中因为 UserDetailsService 只提供一个根据用户名返回用户信息的动作,其他的责任跟他都没有关系,怎么将 UserDetails 组装成 Authentication 进一步向调用者返回呢?其实这个工作是由 AuthenticationProvider 完成的,下面我们自定义一个短信登录的身份鉴权。

  • 自定义一个身份认证,实现 AuthenticationProvider 接口;

  • 确定 AuthenticationProvider 仅支持短信登录类型的 Authentication 对象验证;

  • 1、重写 supports(Class<?> authentication) 方法,指定所定义的 AuthenticationProvider 仅支持短信身份验证。

  • 2、重写 authenticate(Authentication authentication) 方法,实现身份验证逻辑。

package com.ruoyi.framework.security.authentication;

import com.ruoyi.framework.security.authentication.SmsCodeAuthenticationToken;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

/**
 * 自定义短信登录身份认证
 */
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    public SmsCodeAuthenticationProvider(UserDetailsService userDetailsService){
        setUserDetailsService(userDetailsService);
    }
    
    /**
     * 重写 authenticate方法,实现身份验证逻辑。
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
        String telephone = (String) authenticationToken.getPrincipal();
        // 委托 UserDetailsService 查找系统用户
        UserDetails userDetails = userDetailsService.loadUserByUsername(telephone);
        // 鉴权成功,返回一个拥有鉴权的 AbstractAuthenticationToken
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    /**
     * 重写supports方法,指定此 AuthenticationProvider 仅支持短信验证码身份验证。
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

}

四、修改 SecurityConfig 配置类

4.1 添加 bean 注入

在这里插入图片描述

4.2 身份认证方法加入手机登录鉴权

在这里插入图片描述

五、发送短信验证码

/**
 * 发送短信验证码接口
 */
@ApiOperation("发送短信验证码")
@PostMapping("/sendSmsCode/{phoneNumber}")
public AjaxResult sendSmsCode(@PathVariable("phoneNumber") String phoneNumber) {
	// 手机号码
    phoneNumber = phoneNumber.trim();
    // 校验手机号
    SysUser user = sysUserService.selectUserByPhonenumber(phoneNumber);
    if (StringUtils.isNull(user)) {
        throw new ServiceException("登录用户:" + phoneNumber+ " 不存在");
    }else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
        throw new ServiceException("对不起,您的账号:" + phoneNumber+ " 已被删除");
    }else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
        throw new ServiceException("对不起,您的账号:" + phoneNumber+ " 已停用");
    }

    // 生成短信验证码
    String smsCode = "" + (int)((Math.random()*9+1)*1000);
    // 发送短信(提供阿里短信发送短信,实际按系统业务实现)
    SmsEntity entity = new SmsEntity(phoneNumber, smsCode);
    SendMessage.sendSms(entity);
    if(entity==null || !SmsResponseCodeEnum.SUCCESS.getCode().equals(entity.getResponseCode())){
        throw new ServiceException(entity.getResponseDesc());
    }
    // 保存redis缓存
    String uuid = IdUtils.simpleUUID();
    String verifyKey = SysConst.REDIS_KEY_SMSLOGIN_SMSCODE + uuid;
    redisCache.setCacheObject(verifyKey, smsCode, SysConst.REDIS_EXPIRATION_SMSLOGIN_SMSCODE, TimeUnit.MINUTES);

	AjaxResult ajax = AjaxResult.success();	
	ajax.put("uuid", uuid);
    return ajax;
}

六、手机验证码登录接口

/**
 * 短信验证码登录验证
 */
@ApiOperation("短信验证码登录验证")
@PostMapping("/smsLogin")
public AjaxResult smsLogin(@Valid @RequestBody SmsLoginDto dto) {
	// 手机号码
    String phoneNumber = dto.getPhoneNumber().trim();
    // 校验验证码
    String verifyKey = SysConst.REDIS_KEY_SMSLOGIN_SMSCODE + dto.getUuid().trim();
    String captcha = redisCache.getCacheObject(verifyKey);
    if(captcha == null) {
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(phoneNumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
        // 抛出一个验证码过期异常
        throw new CaptchaExpireException();
    }
    if(!captcha.equals(dto.getSmsCode().trim())){
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(phoneNumber, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
        // 抛出一个验证码错误的异常
        throw new CaptchaException();
    }
    redisCache.deleteObject(verifyKey);

    // 用户验证
    Authentication authentication = null;
    try {
        // 该方法会去调用UserDetailsByPhonenumberServiceImpl.loadUserByUsername
        authentication = authenticationManager.authenticate(new SmsCodeAuthenticationToken(phoneNumber));
    } catch (Exception e) {
        if (e instanceof BadCredentialsException) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(phoneNumber, 
            		Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
            throw new UserPasswordNotMatchException();
        } else {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(phoneNumber, 
            		Constants.LOGIN_FAIL, e.getMessage()));
            throw new ServiceException(e.getMessage());
        }
    }
    AsyncManager.me().execute(AsyncFactory.recordLogininfor(phoneNumber, 
    		Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
    
    LoginUser loginUser = (LoginUser) authentication.getPrincipal();
    // 修改最近登录IP和登录时间
    recordLoginInfo(loginUser.getUserId());
    // 生成token
    String token = tokenService.createToken(loginUser);

    // 返回token给前端
    AjaxResult ajax = AjaxResult.success();
    ajax.put(Constants.TOKEN, token);
    return ajax;
}

大功告成!创作不容易,若对您有帮助,欢迎收藏,记得赏个好评

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

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

相关文章

使没有sudo权限的普通用户可以使用容器

一、基本思路将普通用户加入docker组二、ubuntu组管理命令1、配置文件&#xff08;1&#xff09;文件&#xff1a;/etc/group&#xff08;2&#xff09;权限&#xff1a;①超级用户可读可写②普通用户只读2、查看组&#xff08;1&#xff09;命令cat /etc/group&#xff08;2&a…

【从零开始学习深度学习】34. Pytorch-RNN项目实战:RNN创作歌词案例--使用周杰伦专辑歌词训练模型并创作歌曲【含数据集与源码】

目录RNN项目实战使用周杰伦专辑歌词训练模型并创作歌曲1.语言模型数据集预处理1.1 读取数据集1.2 建立字符索引1.3 时序数据的2种采样方式1.3.1 随机采样1.3.2 相邻采样小结2. 从零实现循环神经网络并进行训练预测2.1 one-hot向量表示2.2 初始化模型参数2.3 定义模型2.4 定义预…

2023 年更新计划

前言 2023 年&#xff0c;会继续更新这个 CSDN 博客了&#xff1b; 看了一下博客数据&#xff0c;有些惨不忍睹&#xff0c;不过之前的内容质量并不高&#xff0c;从头来过吧&#xff1b; 当初个人娱乐写的 STM32 学习笔记&#xff0c;莫名受欢迎&#xff0c;不出意外的话&am…

Spring之Bean实例化的基本流程

目录 一&#xff1a;概述 二&#xff1a;代码展示 一&#xff1a;概述 Spring容器在进行初始化时&#xff0c; 会将xml配置的<bean>的信息封装成一个BeanDefinition对象&#xff0c; 所有的 BeanDefinition存储到一个名为be…

勇闯掘金小游戏为一款多个小游戏的合集游戏,有五个关卡:找掘金、石头剪刀布、寻找藏宝图、打地鼠、抽奖。基于Vue

游戏简介 勇闯掘金小游戏为一款多个小游戏的合集游戏&#xff0c;共有五个关卡&#xff0c;分别为&#xff1a;找掘金、石头剪刀布、寻找藏宝图、打地鼠、抽奖。每个环节20分&#xff0c;满分100分。 完整代码下载地址&#xff1a;勇闯掘金小游戏 快速体验 https://ihope_to…

Acwing---730.机器人问题

机器人问题1.题目2.基本思想3.代码实现1.题目 机器人正在玩一个古老的基于 DOS 的游戏。 游戏中有 N1 座建筑——从 0 到 N 编号&#xff0c;从左到右排列。 编号为 0 的建筑高度为 0 个单位&#xff0c;编号为 i 的建筑高度为 H(i) 个单位。 起初&#xff0c;机器人在编号…

Mycat2(四)mycat2 分库分表

文章目录一、分库分表原理垂直切分&#xff1a;分库水平切分&#xff1a;分表二、分库分表环境准备示例&#xff1a;开始准备环境三、实现分库分表3.1 分库分表--广播表&#xff08;BROADCAST&#xff09;3.2 分库分表--分片表&#xff08;dbpartition、tbpartition&#xff09…

电脑录屏怎么录ppt?三个ppt录制视频的方法

PPT演示文稿是人们在日常生活和学习中常用的工具&#xff0c;它也被广泛地运用于各个方面。最近有不少朋友问小编ppt录制视频的方法&#xff0c;其实ppt录制视频的方法有很多。如果只需要录制PPT内容&#xff0c;可以用PPT自带的“屏幕录制”来录制视频就可以了&#xff0c;如果…

Day848.Copy-on-Write模式 -Java 性能调优实战

Copy-on-Write模式 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于Copy-on-Write模式的内容。 Java 里 String 这个类在实现 replace() 方法的时候&#xff0c;并没有更改原字符串里面 value[]数组的内容&#xff0c;而是创建了一个新字符串&#xff0c;这种方法在…

C++GUI之wxWidgets(10)-编写应用涉及的类和方法(5)-事件处理(4)

目录自定义事件AddPendingEvent()QueueEvent()PushEventHandler()ProcessEvent()wxCommandEvent与新的事件类型一起使用自定义事件 AddPendingEvent() virtual void wxEvtHandler::AddPendingEvent ( const wxEvent & event ) 发布要稍后处理的事件。 此函数类似于Qu…

逆向-还原代码之eth (Interl 64)

// 源程序 #include <stdio.h> #define HIETH_SYSREG_BASE (0x101e0000) #define REG_RESET 0x01C // 外设控制寄存器(IP软复位控制) #define RESET_SHIFT 12 static void hieth_set_regbit(unsigned long addr, int bit, int shift) { unsigned long …

nginx学习笔记1(小d课堂)

我们进入到官网可以看到有很多个版本的nginx。 我们点击documentation&#xff0c;可以看到官方文档&#xff0c;但是这里的文档暂时还没有中文的&#xff1a; 我们这里后期会在linux上进行安装部署nginx。 而我们的nginx就是我们的反向代理服务器。 我们可以这样来配置。 我们…

栈和队列(内附模拟实现代码)

一&#xff0c;栈1.1 栈的概念栈是一种线性表&#xff08;是一种特殊的线性表&#xff09;&#xff0c;栈只允许在固定一端进行插入和删除元素。插入元素的一端称为栈顶&#xff0c;另一端称为栈底。所以栈中的数据元素满足先进后出&#xff08;First In Last Out&#xff09;的…

【数据篇】31 # 如何对海量数据进行优化性能?

说明 【跟月影学可视化】学习笔记。 渲染动态的地理位置 用随机的小圆点模拟地图的小圆点&#xff0c;实现呼吸灯效果 最简单的做法&#xff1a;先创建圆的几何顶点数据&#xff0c;然后对每个圆设置不同的参数来分别一个一个圆绘制上去。 <!DOCTYPE html> <html …

如何使用python删除一个文件?别说,还挺好用....

嗨害大家好鸭&#xff01;我是小熊猫~ 若想利用python删除windows里的文件&#xff0c;这里需要使用os模块&#xff01;那接下来就看看利用os模块是如何删除文件的&#xff01; 具体实现方法如下&#xff01; 更多学习资料:点击此处跳转文末名片获取 os.remove(path) 删除文…

Java位运算符:Java移位运算符、复合位赋值运算符及位逻辑运算符

Java 定义的位运算&#xff08;bitwise operators&#xff09;直接对整数类型的位进行操作&#xff0c;这些整数类型包括 long&#xff0c;int&#xff0c;short&#xff0c;char 和 byte。位运算符主要用来对操作数二进制的位进行运算。按位运算表示按每个二进制位&#xff08…

GitLab安装使用(SSH+Docker两种方式)

GitLab安装使用1、在ssh下安装gitlab1.1 安装依赖1.2 配置镜像1.3 开始安装1.4 gitlab常用命令2、在docker下安装gitlab2.1 安装docker2.1.1 更新yum源2.1.2 安装依赖2.1.3 添加镜像2.1.4 查看源中可用版本2.1.5 安装指定版本2.1.6 配置开机启动项2.2 使用容器安装gitlab2.2.1 …

车载以太网 - DoIP头部信息检测逻辑 - 03

通过前面的文章我们已经了解了DoIP所具备的Payload类型,基础的信息都已经具备了,今天我们就要进一步的去了解DoIP的处理逻辑了;按照正常的逻辑来看,处理无论是我们人眼去看书,还是计算机处理一段数据,都是从前到后依次进行处理;而DoIP的信息处理也不例外,也是从头开始进…

2023跨境出海指南:印度网红营销白皮书

前不久&#xff0c;联合国预测印度人口将在4个月后超过中国&#xff0c;成为全球第一人口大国。印度这个国家虽然有些奇葩&#xff0c;但他们的经济实力确实不能小觑&#xff0c;这也是众多国际公司大力发展印度的原因。出海印度容易&#xff0c;但攻克印度市场太难&#xff0c…

Python Tutorial——类

与其它编程语言相比&#xff0c;Python的类机制添加了最小的新语法和语义。它是C和Modula-3中的类机制的混合。Python的类提供了面向对象编程的所有的标准特性&#xff0c;类继承机制允许有多个基类&#xff0c;一个子类可以重写基类中的任何方法&#xff0c;一个方法可以调用基…