SpringSecurity自定义实现手机短信登录

news2024/11/15 19:30:55

SpringSecurity自定义登录验证-手机验证码登录

其实实现原理上跟账号密码登录一样的

1、自定义短信验证Token

定义一个仅使用手机号验证权限的鉴权Token,SpringSecurity原生的UsernamePasswordAuthenticationToken是使用username和password,如下图

1

principal相当于username,credentials相当于password,所以我们仿照他的写一个跟据手机号鉴权的Token即可:

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

import java.util.Collection;

/**
 * 短信登录令牌
 */
public class SmsAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private final Object telephone;

    /**
     * SmsCodeAuthenticationFilter中构建的未认证的Authentication
     *
     * @param telephone
     */
    public SmsAuthenticationToken(Object telephone) {
        super(null);
        this.telephone = telephone;
        this.setAuthenticated(false);
    }

    /**
     * SmsCodeAuthenticationProvider中构建已认证的Authentication
     *
     * @param telephone
     * @param authorities
     */
    public SmsAuthenticationToken(Object telephone, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.telephone = telephone;
        super.setAuthenticated(true);
    }

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

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

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

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

2、实现UserDetailsService接口

这一步就是封装权限的操作,实现类似于SpringSecurity的账号密码封装权限,只不过这里调用userService传入的参数是手机号,如果用户存在就返回一个带有权限的 UserDetails 实现类对象(我这里实现类是LoginUser

/**
 * 查询短信登录信息并封装为 UserDetails 这里可以抽取一个抽象类,权限加载和校验租户等逻辑交给父类处理
 */
@Service("smsUserDetailsService")
public class SmsUserDetailsService implements UserDetailsService {
    private static final Logger log = LoggerFactory.getLogger(SmsUserDetailsService.class);

    @Resource
    private ISysUserService userService;

    @Resource
    private SysPermissionService permissionService;

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

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

}

3、自定义认证authenticate

第二步返回了带有权限的 LoginUser 对象,在这里需要重写authenticate()方法,调用loadUserByUsername()方法实现身份认证逻辑返回验证Token

/**
 * 短信登录校验器
 */
//@Component
public class SmsAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    public SmsAuthenticationProvider(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    /**
     * 重写 authenticate方法,实现身份验证逻辑。
     *
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
        String telephone = (String) authenticationToken.getPrincipal();// 获取凭证也就是用户的手机号
        // 根据手机号查询用户信息UserDetails
        UserDetails userDetails = userDetailsService.loadUserByUsername(telephone);
        if (StringUtil.isEmpty(userDetails)) {
            throw new InternalAuthenticationServiceException("用户不存在");
        }
        // 鉴权成功,返回一个拥有鉴权的 AbstractAuthenticationToken
        SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities());
        smsAuthenticationToken.setDetails(authenticationToken.getDetails());
        return smsAuthenticationToken;
    }

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

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

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

4、SecurityConfig

这里既使用用户名密码登录也使用手机短信登录,所以UserDetailsService自定义的手机短信实现类加个@Qualifier注解防止注入失败,UserDetailsService实现类分别指定别名按别名注入:

// 分别去你的实现类里配置
// 账号密码登录
@Service("userDetailsServiceImpl")
public class UserDetailsServiceImpl implements UserDetailsService {}
@Service("smsUserDetailsService")
// 自定义的手机短信登录
public class SmsUserDetailsService implements UserDetailsService {}

在配置文件中添加自定义的手机短信认证,并放行登录接口。其他配置已省略。

/**
 * spring security配置
 */
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 自定义用户认证逻辑
     */
    @Resource
    @Qualifier("userDetailsServiceImpl")
    private UserDetailsService userDetailsService;

    /**
     * 自定义手机短信登录
     */
    @Resource
    @Qualifier("smsUserDetailsService")
    private UserDetailsService smsUserDetailsService;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
		// 添加手机号短信登录
        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                .antMatchers("/sms-login").anonymous()
                // 。。。。。。

    }

    /**
     * 身份认证接口,添加自定义的手机短信认证
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(new SmsAuthenticationProvider(smsUserDetailsService));
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}

5、验证接口

既然是手机短信登录,这里申请aliyun短信服务或别的企业的短信服务就忽略了,直接使用固定短信验证码+redis的方式。

写一个接口跟据手机号发送验证码,并将验证码存入redis,再响应给前端:

/**
 * 短信发送
 *
 * @param phone
 * @return
 */
@GetMapping("/sendCode/{phone}")
public AjaxResult sendCode(@PathVariable("phone") String phone) {

    SmsCode smsCode = aliyunSmsService.sendCode(phone);

    return AjaxResult.success(smsCode.getCode());
}

此处aliyunSmsService.sendCode(phone)逻辑可以自行百度aliyun短信服务。

登录接口:

/**
 * 手机验证码登录方法
 *
 * @param smsLoginBody
 * @return 结果
 */
@PostMapping("/sms-login")
public AjaxResult smsLogin(@RequestBody SmsLoginBody smsLoginBody) {
    // 生成令牌
    log.info("手机验证码登录:{}",smsLoginBody.getTelephone());
    String token = loginService.smsLogin(smsLoginBody.getTelephone(), smsLoginBody.getCode());
    return AjaxResult.success().put(Constants.TOKEN, token);
}

方法实现:

/**
 * 手机验证码登录
 *
 * @param telephone
 * @param code
 * @return
 */
public String smsLogin(String telephone, String code) {
    // 未携带手机号或验证码
    if (StringUtil.isEmpty(telephone)) {
        throw new TelePhoneException();
    }
    if (StringUtil.isEmpty(code)) {
        throw new CaptchaException();
    }
    // 获取手机验证码
    String verifyKey = CacheConstants.ALIYUN_SMS_KEY + telephone;
    String phoneCode = redisTemplate.opsForValue().get(verifyKey);
    if (StringUtil.isEmpty(phoneCode)) {
        throw new SmsException("验证码已失效");
    }
    if (!phoneCode.equals(code)) {
        throw new SmsException("验证码错误");
    }
    // 删除key
      redisTemplate.delete(verifyKey);
    // 通过手机号获取用户
    SysUser userByTelephone = userService.getUserByTelephone(telephone);
    if (StringUtil.isEmpty(userByTelephone)) {
        throw new TelePhoneException();
    }
    // 用户验证
    Authentication authentication = null;
    String username = userByTelephone.getUserName();
    try {
        SmsAuthenticationToken authenticationToken = new SmsAuthenticationToken(telephone);
        AuthenticationContextHolder.setContext(authenticationToken);
        // 该方法会去调用 SmsUserDetailsService.loadUserByUsername
        authentication = authenticationManager.authenticate(authenticationToken);
    } catch (Exception e) {
        if (e instanceof BadCredentialsException) {
            // 异步记录日志
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(telephone, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
            throw new UserPasswordNotMatchException();
        } else {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(telephone, Constants.LOGIN_FAIL, e.getMessage()));
            throw new ServiceException(e.getMessage());
        }
    } finally {
        AuthenticationContextHolder.clearContext();
    }
    AsyncManager.me().execute(AsyncFactory.recordLogininfor(telephone, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));

    LoginUser loginUser = (LoginUser) authentication.getPrincipal();
    // 记录登录信息,修改用户表,添加登录IP、登录时间
    recordLoginInfo(loginUser.getUserId());
    // 生成token
    return tokenService.createToken(loginUser);
}

如果是若依系统,记得所有用户要有role角色。

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

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

相关文章

向量时钟算法

向量时钟不仅同步本进程的时钟值&#xff0c;而且还同步已知的其他进程时钟值 分布式系统中每个进程Pi保存一个本地逻辑时钟向量值VCi&#xff0c;VCi(j)代表进程Pi知道的进程Pj的本地逻辑时钟值 初始化VCi向量为[0,…]进程Pi每发生一次事件&#xff0c;VCi[i]加一进程Pi给进…

应付模块无法关账问题 APP-AR-11332 您必须在关闭此期之前过账其中的所有事务处理

问题描述 AR关账时遇到了这个问题&#xff0c;根本原因是&#xff0c;因为用户录入另一个贷项的发票&#xff0c;做过核销&#xff0c;后来又取消了核销&#xff0c;未创建会计分类&#xff0c;未传送总&#xff0c;不想要这个贷项发票了&#xff0c;前台删除不了&#xff0c;…

经验分享,api 接口设计原则有这几条

结合我多年在 API 行业摸爬滚打的经验&#xff0c;我总结了一下&#xff0c;API 接口设计原则有这几条&#xff1a; 接口设计应该简单易用&#xff0c;易于理解和使用&#xff1b; 接口设计应该支持多种格式&#xff0c;如JSON、XML等&#xff1b; 接口设计应该支持多种请求方…

渲染速度慢,使用云渲染会快多少?

设计师在使用软件制作效果图和动画师在制作动画时&#xff0c;其中有一个比较关键的环节就是渲染成像&#xff0c;渲染的效率主要跟使用的电脑显卡或CPU性能有关&#xff0c;如果性能太低&#xff0c;渲染的速度会很慢&#xff0c;拉长了项目整体的交付周期&#xff0c;云渲染速…

Vite + Vue3 实现前端项目工程化

Vue3 发布至今&#xff0c;周边的生态、技术方案已足够成熟&#xff0c;个人认为新项目是时候切换到 Vite Vue3 了。今天就给大家操作一下这种技术方案实现前端工程化。 1. 初始化项目 通过官方脚手架初始化项目 第一种方式&#xff0c;这是使用vite命令创建&#xff0c;这种…

FM33A048B SPI1/2

概述 芯片的2 个SPI 接口模块SPI1 和SPI2&#xff0c;可配置为主设备或从设备&#xff0c;实现与外部的SPI 通信。 特点&#xff1a; ⚫ 全双工3线串行同步收发 ⚫ 2路独立通道 ⚫ 主从模式 ⚫ 可编程时钟极性和相位 ⚫ 可编程比特速率 ⚫ 从模式最大频率为FAHBCLK/2 ⚫ 传输结…

Hello算法——笔记

文章目录 1 引言算法数据结构算法和数据结构的关系 2 复杂度分析时间复杂度空间复杂度 3 数据结构数据与内存数据结构分类 4 数组与链表 参考资料 1 引言 算法 算法是一组用于解决特定问题或执行特定任务的明确定义的计算步骤或指令集合。算法可以被视为一种解决问题的方法或…

【iOS】--手势操作

文章目录 UIGestureRecognizer 的继承关系&#xff1a; 使用手势步骤UIPanGestureRecognizer&#xff08;拖动&#xff09;UIPinchGestureRecognizer(拖动&#xff09;UIRotationGestureRecognizer&#xff08;旋转&#xff09;UITapGestureRecognizer&#xff08;点按&#xf…

浅谈造纸配电室环境监控系统的应用案例

摘要&#xff1a;智能配电室环境监控系统可实现自动巡检、自动预警等功能&#xff0c;减少人员到现场巡视次数&#xff0c;能及早发现设备的潜在风险&#xff0c;迅速检测故障&#xff0c;节约维护保养时长&#xff0c;为配电生产检修、运行、各业务的标准化、规范化管理提供有…

【Flowable】Flowable候选人和候选人组

在流程定义中在任务结点的 assignee 固定设置任务负责人&#xff0c;在流程定义时将参与者固定设置在.bpmn 文件中&#xff0c;如果临时任务负责人变更则需要修改流程定义&#xff0c;系统可扩展性差。针对这种情况可以给任务设置多个候选人或者候选人组&#xff0c;可以从候选…

IntelliJ IDEA编辑模板变量详解(Edit Template Variables)

函数描述annotated(“annotation qname”)使用驻留在指定位置的注释创建类型符号。 例如&#xff0c;请参见迭代组中的活动模板。anonymousSuper()为Kotlin对象表达式建议一个超类型。arrayVariable()建议当前作用域中适用的所有数组变量。 例如&#xff0c;请参见迭代组中的活…

ACM - DP习题集(word里面的一小部分题集)

DP 一、经典问题1、编辑距离2、扔鸡蛋问题3、整数背包4、最大独立集5、最长公共子序列6、最长公共递增子序列7、最长公共子串&#xff08;ing&#xff09;8、最长上升子序列9、最长回文子序列10、最长回文子串&#xff08;ing&#xff09;11、最长不重复子字符串&#xff08;in…

OpenGL教程中矩阵Matrix的介绍

变换 原文Transformations作者JoeyDeVries翻译Django, Krasjet, BLumia校对暂未校对 尽管我们现在已经知道了如何创建一个物体、着色、加入纹理&#xff0c;给它们一些细节的表现&#xff0c;但因为它们都还是静态的物体&#xff0c;仍是不够有趣。我们可以尝试着在每一帧改变…

Python 操作 Excel,如何又快又好?

➤数据处理是 Python 的一大应用场景&#xff0c;而 Excel 则是最流行的数据处理软件。因此用 Python 进行数据相关的工作时&#xff0c;难免要和 Excel 打交道。Python处理Excel 常用的系列库有&#xff1a;xlrd、xlwt、xlutils、openpyxl ◈xlrd &#xff0d; 用于读取 Exce…

import一个“太极”库,让Python代码提速100倍!

众所周知&#xff0c;Python的简单和易读性是靠牺牲性能为代价的—— 尤其是在计算密集的情况下&#xff0c;比如多重for循环。 不过现在&#xff0c;大佬胡渊鸣说了&#xff1a; 只需import 一个叫做“Taichi”的库&#xff0c;就可以把代码速度提升100倍&#xff01; 不信…

0-1规划在编程问题中的应用(UnityC#脚本/折返约瑟夫/OpenGL机器人摆臂循环)

一、0-1规划的定义 百度百科的解释&#xff1a;0-1规划是决策变量仅取值0或1的一类特殊的整数规划。在处理经济管理中某些规划问题时&#xff0c;若决策变量采用 0-1变量即逻辑变量&#xff0c;可把本来需要分别各种情况加以讨论的问题统一在一个问题中讨论。 如上面所说&…

记csdn打不开或打开缓慢后的修复--如何查找dns并修改hosts文件

记csdn打开缓慢后的修复–如何查找dns并修改hosts文件 问题&#xff1a; CSDN文章打开的十分缓慢&#xff0c;经常出现无法打开页面的错误提示 &#xff08;以前用的好好的&#xff0c;现在不知道公司局域网改了什么东西&#xff0c;导致我的电脑打开CSDN经常缓慢好久&#x…

ACM - 其他算法 - 基础(前缀和 + 差分)

ACM- 其他算法 一、前缀和模板例题1、区间余数求K倍区间个数&#xff1a;AcWing 1230. K倍区间例题2、前缀和哈希求最长个数平分子串:Leetcode 面试题 17.05 字母与数字 二、差分1、一维差分2、二维差分 一、前缀和 模板 //一维前缀和 S[i] a[1] a[2] ... a[i] a[l] ... …

【行情速递】MLCC龙头涨价;车厂砍单芯片;台积电28nm设备订单全部取消!

导语&#xff1a;进入第二季度&#xff0c;MLCC龙头三环集团官宣涨价!风华高科紧随其后。车市价格战蔓延至芯片端&#xff0c;车厂开始砍单芯片&#xff0c;短短半年时间不到&#xff0c;车用芯片市场从价格飞涨和一片难求的背景&#xff0c;转为砍单与降价促销...更多详情请阅…

智能警示输出器在ESD系统中的作用

ESD&#xff08;Electrostatic Discharge&#xff09;是指静电放电&#xff0c;是一种电子元器件损坏的主要原因之一。在电子制造业中&#xff0c;ESD防静电系统是非常重要的&#xff0c;可以有效地保护电子元器件&#xff0c;避免因静电放电而造成的损坏和故障。而智能警示输出…