redis实战-项目集成git及redis实现短信验证码登录

开源 0

 

目录

 

 

IDEA集成git

传统session存在的问题 

redis方案

业务流程

选用的数据结构

整体访问流程

发送短信验证码

 获取校验验证码

配置登录拦截器

拦截器注册配置类

拦截器

用户状态刷新问题

刷新问题解决方案


 

IDEA集成git

远程仓库采用码云,创建好仓库,复制仓库的url

2b81c37fc98443f18a1d0fb2bc188eab.png

 

 在idea中点击,出现git选项,点击ok

f7b350cd46a64df3ae76751c89607036.png

ce290b018ee94e3f8e315fbd3c00e387.png

 之后右击项目,点击remotes654dd9e6be9541b8a4db410eeab3d6d8.png

 填写url即可集成gitee31465e142b4a249aea4bd7fc51d503.png

传统session存在的问题 

传统的登录认证会采用session进行登录认证,将登录的验证码,用户信息都存放到session中,我们通过session来进行操作数据,这有什么问题呢

每个tomcat服务器中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,即session在各个服务器之间是不共通的,所以此时整个登录拦截功能就会出现问题,而redis数据本身就是共享的,就可以避免session共享的问题了

redis方案

业务流程

  • 将验证码存储到redis中
  • 将用户数据存储到redis中

选用的数据结构

存储验证码时采用String类型,key采用业务代码+手机号

存储user数据时采用hash,key采用业务代码+随机的tonke

hash可以将对象中的每个字段独立存储,可以针对单个字段做CRUD,并且内存占用更少,实际上也可以采用String类型,但hash类型消耗内存较少,故选用String类型

整体访问流程

 当注册完成后,用户去登录会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到redis,并且生成token作为redis的key,当我们校验用户是否登录时,会去携带着token进行访问,从redis中取出token对应的value,判断是否存在这个数据,如果没有则拦截,如果存在则将其保存到threadLocal中,简化后续业务获取用户信息的代码,后续业务需要登录用户的信息,只要在threadLocal中取即可,无需从redis中取从而增加复杂度,最后放行。

66fa57b113e7443a80b588a092f4d75c.png

redis业务代码常量(后续继续补充)

public class RedisConstants {    //发送验证码业务标识    public static final String LOGIN_CODE_KEY = "login:code:";    public static final Long LOGIN_CODE_TTL = 2L;    //用户登录业务标识    public static final String LOGIN_USER_KEY = "login:token:";    public static final Long LOGIN_USER_TTL = 30L;   }

发送短信验证码

采用日志打印形式,将验证码打印在控制台

 public Result sendCode(String phone) {        //校验手机号        if (RegexUtils.isPhoneInvalid(phone)) {            return Result.fail("手机号码格式不正确");        }        //手机号格式正确生成验证码        String code = RandomUtil.randomNumbers(6);        //保存到redis        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);        //打印日志        log.debug("手机验证码为:" + code);        return Result.ok();    }

 获取校验验证码

我们需要对用户敏感信息进行筛选,只给前端返回必要的用户信息数据,需要封装dto对象,同时在uuid生成token,作为redis取数据的key,最后要设置过期时间,防止reids内存爆炸,设置为30分钟,因为session的过期时间也是30分钟

 public Result login(LoginFormDTO loginForm) {        String phone = loginForm.getPhone();        String code = loginForm.getCode();        //校验手机号        if (RegexUtils.isPhoneInvalid(phone)) {            return Result.fail("手机号码格式不正确");        }        //从redis取验证码,进行比对        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);        if (cacheCode == null || !cacheCode.equals(code)) {            return Result.fail("验证码错误");        }        //根据电话号码从数据库查询用户是否存在        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();        queryWrapper.eq(User::getPhone, phone);        User user = userMapper.selectOne(queryWrapper);        //不存在则创建        if (user == null) {            user = createUserWithPhone(phone);        }        String token = UUID.randomUUID().toString(true);        //只返回不敏感的信息,使用dto进行封装        UserDTO userDTO = new UserDTO();        BeanUtils.copyProperties(user, userDTO);        //将dto对象转化为map结构        Map<String, String> userMap = new HashMap<>();        userMap.put("id", userDTO.getId().toString());        userMap.put("icon", userDTO.getIcon());        userMap.put("nickName", userDTO.getNickName());//业务代码+token形成redis中的key        String tokenKey = LOGIN_USER_KEY + token;        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);        return Result.ok(token);    }

配置登录拦截器

拦截器注册配置类

@Configurationpublic class MvcConfig implements WebMvcConfigurer {  @Autowired    private StringRedisTemplate stringRedisTemplate;    @Override    public void addInterceptors(InterceptorRegistry registry) {        // 登录拦截器        registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))                .excludePathPatterns(                        "/shop/**",                        "/voucher/**",                        "/shop-type/**",                        "/upload/**",                        "/blog/hot",                        "/user/code",                        "/user/login"                );    }}

拦截器

拦截器需要的stringRedisTemplate对象不能通过@Autowired直接注入

因为LoginInterceptor 对象是我们手动创建的,不受spring管控,不在spring容器中,故不能注入spring容器中的bean,只能通过在配置类中通过构造方法注入stringRedisTemplate对象

public class LoginInterceptor implements HandlerInterceptor {    private StringRedisTemplate stringRedisTemplate;    public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {        this.stringRedisTemplate = stringRedisTemplate;    }    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        // 1.获取请求头中的token        String token = request.getHeader("authorization");        if (StrUtil.isBlank(token)) {            response.setStatus(401);            return false;        }        String tokenKey = LOGIN_USER_KEY + token;        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);        //3.判断用户是否存在        if (userMap.isEmpty()) {            //4.不存在,拦截,返回401状态码            response.setStatus(401);            return false;        }        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);        //5.存在,保存用户信息到Threadlocal        UserHolder.saveUser(userDTO);        //刷新登录状态        stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);        //6.放行        return true;    }}

用户状态刷新问题

那就是登录认证和刷新用户状态绑定在一个拦截器中,而需要登录认证的路径并不是全部路径,如果用户不访问需要登录认证的路径,那就刷新不了用户状态,即30分钟之后登录就会退出,这明显不符合我们的常识

刷新问题解决方案

我们可以再加一个拦截器,形成一个拦截器链,第一个拦截器对所有路径进行拦截,而它的功能就是刷新登录状态,登录认证则由第二个拦截器完成

 

2bad35b884d24d54ab1d2d27bfd97849.png

 

 刷新状态拦截器

public class RefreshTokenInterceptor implements HandlerInterceptor {    private StringRedisTemplate stringRedisTemplate;    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {        this.stringRedisTemplate = stringRedisTemplate;    }    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        // 1.获取请求头中的token        String token = request.getHeader("authorization");        if (StrUtil.isBlank(token)) {            return true;        }        // 2.基于TOKEN获取redis中的用户        String key  = LOGIN_USER_KEY + token;        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);        // 3.判断用户是否存在        if (userMap.isEmpty()) {            return true;        }        // 5.将查询到的hash数据转为UserDTO        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);        // 6.存在,保存用户信息到 ThreadLocal        UserHolder.saveUser(userDTO);        // 7.刷新token有效期        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);        // 8.放行        return true;    }    @Override    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {        // 移除用户        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;    }}

为了保证拦截器的执行先后顺序,配置类需设置order的大小,设定拦截器执行的先后的顺序,如若不设置,就按照注册顺序的先后来执行

@Configurationpublic class MvcConfig implements WebMvcConfigurer {    @Resource    private StringRedisTemplate stringRedisTemplate;    @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刷新的拦截器        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);    }}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

也许您对下面的内容还感兴趣: