Redis实现短信登录的企业实战

目录
  • 一、导入黑马点评项目
    • 1. 导入SQL
    • 2. 前后端分离
    • 3. 导入后端项目
    • 4. 导入前端项目
  • 二、基于Session实现登录流程
    • 1. 发送短信验证码
    • 2. 短信验证码登录、注册
    • 3. 登录验证功能
  • 三、集群的session共享问题
  • 四、基于Redis实现共享session的登录功能
    • 1. 选择合适的数据结构存入Redis
    • 2. 发送短信验证码
    • 3. 短信验证码登录、注册
    • 4. 解决token刷新问题

一、导入黑马点评项目

黑马点评项目主要包括以下功能:

这一章主要介绍短信登录功能,短信登录功能是基于Redis的共享session实现的

1. 导入SQL

需要项目资料的私信我

其中的表有:

  • tb_user:用户表
  • tb_user_info:用户详情表
  • tb_shop:商户信息表
  • tb_shop_type:商户类型表
  • tb_blog:用户日记表(达人探店日记)
  • tb_follow:用户关注表
  • tb_voucher:优惠券表
  • tb_voucher_order:优惠券的订单表

注意:Mysql的版本采用5.7及以上版本

2. 前后端分离

3. 导入后端项目

3.1 将后端项目导入到 Idea 中

3.2 注意:修改application.yaml文件中的mysql、redis地址信息 将mysql、redis地址信息修改为自己的信息

3.3 启动项目 启动项目后,在浏览器访问:http://localhost:8081/shop-type/list ,如果可以看到数据则证明运行没有问题

4. 导入前端项目

4.1 导入nginx文件夹 将nginx文件夹复制到任意目录,要确保该目录不包含中文、特殊字符和空格,例如:

4.2 运行前端项目 在nginx所在目录下打开一个CMD窗口,输入命令启动nginx:

start nginx.exe

打开chrome浏览器,在空白页面点击鼠标右键,选择检查,即可打开开发者工具:

然后访问: http://127.0.0.1:8080 ,即可看到页面:

二、基于Session实现登录流程

  • 后端将生成的验证码和用户信息保存到session中,并将sessionId返回给前端保存到cookie中
  • 用户登录时,会携带cookie向后端发起请求,后端进行校验时,从cookie中获取sessionId,通过sessionId可以从session中获取用户信息并保存到ThreadLocal中
  • 后续每个线程都有一份ThreadLocal中的用户副本信息,不同线程拿到用户信息后可以实现不同的操作,从而起到线程隔离作用

1. 发送短信验证码

主要代码:

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private IUserService userService;

    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        // 发送短信验证码并保存验证码
        return userService.sendCode(phone, session);
    }
}
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1.使用工具类校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3.符合,生成验证码
        String code = RandomUtil.randomNumbers(6);

        // 4.保存验证码到 session
        session.setAttribute("code",code);

        // 5.模拟发送验证码
        log.debug("发送短信验证码成功,验证码:{}", code);
        // 返回ok
        return Result.ok();
    }
}

2. 短信验证码登录、注册

主要代码:

UserController

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

UserServiceImpl

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1.校验手机号
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 如果不符合,返回错误信息
        return Result.fail("手机号格式错误!");
    }

    // 2.校验验证码
    Object cacheCode = session.getAttribute("code");
    String code = loginForm.getCode();
    if (cacheCode == null || !cacheCode.toString().equals(code)) {
        // 3.验证码不一致,则报错
        return Result.fail("验证码错误");
    }

    // 4.验证码一致,根据手机号查询用户
    User user = query().eq("phone", phone).one();

    // 5.判断用户是否存在
    if (user == null) {
        // 6.用户不存在,则创建用户并保存
        user = createUserWithPhone(phone);
    }

    // 7.保存用户信息到session中,UserDTO只包含简单的用户信息,
    // 而不是完整的User,这样可以隐藏用户的敏感信息(例如:密码等),还能减少内存使用
    session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));

    // 8.返回ok
    return Result.ok();
}

private User createUserWithPhone(String phone) {
    // 1.创建用户
    User user = new User();
    user.setPhone(phone);
    // 随机设置昵称 user_mrkuw05lok
    user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
    // 2.保存用户
    save(user);
    return user;
}

3. 登录验证功能

用户请求登录时,会携带cookie,cookie中包含JSEESIONID

为了避免用户请求每个controller时,每次都去校验用户信息,所以可以加拦截器

拦截器只需在用户请求访问时,校验一次后将用户信息保存到ThreadLocal中,供后续线程使用

主要代码:

在工具类中编写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();
    }
}

在工具类中编写登录拦截器

public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 前置拦截
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取session
        HttpSession session = request.getSession();
        // 2.获取session中的用户
        Object user = session.getAttribute("user");
        // 3.判断用户是否存在
        if(user == null){
            // 4.不存在,拦截,返回401状态码
            response.setStatus(401);
            return false;
        }
        // 5.存在,保存用户信息到ThreadLocal
        UserHolder.saveUser((User)user);
        // 6.放行
        return true;
    }

    /**
     * 后置拦截器
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
        Object handler, Exception ex) throws Exception {
        // 请求结束后移除用户,防止ThreadLocal造成内存泄漏
        UserHolder.removeUser();
    }
}

在配置类中添加拦截器配置类

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    /**
     * 添加拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
            // 排除不需要拦截的路径
            .excludePathPatterns(
                "/shop/**",
                "/voucher/**",
                "/shop-type/**",
                "/upload/**",
                "/blog/hot",
                "/user/code",
                "/user/login"
            );
    }
}

UserController

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

三、集群的session共享问题

四、基于Redis实现共享session的登录功能

1. 选择合适的数据结构存入Redis

  • 手机号作为key,String类型的验证码作为value
  • 用户登录时正好会提交手机号,方便通过Redis进行校验验证码

token作为key,Hash类型的用户信息作为value

后端校验成功后,会返回token给前端,前端会将token保存到sessionStorage中(这是浏览器的存储方式),以后前端每次请求都会携带token,方便后端通过Redis校验用户信息

前端代码:将后端返回的token保存到sessionStorage中

前端每次请求时,都会通过拦截器将token设置到请求头中,赋值给变量authorization,后端通过authorization获取前端携带的token进行校验

2. 发送短信验证码

修改之前代码,将验证码存入Redis

@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1.使用工具类校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3.符合,生成验证码
        String code = RandomUtil.randomNumbers(6);

        // 4.保存验证码到 session
//        session.setAttribute("code",code);
        // 4.保存验证码到 redis
        // "login:code:"是业务前缀,以"login:code:" + 手机号为key,过期时间2分钟
        stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);

        // 5.模拟发送验证码
        log.debug("发送短信验证码成功,验证码:{}", code);
        // 返回ok
        return Result.ok();
    }
}

3. 短信验证码登录、注册

  • 修改之前代码,从Redis获取验证码并校验
  • 随机生成token,保存用户信息到redis中,返回token
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1.校验手机号
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 如果不符合,返回错误信息
        return Result.fail("手机号格式错误!");
    }

//        // 2.校验验证码
//        Object cacheCode = session.getAttribute("code");
//        String code = loginForm.getCode();
//        if (cacheCode == null || !cacheCode.toString().equals(code)) {
//            // 3.验证码不一致,则报错
//            return Result.fail("验证码错误");
//        }

    // 2.从Redis获取验证码并校验
    String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
    String code = loginForm.getCode();
    if (cacheCode == null || !cacheCode.equals(code)) {
        // 3.验证码不一致,则报错
        return Result.fail("验证码错误");
    }

    // 4.验证码一致,根据手机号查询用户
    User user = query().eq("phone", phone).one();

    // 5.判断用户是否存在
    if (user == null) {
        // 6.用户不存在,则创建用户并保存
        user = createUserWithPhone(phone);
    }

//        // 7.保存用户信息到session中,UserDTO只包含简单的用户信息,而不是完整的User,这样可以隐藏用户的敏感信息(例如:密码等),还能减少内存使用
//        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));

    // 7.保存用户信息到redis中
    // 7.1随机生成token,作为登录令牌
    // 使用hutool工具中的UUID,true表示不带“-”符号的UUID
    String token = UUID.randomUUID().toString(true);

    // 7.2将User对象转为Hash类型进行存储
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    // 由于使用的是stringRedisTemplate,所以存入的value中的值必须都是String类型的
    // 但是UserDTO中的id是Long类型的,所以进行对象属性拷贝时,需要自定义实现转换规则
     Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));

    // 7.3存入redis, "login:token:"是业务前缀,以 "login:token:" + token作为key
    stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap);
    // 7.4设置token有效期,有效期为30分钟
    stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

    // 8.返回token
    return Result.ok(token);
}

private User createUserWithPhone(String phone) {
    // 1.创建用户
    User user = new User();
    user.setPhone(phone);
    // 随机设置昵称 user_mrkuw05lok
    user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
    // 2.保存用户
    save(user);
    return user;
}

4. 解决token刷新问题

  • token刷新问题是指,用户长时间不进行界面操作时,到了过期时间,token自动失效;但是,用户一旦进行操作,就需要给token续期,即更新token过期时间
  • 为了解决token刷新问题,需要加2个拦截器
  • 第一个拦截器可以拦截所有请求,只要用户有请求就刷新token,并保存用户信息到ThreadLocal中
  • 第二个拦截器只对登录请求进行拦截,从ThreadLocal中获取用户信息进行校验

刷新token的拦截器代码:

public class RefreshTokenInterceptor implements HandlerInterceptor {

    // 因为LoginInterceptor不是通过Spring进行管理的Bean,所以不能再LoginInterceptor中进行注入StringRedisTemplate
    // 可以通过构造方法传入StringRedisTemplate
    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 前置拦截
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        // 1.获取session
//        HttpSession session = request.getSession();
//        // 2.获取session中的用户
//        Object user = session.getAttribute("user");
//        // 3.判断用户是否存在
//        if(user == null){
//            // 4.不存在,拦截,返回401状态码
//            response.setStatus(401);
//            return false;
//        }
//        // 5.存在,保存用户信息到ThreadLocal
//        UserHolder.saveUser((UserDTO)user);
//        // 6.放行
//        return true;

        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            // 不存在,则拦截,返回401状态码
            response.setStatus(401);
            return false;
        }

        // 2.通过token获取redis中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
            .entries(RedisConstants.LOGIN_USER_KEY + token);

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

        // 5.将redis中Hash类型数据转换成UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);

        // 6.用户存在,保存用户信息到ThreadLocal
        UserHolder.saveUser(userDTO);

        // 7.刷新token有效期
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 8.放行
        return true;
    }

    /**
     * 后置拦截器
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
        Object handler, Exception ex) throws Exception {
        // 请求结束后移除用户,防止ThreadLocal造成内存泄漏
        UserHolder.removeUser();
    }
}

登录拦截器的代码:

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.判断是否需要拦截(ThreadLocal中是否有用户)
        if (UserHolder.getUser() == null) {
            // 没有,需要拦截,设置状态码
            response.setStatus(401);
            // 拦截
            return false;
        }
        // 有用户,则放行
        return true;
    }

}
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 添加拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
            // 排除不需要拦截的路径
            .excludePathPatterns(
                "/shop/**",
                "/voucher/**",
                "/shop-type/**",
                "/upload/**",
                "/blog/hot",
                "/user/code",
                "/user/login"
            ).order(1);

        // token刷新的拦截器,order越小,执行优先级越高,所以token刷新的拦截器先执行
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**")
            .excludePathPatterns(
            // RefreshTokenInterceptor拦截器也需要放行"/user/code","/user/login",不然token过期后再重新登录就会一直被拦截
                "/user/code",
                "/user/login")
            .order(0);
    }
}

到此这篇关于Redis实现短信登录的企业实战的文章就介绍到这了,更多相关Redis 短信登录 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 基于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

  • Redis实现短信登录的企业实战

    目录 一.导入黑马点评项目 1. 导入SQL 2. 前后端分离 3. 导入后端项目 4. 导入前端项目 二.基于Session实现登录流程 1. 发送短信验证码 2. 短信验证码登录.注册 3. 登录验证功能 三.集群的session共享问题 四.基于Redis实现共享session的登录功能 1. 选择合适的数据结构存入Redis 2. 发送短信验证码 3. 短信验证码登录.注册 4. 解决token刷新问题 一.导入黑马点评项目 黑马点评项目主要包括以下功能: 这一章主要介绍短信登录功能,短

  • Redis的共享session应用实现短信登录

    目录 1. 基于 session 实现短信登录 1.1 短信登录流程图 1.2 实现发送短信验证码 1.3 实现短信验证码登录.注册 1.4 实现登录校验拦截器 2. 集群的 session 共享问题 3. 基于 Redis 实现共享 session 登录 3.1 Redis 实现共享 session 登录流程图 3.2 实现发送短信验证码 3.3 实现短信验证码登录.注册 3.4 实现登录校验拦截器 1. 基于 session 实现短信登录 1.1 短信登录流程图 1.2 实现发送短信验证码

  • 微信小程序实现短信登录的实战

    目录 1.界面效果预览 2.uView安装 3.uView配置 3.1 main.js中引入 3.2 uni.scss中引入 3.3 App.vue中引入 3.4 pages.json中配置 4.短信登录界面 5.点击获取验证码接口 项目要求增加短信登录及人脸识别登录功能,下面我们来实现下短信登录功能 1.界面效果预览 2.uView安装 uView官网:https://www.uviewui.com 以npm安装为例,执行:npm install uview-ui即可 3.uView配置 3.

  • 如何基于Session实现短信登录功能

    目录 一.基于Session实现登录 1.1 业务流程图 二.发送短信验证码 2.1 发送短信请求方式及参数说明 三.登录功能 3.1  短信验证的请求方式及路径 3.2  业务层代码实现用户登录 3.3 拦截器——登录验证功能 三.隐藏用户敏感信息 四.session共享问题 总结 一.基于Session实现登录 1.1 业务流程图 二.发送短信验证码 2.1 发送短信请求方式及参数说明 这个地方为什么需要session?  因为我们需要把验证码保存在session当中 /** * 发送手机验

  • 小程序实现短信登录倒计时

    在平时的短信登录中,当发送短信验证码后会显示倒计时,那么这个倒计时如何实现呢? wxml文件 <view class='Form'> <form bindsubmit="formSubmit" bindreset="formReset" class='forms'> <view class="fidpas"> <input type="number" class="phone

  • javaweb项目如何实现手机短信登录

    手机号登录在现在的项目中用的场景非常多,实现起来也不难,今天我们就一起来通过演示实现登录过程.   一.首先需要注册个第三方的账户,比如秒嘀科技等,然后拿到三个参数值:QUERAY_PATH   ACCOUNT_SID       AUTH_TOKEN 二.编写获取验证码类getMessage.java private static final String QUERAY_PATH="xxxx"; private static final String ACCOUNT_SID=&quo

  • mpvue网易云短信接口实现小程序短信登录的示例代码

    上一篇简单介绍了mpvue实现快递单号查询,慢慢发现mpvue真的和vue很像,但它有几乎十分的吻合小程序的语法规范,刚开始用起来会觉得特点的爽,但涉及到细节却是有很多采坑的地方.今天利用网上的网易云接口,再结合mpvue简单写一写小程序短信验证登录. 简单封装的一个网络请求文件,网易云接口网上大佬们GitHub上还是比较的多而且开源 const baseURL = "https://*****:1717"; //基路径 exports.http = function({url,met

  • Spring Security 自定义短信登录认证的实现

    自定义登录filter 上篇文章我们说到,对于用户的登录,security通过定义一个filter拦截login路径来实现的,所以我们要实现自定义登录,需要自己定义一个filter,继承AbstractAuthenticationProcessingFilter,从request中提取到手机号和验证码,然后提交给AuthenticationManager: public class SmsAuthenticationFilter extends AbstractAuthenticationPro

随机推荐