Session、Redis实现登录,附有超简单的redis登陆方式

136 阅读5分钟

一 、基于Session实现登录

1.1流程图

image-20240520133058474

1.2获取验证码

 @Override
 public Result sendCode(String phone, HttpSession session) {
     if(phone == null){
         return Result.fail("请输入手机号");
     }
     //1.校验手机号
     if (RegexUtils.isPhoneInvalid(phone)) {
         //2.如果不符合,返回错误信息
         return Result.fail("手机号格式错误");
     }
     //3.生成验证码
     String code = RandomUtil.randomNumbers(6);
     //4.保存验证码到session
     session.setAttribute("code",code);
     //5.发送验证码 模拟发送
     log.info(code);
     return Result.ok();
 }

1.3登录校验

 @Override
 public Result login(LoginFormDTO loginForm, HttpSession session) {
     if(loginForm.getCode() == null){
         return Result.fail("请输入验证码");
     }
     if(loginForm.getPhone() == null){
         return Result.fail("请输入手机号");
     }
     //1.验证手机号和校验码
     if (RegexUtils.isPhoneInvalid(loginForm.getPhone())) {
 ​
         return Result.fail("手机号格式错误");
     }
     String cacheCode = (String) session.getAttribute("code");
     if (!cacheCode.equals(loginForm.getCode())) {
         return Result.fail("验证码错误");
     }
     //2.查询用户是否存在
     User user = query().eq("phone", loginForm.getPhone()).one();
     //3.不存在,创建新用户并保存
     if (user == null){
         user = createUserWithPhone(loginForm.getPhone());
     }
     //4.存在,保存用户信息到session
     session.setAttribute("user",user);
     return Result.ok();
 }

1.4用户状态处理

登录拦截器

 public class LoginInterceptor implements HandlerInterceptor {
 ​
     @Override
     public boolean preHandle(javax.servlet.http.HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
         //1.获取用户
         Object user = request.getSession().getAttribute("user");
         //2.判断用户是否存在
         if (user == null) {
             //3.不存在拦截
             response.setStatus(401);
             return false;
         }
         //4.保存用户到UserHolder
         UserHolder.saveUser(BeanUtil.copyProperties(user, UserDTO.class));
         //5.放行
         return true;
     }
 ​
     @Override
     public void afterCompletion(javax.servlet.http.HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
         //移除用户
         UserHolder.removeUser();
     }
 ​
 }

配置登录拦截器

 @Configuration
 public class MvcConfig implements WebMvcConfigurer {
 ​
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
         //登录拦截器
         registry.addInterceptor(new LoginInterceptor())
                 .excludePathPatterns(
                        //配置不会拦截的路径 例如
                         "/user/login"
                 ).order(1);
     }
 }

1.5集群的session共享问题

session共享问题:多台Tomcat并不共享session存储空间,当请求到不同tomcat服务器时导致数据丢失的问题

如果session做数据拷贝,则内存消耗太大;

解决方案:Redis

二、基于Redis实现共享session登录

1.1流程图

image-20240520145534751

1.2存储验证码

 @Override
 public Result sendCode(String phone, HttpSession session) {
     if(phone == null){
         return Result.fail("请输入手机号");
     }
     //1.校验手机号
     if (RegexUtils.isPhoneInvalid(phone)) {
         //2.如果不符合,返回错误信息
         return Result.fail("手机号格式错误");
     }
     //3.生成验证码
     String code = RandomUtil.randomNumbers(6);
     //4.保存验证码到redis
     stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
     //5.发送验证码 模拟发送
     log.debug("发送验证码成功,验证码:{}",code);
     return Result.ok();
 }
 ​

1.3登录校验

 @Override
 public Result login(LoginFormDTO loginForm, HttpSession session) {
     if(loginForm.getCode() == null){
         return Result.fail("请输入验证码");
     }
     if(loginForm.getPhone() == null){
         return Result.fail("请输入手机号");
     }
     //1.验证手机号和校验码
     if (RegexUtils.isPhoneInvalid(loginForm.getPhone())) {
         return Result.fail("手机号格式错误");
     }
     String redisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + loginForm.getPhone());
     if (redisCode == null || !redisCode.equals(loginForm.getCode())) {
         return Result.fail("验证码错误");
     }
     //2.查询用户是否存在
     User user = query().eq("phone", loginForm.getPhone()).one();
     //3.不存在,创建新用户并保存
     if (user == null){
         user = createUserWithPhone(loginForm.getPhone());
     }
     //4.存在,保存用户信息到Redis
     //4.1随机生成token
     String token = UUID.randomUUID().toString(true);
     //4.2将User对象转换成Hash存储
     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())
                                                     );
     String tokenKey = LOGIN_USER_KEY + token;
     stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
     //4.3设置token有效期
     stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
     //5.返回token
     return Result.ok(token);
 }
 ​

1.4拦截器

LoginInterceptor:

 public class LoginInterceptor implements HandlerInterceptor {
     
     private StringRedisTemplate stringRedisTemplate;
     
     public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
         this.stringRedisTemplate = stringRedisTemplate;
     }
 ​
     @Override
     public boolean preHandle(javax.servlet.http.HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
         //1.获取token
         String token = request.getHeader("authorization");
         if(StrUtil.isBlank(token)){
             //不存在拦截
             response.setStatus(401);
             return false;
         }
         //2.基于token查询用户是否存在
         String key = LOGIN_USER_KEY + token;
         Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
         //3.判断用户是否存在
         if (userMap.isEmpty()) {
             //4.不存在拦截
             response.setStatus(401);
             return false;
         }
         //5.转为UserDTO对象
         UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
         //6.保存用户到UserHolder
         UserHolder.saveUser(BeanUtil.copyProperties(user, UserDTO.class));
         //7.刷新token有效期
         stringRedisTemplate.expire(key,LOGIN_USER_TTL, TimeUnit.MINUTES);
         //8.放行
         return true;
     }
 ​
     @Override
     public void afterCompletion(javax.servlet.http.HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
         //移除用户
         UserHolder.removeUser();
     }
 ​
 }
 ​

MvcConfig

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

1.5解决状态登录刷新问题

在这个方案中,他确实可以使用对应路径的拦截,同时刷新登录token令牌的存活时间,但是现在这个拦截器他只是拦截需要被拦截的路径,假设当前用户访问了一些不需要拦截的路径,那么这个拦截器就不会生效,所以此时令牌刷新的动作实际上就不会执行,所以这个方案他是存在问题的

image-20240520165642070

既然之前的拦截器无法对不需要拦截的路径生效,那么我们可以添加一个拦截器,在第一个拦截器中拦截所有的路径,把第二个拦截器做的事情放入到第一个拦截器中,同时刷新令牌,因为第一个拦截器有了threadLocal的数据,所以此时第二个拦截器只需要判断拦截器中的user对象是否存在即可,完成整体刷新功能。

image-20240520165656557

RefreshTokenInterceptor

 public class RefreshTokenInterceptor implements HandlerInterceptor {
     
     private StringRedisTemplate stringRedisTemplate;
     
     public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
         this.stringRedisTemplate = stringRedisTemplate;
     }
 ​
     @Override
     public boolean preHandle(javax.servlet.http.HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
         //1.获取token
         String token = request.getHeader("authorization");
         if(StrUtil.isBlank(token)){
             //不存在拦截
             response.setStatus(401);
             return false;
         }
         //2.基于token查询用户是否存在
         String key = LOGIN_USER_KEY + token;
         Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
         //3.判断用户是否存在
         if (userMap.isEmpty()) {
             //4.不存在拦截
             response.setStatus(401);
             return false;
         }
         //5.转为UserDTO对象
         UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
         //6.保存用户到UserHolder
         UserHolder.saveUser(BeanUtil.copyProperties(user, UserDTO.class));
         //7.刷新token有效期
         stringRedisTemplate.expire(key,LOGIN_USER_TTL, TimeUnit.MINUTES);
         //8.放行
         return true;
     }
 ​
     @Override
     public void afterCompletion(javax.servlet.http.HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
         //移除用户
         UserHolder.removeUser();
     }
 ​
 }
 ​

LoginInterceptor

 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;
         }
         //2.由用户,放行
         return true;
     }
 ​
 }

MvcConfig

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

三、 使用配置文件实现session的存储到Redis(超简单)

1. 引入依赖

redis-starter:可以操作redis(注意:这里的版本引入与 SpringBoot 相同的版本。)

spring-session-redis:可以将session 自动存储到redis中

 
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
     <version>2.6.4</version>
 </dependency>
 <dependency>
     <groupId>org.springframework.session</groupId>
     <artifactId>spring-session-data-redis</artifactId>
     <version>2.6.3</version>
 </dependency>

2.配置redis

 spring:
   redis:
     host: 127.0.0.1
     port: 6379
     password: xxxxx #没有则不需要配置
     database: 1
     lettuce:
       pool:
         max-active: 10
         max-idle: 10
         min-idle: 1
         time-between-eviction-runs: 10s

3.修改spring-session存储配置

 spring:
   session:
    #失效时间
     timeout: 86400
     store-type: redis

配置完成之后,就可以自动将session存储到数据库中,并且还有原来request的方式

4.使用方式

 // 存储用户信息到 session
 request.getSession().setAttribute("user", user);
  
 // 在 session 中读取用户信息
 request.getSession().getAttribute("user");
 ​

超级简单而且还解决了session单机存储信息的问题