基于Redis实现短信验证码登录项目示例(附源码)

目录
  • Redis短信登录流程描述
    • 短信验证码的发送
    • 短信验证码的验证
    • 是否登录的验证
  • 源码分析
    • 模拟发送短信验证码
    • 短信验证码的验证
    • 校验是否登录
    • 登录验证优化

Redis短信登录流程描述

短信验证码的发送

用户提交手机号,系统验证手机号是否有效,毕竟无效手机号会消耗你的短信验证次数还会导致系统的性能下降。如果手机号为无效的话就让用户重新提交手机号,如果有效就生成验证码并将该验证码作为value保存到redis中对应的key是手机号,之所以这么做的原因是保证key的唯一性,如果使用固定字符串作为可以的话会被后面的数据所覆盖。然后在控制台输出验证码模拟发送验证码的过程

短信验证码的验证

用户的手机号接收到验证码后在平台上提交验证码,系统从redis中根据手机号读取验证码并进行校验,如果验证通过的话就根据用户验证使用的手机号去数据库中进行查询用户信息。如果存在就将查询到的用户信息保存到redis中,完成登录;如果不存在的话就创建一个新用户,并将该用户的信息分别保存到sql数据库和redis中,生成随机token作为key、使用hash结构存储user数据作为value,并将这个token返回给客户端,至此完成登录注册

是否登录的验证

用户访问系统业务逻辑的时候需要校验他是否已经登录,如果登录可以访问否则就去登录,那么该如何完成是否登录的校验呢?这就要了解session的相关知识了,每一个session都有一个sessionId信息保存在浏览器的cookie中,当用户使用浏览器发送请求的时候会携带上cookie信息,此时系统就可以使用cookie中的sessionId获取到session信息,并通过session获取到登录时存储的用户信息。如果此时用户在数据库中存在的话就将该用户的信息缓存在ThreadLocal(方便后续验证)中,并放行该访问;否则就说明发送请求的用户未登录或不合法,就要拦截到他的请求前往登录

源码分析

模拟发送短信验证码

UserController定义与前端交互

@Resource
private IUserService userService;

/**
 * 发送手机验证码
 */
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
    // 发送短信验证码并保存验证码
    return userService.sendCode(phone, session);
}

上面使用到了sendCode方法,在userService里定义一下接口,然后在对应实现类中按照上面的流程重写该方法的业务逻辑代码

@Override
public Result sendCode(String phone, HttpSession session) {
    // 校验手机号
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 无效手机号,返回错误信息
        return Result.fail("手机号格式有误!");
    }
    // 有效生成验证码
    String code = RandomUtil.randomNumbers(6);
    // 保存 (固定前缀+手机号) 和验证码到Redis中,设置验证码的有效期为2分钟
    // RedisConstants.LOGIN_CODE_KEY = “login:code:”
    // RedisConstants.LOGIN_CODE_TTL = 2L
    stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
    // 模拟发送验证码
    log.debug("验证码:{}", code);
    // 返回
    return Result.ok();
}

手机号格式校验使用到的RegexUtils类中的工具方法

/**
 * 手机号正则
 */
public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";

/**
* 是否是无效手机格式
 * @param phone 要校验的手机号
 * @return true:符合,false:不符合
 */
public static boolean isPhoneInvalid(String phone){
    return mismatch(phone, RegexPatterns.PHONE_REGEX);
}

// 校验是否不符合正则格式
private static boolean mismatch(String str, String regex){
    if (StrUtil.isBlank(str)) {
        return true;
    }
    return !str.matches(regex);
}

短信验证码的验证

UserController定义与前端交互,其中参数LoginFormDTO 是前端使用手机号+验证码登录或者手机号+密码登录是传递过来的JSON数据

/**
 * 登录功能
 * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
 */
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
    // 实现登录功能
    return userService.login(loginForm, session);
}

上面使用到了login方法,在userService里定义一下接口,然后在对应实现类中按照上卖弄的流程描述重写该方法的业务逻辑代码

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    String phone = loginForm.getPhone();

    // 验证码校验
    String code = loginForm.getCode();
    String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
    if (cacheCode == null || !code.equals(cacheCode)) {
        return Result.fail("验证码错误!");
    }

    // 根据手机号查询用户信息
    User user = query().eq("phone", phone).one();
    if (user == null) {
        // 不存在就创建一个新用户
        user = createUserWithPhone(phone);
    }

    // 保存用户信息到redis中
    // 生成随机token
    String token = UUID.randomUUID().toString(true);
    // user先转userDTO再转hashMap存储  转HashMap时的第三个参数的意思是忽略null值将值都转换成String类型
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
            CopyOptions.create()
                    .setIgnoreNullValue(true)
                    .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
    // RedisConstants.LOGIN_USER_KEY = "login:token:"
    stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap);
    // 设置失效时间为30分钟
    // RedisConstants.LOGIN_USER_TTL = 30L
    stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
    // 返回前端token
    return Result.ok(token);
}

private User createUserWithPhone(String phone) {
    User user = new User();
    user.setPhone(phone);
    // SystemConstants.USER_NICK_NAME_PREFIX = "user_"
    user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
    save(user);
    return user;
}

保存的时候使用BeanUtil将User转换成UserDTO进行存储,UserDTO的结构如下,只保存一部分的数据,一方面可以不用来回传递用户有关的隐私数据,一方面也节省内存提高性能。由于这里的id是数值类型,但是stringRedisTemplate存储时需要hash的键值都是String型,所以说应该在存储之前将id的值转换成String类型,就在上面代码块的24~27行完成了这个操作

@Data
public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
}

校验是否登录

用户发送请求不止一次,所以说登录验证也不止进行一次,于是可以使用拦截器完成验证,拦截器的使用可分为两步:

创建拦截器

/**
 * @author : mereign
 * @date : 2022/5/5 - 10:31
 * @desc : 拦截器,实现请求拦截,判断登录信息
 */
@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取请求头中的token信息
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            // token为空,返回401未授权状态码,拦截
            response.setStatus(401);
            return false;
        }
        // 根据token获取redis中的用户value
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
        HttpSession session = request.getSession();

        // 判断用户是否存在
        if (userMap.isEmpty()) {
            // 用户不存在,返回401未授权状态码,拦截
            response.setStatus(401);
            return false;
        }

        // 用户存在,将hash数据转换为userDTO,存信息到ThreadLocal
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        UserHolder.saveUser(userDTO);

        // 刷新token有效期,放行
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

注册拦截器

/**
 * @author : mereign
 * @date : 2022/5/5 - 10:43
 * @desc :
 */
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .excludePathPatterns(
                        "/shop/**",
                        "/shop-type/**",
                        "/voucher/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
    }
}

缓存用户的信息到ThreadLocal中的工具方法

public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    public static UserDTO getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}

UserController定义与前端交互

@GetMapping("/me")
public Result me(){
    // 获取当前登录的用户并返回
    UserDTO user = UserHolder.getUser();
    return Result.ok(user);
}

登录验证优化

由上面的登录验证可知,我们对一些需要用户登录验证的功能设置了拦截器,如果验证通过会刷新token的有效期,这样的话只要用户一直访问我们拦截的功能就可以一直保持token是有效的。但是,如果用户登陆之后的操作一直是不需要验证的,那也就意味着token的有效期一直不会刷新,这样的话30分钟之后token就会失效用户验证就会失败,这样显然是不合理的
于是我们可以使用两个拦截器完成,最前面的负责拦截所有的请求,获取token、从redis中查询用户,将查询结果放到ThreadLocal(可能存null)、刷新token有效期,最后直接放行;后面的拦截器只负责判断有没有从redis中查询到用户,他从ThreadLocal获取查询结果,判断有则放行无则拦截

创建两个拦截器

/**
 * @author : mereign
 * @date : 2022/5/5 - 10:31
 * @desc : 前置拦截器,拦截所有请求,前置工作
 */
@Component
public class RefreshTokenInterceptor implements HandlerInterceptor {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取请求头中的token信息
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            // token为空 直接放行
            return true;
        }

        // 根据token获取redis中的用户value
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
        HttpSession session = request.getSession();
        // 判断用户是否存在
        if (userMap.isEmpty()) {
            // 用户不存在 直接放行
            return true;
        }

        // 用户存在,将hash数据转换为userDTO,存信息到ThreadLocal
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        UserHolder.saveUser(userDTO);

        // 刷新token有效期,放行
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}
/**
 * @author : mereign
 * @date : 2022/5/5 - 10:31
 * @desc : 登录拦截器,拦截需要拦截的请求,判断登录信息
 */
@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 判断登录
        if (UserHolder.getUser() == null) {
            response.setStatus(401);
            return false;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

创建完拦截器之后要将两个拦截器通过配置类配置到容器中生效,多个拦截器的优先级,默认按照添加顺序执行优先级,但是也可以使用order方法指定优先级,按参数的大小排序优先级,参数越小优先级越高

/**
 * @author : mereign
 * @date : 2022/5/5 - 10:43
 * @desc : 配置类注册拦截器
 */
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    private RefreshTokenInterceptor refreshTokenInterceptor;
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 前置拦截器
        registry.addInterceptor(refreshTokenInterceptor)
                .addPathPatterns("/**")
                .order(0);
        // 后置拦截器
        registry.addInterceptor(loginInterceptor)
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                )
                .order(1);
    }
}

到此这篇关于基于Redis实现短信验证码登录项目示例(附源码)的文章就介绍到这了,更多相关Redis 短信验证码登录内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java实现发送短信验证码+redis限制发送的次数功能

    java实现短信验证码发送,由于我们使用第三方平台进行验证码的发送,所以首先,我们要在一个平台进行注册.这样的平台有很多,有的平台在新建账号的时候会附带赠几条免费短信.这里我仅做测试使用(具体哪个平台见参考三,很简单,注册账号就行,记得添加短信签名). 另外,在实际项目中,如果有人恶意攻击,不停的发送短信验证码,就会造成很大的损失.故对发送次数做一定的限制就非常必要,这里我们限制一个手机号一天可以发多少短信和短信平台无关. 这里采用的是存redis来实现这一个功能.就是每次调用发送验证码这个接口

  • 基于Redis实现短信验证码登录项目示例(附源码)

    目录 Redis短信登录流程描述 短信验证码的发送 短信验证码的验证 是否登录的验证 源码分析 模拟发送短信验证码 短信验证码的验证 校验是否登录 登录验证优化 Redis短信登录流程描述 短信验证码的发送 用户提交手机号,系统验证手机号是否有效,毕竟无效手机号会消耗你的短信验证次数还会导致系统的性能下降.如果手机号为无效的话就让用户重新提交手机号,如果有效就生成验证码并将该验证码作为value保存到redis中对应的key是手机号,之所以这么做的原因是保证key的唯一性,如果使用固定字符串作为

  • Redis实现短信验证码登录的示例代码

    目录 效果图 pom.xml applicatoin.yml Redis配置类 controller serviceImpl mapper 效果图 发送验证码 输入手机号.密码以及验证码完成登录操作 pom.xml 核心依赖 <dependencies> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version&g

  • vue实现通过手机号发送短信验证码登录的示例代码

    本文主要介绍了vue实现通过手机号发送短信验证码登录的示例代码,分享给大家,具体如下: <template> <div class="get-mobile" @touchmove.prevent> <div class="main"> <div class="pt-20 pr-15 pl-15 pb-20"> <input class="input mb-15" v-mod

  • Spring+SpringMVC+JDBC实现登录的示例(附源码)

    有一位程序员去相亲的时候,非常礼貌得说自己是一名程序员,并解释自己是做底层架构的,于是女方听到"底层"两个字,就一脸嫌弃:什么时候能够到中高级? 用久了框架,把原生都忘记了,本章从零开始,熟悉一遍JDBC实现增删改查 开发环境 jdk 1.8 Maven 3.6 Spring.SpringMVC 4.3.18 dbcp2 jsp Idea 创建项目 创建项目时,我们选择传统的Maven项目结构 创建项目时不要选择任何模板,直接点Next 填写包名及项目名Next --> Fini

  • java短信验证码登录功能设计与实现

    目录 前言 业务案例 业务关键点剖析 短信验证码功能实现思路 有效期问题 操作步骤 前言 现在不管是各类的网站,还是大小社交app,登录方式是越来越多了,其中基于短信验证码的登录可以说是各类app必不可少的方式,短信验证码登录以其高效,安全,便捷等特性受到许多用户的青睐 业务案例 如下所示,是一个大家熟知的采用短信登录的入口 输入手机号之后,出现如下效果, 输入手机上面收到的验证码之后,就可以正常登录了 业务关键点剖析 以上是一个正常的使用短信验证码登录的业务流程,在实际开发中,需要考虑的因素更

  • 基于 antd pro 的短信验证码登录功能(流程分析)

    概要 最近使用 antd pro 开发项目时遇到个新的需求, 就是在登录界面通过短信验证码来登录, 不使用之前的用户名密码之类登录方式. 这种方式虽然增加了额外的短信费用, 但是对于安全性确实提高了不少. antd 中并没有自带能够倒计时的按钮, 但是 antd pro 的 ProForm components 中倒是提供了针对短信验证码相关的组件. 组件说明可参见: https://procomponents.ant.design/components/form 整体流程 通过短信验证码登录的

  • SSM项目实现短信验证码登录功能的示例代码

    目录 1.登入网站 zz短信平台 2.导入工具类MessageUtil 3.ajax 模块 4. html页面 5.编写controller层 1.登入网站 zz短信平台 http://sms_developer.zhenzikj.com/zhenzisms_user/login.html 导入pom依赖 <dependency> <groupId>com.zhenzikj</groupId> <artifactId>zhenzisms</artifa

  • 基于PHP实现短信验证码接口(容联运通讯)

    自己也是刚刚研究,希望对也在研究的伙伴有帮助. 步骤: 1.登录荣联运通讯注册获取ACCOUNT SID.AUTH TOKEN.Rest URL(生产).AppID(默认): 2.注册测试用手机号码(先注册测试号码方可使用): 3.下载demo示例,并将代码放到项目中(最好单独建文件夹存储). 代码区: 一.新建test.app.php(测试用控制器) <?php /* * 短信接口测试 */ class TestApp extends ShoppingbaseApp{ public funct

  • Spring Security OAuth2集成短信验证码登录以及第三方登录

    前言 基于SpringCloud做微服务架构分布式系统时,OAuth2.0作为认证的业内标准,Spring Security OAuth2也提供了全套的解决方案来支持在Spring Cloud/Spring Boot环境下使用OAuth2.0,提供了开箱即用的组件.但是在开发过程中我们会发现由于Spring Security OAuth2的组件特别全面,这样就导致了扩展很不方便或者说是不太容易直指定扩展的方案,例如: 图片验证码登录 短信验证码登录 微信小程序登录 第三方系统登录 CAS单点登录

  • Spring Security Oauth2.0 实现短信验证码登录示例

    本文介绍了Spring Security Oauth2.0 实现短信验证码登录示例,分享给大家,具体如下: 定义手机号登录令牌 /** * @author lengleng * @date 2018/1/9 * 手机号登录令牌 */ public class MobileAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecur

随机推荐