Redis企业实战—基于Redis短信验证功能
文章目录
- Redis企业实战---基于Redis短信验证功能
- 一、短信登录实现
- 1.1、导入[黑马点评项目](https://pan.baidu.com/s/1189u6u4icQYHg_9_7ovWmA?pwd=eh11)
- 1.2、基于Session实现
- 1.3、集群的session的共享问题
- 1.4、基于Redis实现共享session登录
 
Redis可以进行如下操作

一、短信登录实现
1.1、导入黑马点评项目
表的基本信息
 
 前后端分离
 
 导入后端项目
 
 导入前端项目

 运行前端项目

1.2、基于Session实现
实现流程
 
 发送验证码
 
 发送验证码
主要进行如下操作:
- 对手机号进行校验
- 模拟生成一个验证码并保存在session中
 @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone))
        {
            // 2.不符合就返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3. 符合,生成验证码--一般长度为6位  用一个随机数默认向手机号发送短信
        String code = RandomUtil.randomNumbers(6);
        // 4.将验证码保存到session中
        session.setAttribute("code",code);
        // 5.发送验证码--aliyun的第三方库
        log.debug("验证码发送成功,验证码:"+code);
        // 5.返回ok
        return Result.ok();
    }
登录
主要进行如下操作:
- 通过数据绑定获取到前端的表单数据
- 对手机号进行校验
- 对验证码进行检验看是否和存在session中的验证码一致
- 按手机号对user表进行查询
- 没有当前用户,就创建为新用户并保存到session中
 @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        String phone = loginForm.getPhone();
        // 1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 不符合就返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 2.继手机号之后,校验验证码
        String code = loginForm.getCode();
        String cacheCode = (String) session.getAttribute("code");
        if (cacheCode == null || RegexUtils.isCodeInvalid(code) || cacheCode == code) {
            // 3.不一致,报错
            return Result.fail("验证码错误!");
        }
        // 4.一致,根据手机号查询用户
        User user = query().eq("phone", phone).one();
        // 5.判断用户是否存在
        if (user == null){
            // 6.不存在,创建新用户并保存
             user = createUserWithPhone(phone);
        }
        session.setAttribute("user",user);
        //  每一个session都有一个SessionID
        return Result.ok();
    }
    private User createUserWithPhone(String phone) {
        // 1. 创建用户
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
        // 2.保存用户
        save(user);
        return user;
    }
登录验证功能
 
定义拦截器
主要进行如下操作:
- 获取session中的用户
- 判断用户是否存在,
- 存在,保存用户信息到 ThreadLocal
- 放行
public class LoginInterceptor implements HandlerInterceptor {
    // 1. 前置拦截器
    @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;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}
配置拦截器
主要进行如下操作:
- 将定义的LoginInterceptor登录拦截器添加进去
- 并排除一些不需要拦截的请求
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    // 将定义的LoginInterceptor登录拦截器添加进去
  	// 并排除一些不需要拦截的请求
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/shop-type/**",
                        "/voucher/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
    }
}
1.3、集群的session的共享问题
session实现共享所面临的问题:
 
 Redis代替session需要考虑的问题
- 选择合适的数据结构
- 选择合适的key
- 选择合适粒度
短信验证和登录注册
 
 验证码发送
主要进行如下操作:
- 检验手机号是否合法
- 生成一个模拟验证码,并将验证码保存到redis中
- 给保证验证码的key设置有效期
  @Autowired
    private StringRedisTemplate stringRedisTemplate;
    //引入ObjectMapperJSON处理类
    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone))
        {
            // 2.不符合就返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3. 符合,生成验证码--一般长度为6位  用一个随机数默认向手机号发送短信
        String code = RandomUtil.randomNumbers(6);
        // 4.将验证码保存到Redis中 --- 一般加个前缀即能保证唯一性,又能指明当前key-value的功能
        // 5.设置验证码的key的有效期  LOGIN_CODE_KEY:前缀  LOGIN_CODE_TTL:有效期时间
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
        // 6.发送验证码--aliyun的第三方库
        log.debug("验证码发送成功,验证码:"+code);
        // 7.返回ok
        return Result.ok();
    }
校验登录状态
 
 登录凭证token的存储方式
 
 登录代码
主要操作分为以下几点:
- 获取表单的数据,进行手机号的验证
- 从redis中读取保存的验证码(前缀+手机号为key),并判断是否正确
- 当数据符合要求时,利用手机号进行查询user表
- 当没有数据的时候,进行创建新用户并保存
- 保存用户信息到redis 
  - 用随机token 作为登录令牌
- 将Java对象转换为Hash进行存储
- 设置token有效期
 
@Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        String phone = loginForm.getPhone();
        // 1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 不符合就返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // TODO 2.从redis获取到验证码并进行验证
        String code = loginForm.getCode();
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);
        if (cacheCode == null || RegexUtils.isCodeInvalid(code) || cacheCode == code) {
            // 3.不一致,报错
            return Result.fail("验证码错误!");
        }
        // 4.一致,根据手机号查询用户
        User user = query().eq("phone", phone).one();
        // 5.判断用户是否存在
        if (user == null){
            // 6.不存在,创建新用户并保存
             user = createUserWithPhone(phone);
        }
        // TODO 7. 保存用户信息到redis,问题进行保存的只能是用户的部分信息,不能包括完整信息,用户的敏感数据必须被隐藏。UserDTD类只包含数据的部分信息
        // 7.1、用随机token 作为登录令牌
        String token = UUID.randomUUID().toString(true);
        //  7.2、将Java对象转换为Hash进行存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> map = BeanUtil.beanToMap(userDTO);
        String key = LOGIN_USER_KEY+token;
        map.put("id",userDTO.getId().toString());
        stringRedisTemplate.opsForHash().putAll(key,map);
        //  7.3、设置token有效期 30分钟 CACHE_SHOP_TTL常量
        stringRedisTemplate.expire(key,CACHE_SHOP_TTL,TimeUnit.MINUTES);
        return Result.ok(token);
    }
拦截器配置
主要获得请求头携带的token(登录动作的返回值,在前端作为数据存储在请求头中),方便后续的操作。主要操作:
- 创建构造函数,方便MvcConfig 注入StringRedisTemplate对象
- 获取请求头中的token
- 基于token获取reids中的用户
- 判断用户是否为空
- 将查询到的hash数据转为UserDTD对象
- 存在,保存用户信息到 ThreadLocal
- 刷新token的有效期
- 放行
public class LoginInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;
    public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    // 1. 前置拦截器
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)){
            //  不存在,拦截,返回401代码
            response.setStatus(401);
            return false;
        }
        // 2. 基于token获取reids中的用户
        Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
        // 3. 判断是否是空值
        if (map.isEmpty()){
            // 4. 不存在,拦截,返回401代码
            response.setStatus(401);
            return false;
        }
        // 5. 将查询到的hash数据转为UserDTD对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(map, new UserDTO(), false);
        // 6. 存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7. 刷新token的有效期
        String key = LOGIN_USER_KEY + token;
        stringRedisTemplate.expire(key,30, TimeUnit.MINUTES);
        // 8. 放行
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}
注入拦截器
将定义的LoginInterceptor登录拦截器注入到spring中,需要进行以下操作:
- 排序一些不需要拦截的请求
- 注入StringRedisTemplate 对象
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 排除一些不需要拦截的请求
        registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
                .excludePathPatterns(
                        "/shop/**",
                        "/shop-type/**",
                        "/voucher/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
    }
}

















![[论文分享] How to Better Utilize Code Graphs in Semantic Code Search?](https://img-blog.csdnimg.cn/26009fcef45c4eda8540f56f243ec6ed.png)

