Redis代替session
- redis中设计key
- 在使用session时,每个用户都会有自己的session,这样虽然验证码的键都是"code",但是相互不影响,从而确保每个用户获取到的验证码只能够自己使用,当使用redis时,redis的key是共享的,不分用户,就要求在redis中存储验证码时,不能直接将验证码的键设置为"code",这样无法保证其唯一性。
- redis中设计value
- 到底该使用redis中什么数据类型存储数据,主要需要看数据样式和使用方式,一般会考虑使用String、Hash,String存储时,会多占一点内存空间,则相对来说Hash存储时,会少占用一点内存空间。
- String结构:以Json字符串来存储,比较直观
- Hash结构:,每个对象中每个字段独立存储,可以针对单个字段做CRUD
- 到底该使用redis中什么数据类型存储数据,主要需要看数据样式和使用方式,一般会考虑使用String、Hash,String存储时,会多占一点内存空间,则相对来说Hash存储时,会少占用一点内存空间。
redis实现登录
发送验证码
-
添加redis
pom<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
设置redis的连接信息
yamlspring: redis: host: 192.168.175.128 port: 6379 password: liang lettuce: pool: max-active: 10 max-idle: 10 min-idle: 1 time-between-eviction-runs: 10s
-
增加相关常量
java/** * 保存验证码的redis中的key */ public static final String LOGIN_CODE_KEY = "login:code:"; /** * 验证码的过期时间 */ public static final Long LOGIN_CODE_TTL = 2L;
-
修改Service层
java@Autowired StringRedisTemplate stringRedisTemplate; @Override public boolean sendCode(String phone, HttpSession session) { //获取手机号,验证手机号是否合规 boolean mobile = PhoneUtil.isMobile(phone); //不合规,则提示 if (!mobile){ return false; } //生成验证码 String code = RandomUtil.randomNumbers(6); //保存验证码到redis,并设置过期时间 stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES); //发送验证码,这里就通过打印验证码模拟了下发送验证码 System.out.println("验证码:" + code); return true; }
-
修改Controller层
java@PostMapping("code") public Result sendCode(@RequestParam("phone") String phone, HttpSession session) { String uuid = userService.sendCode(phone, session); return uuid.equals("") ? Result.fail("手机号码不合规"): Result.ok(uuid); }
验证码登录、注册
-
增加相关常量
javapublic static final String LOGIN_USER_KEY = "login:token:"; public static final Long LOGIN_USER_TTL = 30L;
-
修改Service层
java@Override public String login(LoginFormDTO loginForm, HttpSession session) { //获取手机号 String phone = loginForm.getPhone(); //验证手机号是否合理 boolean mobile = PhoneUtil.isMobile(phone); //如果不合理 提示 if (!mobile){ //提示用户手机号不合理 return ""; } //手机号合理 进行验证码验证 String code = loginForm.getCode(); //从redis中获取验证码 String redisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone); //如果验证码输入的是错误的 提示 if (!code.equals(redisCode)){ return ""; } //如果验证码也正确 那么通过手机号进行查询 User user = this.getOne(new LambdaQueryWrapper<User>().eq(User::getPhone, phone)); // 数据库中没查询到用户信息 if (ObjectUtil.isNull(user)){ user = new User(); user.setPhone(phone); user.setNickName("user_"+ RandomUtil.randomString(10)); this.save(user); } // 将用户信息保存到Redis中,注意避免保存用户敏感信息 UserDTO userDTO = BeanUtil.toBean(user, UserDTO.class); // 设置UUID保存用户信息 String uuid = IdUtil.fastSimpleUUID(); // 将user对象转化为Map,同时将Map中的值存储为String类型的 Map<String, Object> userDTOMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create().ignoreNullValue() .setFieldValueEditor((key, value) -> value.toString())); stringRedisTemplate.opsForHash().putAll( LOGIN_USER_KEY + uuid, userDTOMap); //设置过期时间 stringRedisTemplate.expire(LOGIN_USER_KEY + uuid, LOGIN_USER_TTL, TimeUnit.MINUTES); // 通过UUID生成简单的token String token = uuid + userDTO.getId(); return token; }
javaString login(LoginFormDTO loginForm, HttpSession session);
-
修改Controller层
java@PostMapping("/login") public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){ String token = userService.login(loginForm, session); return StrUtil.isNotBlank(token) ? Result.ok(token) : Result.fail("手机号或验证码错误"); }
校验登录状态
-
修改LoginInterceptor拦截器
javaprivate StringRedisTemplate stringRedisTemplate; /** * 构造函数 * @param stringRedisTemplate */ public LoginInterceptor(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } /** * preHandle方法的返回值决定是否放行,该方法在控制层方法执行前执行 * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //从请求头中获取token String token = request.getHeader("authorization"); if (StrUtil.isBlank(token)){ return false; } String uuid = token.substring(0,token.lastIndexOf("-")); System.out.println(uuid); //从redis中获取值 Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + uuid); if (ObjectUtil.isNull(entries)){ return false; } //将map转化为UserDTO对象 UserDTO userDTO = BeanUtil.fillBeanWithMap(entries, new UserDTO(), true); //将用户信息保存到 ThreadLocal UserHolder.saveUser(userDTO); return true; }
java@Configuration public class MvcConfig implements WebMvcConfigurer { @Resource StringRedisTemplate stringRedisTemplate; /** * 添加拦截器 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { //添加拦截器 registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)) //放行资源 .excludePathPatterns( "/shop/**", "/voucher/**", "/shop-type/**", "/upload/**", "/blog/hot", "/user/code", "/user/login" ) // 设置拦截器优先级 .order(1); } }
登录状态的刷新问题
-
因为设置了redis中存储的用户的有效期,所以在用户访问界面的时,需要更新token令牌的存活时间,例如修改LoginInterceptor拦截器,在此拦截器中刷新过期时间
java@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //从请求头中获取token String token = request.getHeader("authorization"); if (StrUtil.isBlank(token)){ return false; } String uuid = token.substring(0,token.lastIndexOf("-")); System.out.println(uuid); //从redis中获取值 Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + uuid); if (ObjectUtil.isNull(entries)){ return false; } //将map转化为UserDTO对象 UserDTO userDTO = BeanUtil.fillBeanWithMap(entries, new UserDTO(), true); //将用户信息保存到 ThreadLocal UserHolder.saveUser(userDTO); //刷新token有效期 stringRedisTemplate.expire(LOGIN_USER_KEY + uuid, LOGIN_USER_TTL, TimeUnit.MINUTES); return true; }
-
但是需要注意的是,自定义的登录拦截器只是针对需要登录访问的请求进行了拦截,如果用户访问没被拦截的请求,该拦截器不会生效,则token令牌不能进行更新,当用户长时间访问不需要登录的页面,token令牌失效,再去访问被拦截的请求,则需要重新登录,这是不合理的。所有我们还需要在定义一个拦截器,进行token令牌刷新。
-
刷新令牌的Interceptor
java/** * 刷新令牌的拦截器 * @author liang */ public class RefreshTokenInterceptor implements HandlerInterceptor { private StringRedisTemplate redisTemplate; public RefreshTokenInterceptor(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //从请求头中获取token String token = request.getHeader("authorization"); if (StrUtil.isBlank(token)){ return false; } String uuid = token.substring(0, token.lastIndexOf("-")); //从Redis中获取值 Map<Object, Object> userMap = redisTemplate.opsForHash().entries(LOGIN_USER_KEY + uuid); if (ObjectUtil.isNull(userMap)){ return false; } //将map转换为UserDTO对象 UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); //将用户信息保存到 ThreadLocal UserHolder.saveUser(userDTO); //刷新token有效期 redisTemplate.expire(LOGIN_USER_KEY + uuid, LOGIN_USER_TTL, TimeUnit.MINUTES); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserHolder.removeUser(); } }
-
修改登录的Interceptor
javapublic class LoginInterceptor implements HandlerInterceptor { /** * preHandle方法的返回值决定是否放行,该方法在控制层方法执行前执行 * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { UserDTO user = UserHolder.getUser(); return ObjectUtil.isNotNull(user); } }
-
修改WebMvcConfigurer配置类
java@Configuration public class MvcConfig implements WebMvcConfigurer { @Resource StringRedisTemplate stringRedisTemplate; /** * 添加拦截器 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { //添加拦截器 registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)); registry.addInterceptor(new LoginInterceptor()) //放行资源 .excludePathPatterns( "/shop/**", "/voucher/**", "/shop-type/**", "/upload/**", "/blog/hot", "/user/code", "/user/login" ) // 设置拦截器优先级 .order(1); } }
本文由mdnice多平台发布