目录
IDEA集成git
传统session存在的问题
redis方案
业务流程
选用的数据结构
整体访问流程
发送短信验证码
获取校验验证码
配置登录拦截器
拦截器注册配置类
拦截器
用户状态刷新问题
刷新问题解决方案
IDEA集成git
远程仓库采用码云,创建好仓库,复制仓库的url
在idea中点击,出现git选项,点击ok
之后右击项目,点击remotes
填写url即可集成git
传统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中取从而增加复杂度,最后放行。
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分钟之后登录就会退出,这明显不符合我们的常识
刷新问题解决方案
我们可以再加一个拦截器,形成一个拦截器链,第一个拦截器对所有路径进行拦截,而它的功能就是刷新登录状态,登录认证则由第二个拦截器完成
刷新状态拦截器
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); }}