一、为什么进行登录验证

进行登录验证的作用,是为了保护数据,在用户没有进行登录时有些数据和操作是不能被授权使用的。例如,购物商城里的评论,未登录时可以看评论但是不能写评论。也不可以买东西。这就要求我们进行每一个请求的登录验证了。

二、实现思路

实现登录验证我知道的方法有两种,第一种是存入session中,但是session只能保证一次会话有效,就是说在同一个Tomcat中有效,但是我们为了减少服务器的压力,通常会配置一个tomcat的集群,这时一个tomcat中session可以在另一个tomcat中被查询到吗?肯定是不行的。
所以,有了第二种方法,存入redis中。每个tomcat服务都可以通过redis进行校验。具体过程如下(前面还有验证码校验省略了,道理一样):

1.首先进行用户登录,先在数据库中查,如果存在用户则将用户信息存入redis中,并设置有效时间:
//                   修改: 将信息存入redis中,并随机生成token登录令牌,将User对象转为hash格式存储
//                    随机生成UUID作为token
                    String token = UUID.randomUUID().toString(true);
                    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);

//                    将userDTO转成hashMap,userDTO中的Long类型不能转化为String,所以需要我们自己进行转化
                    Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                            CopyOptions.create()
                                    .setIgnoreNullValue(true)
//                                    将map中的值都变成了字符串类型
                                    .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));

                    HashOperations<String, Object, Object> stringObjectObjectHashOperations = stringRedisTemplate.opsForHash();
                    stringObjectObjectHashOperations.putAll(LOGIN_USER_KEY + token, userMap);
//                    设置有效时间,问题:该方式说明无论你是否操作一但过了30分钟,就会被认定为未登录,所以我们应该在拦截器中设置每次操作更新token的存活时间
                    stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);
//                    返回token给浏览器
                    return Result.ok(token);
2.如果在数据库中没有说明第一次登录,则直接进行注册
 private User createUserWithPhone(String phone) {
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(6));
        this.save(user);
        return user;
    }
三、出现的问题

1.设置了有效时间为30分钟实现,但是这样写表示无论怎样操作我们用户登录验证的有效时间只有30分钟,时间一过我们就会被强制登出。
2.如何实现对请求的登录验证呢?

四、解决问题
问题1
      每次请求操作我们都更新一次redis中用户信息的存活时间
//            新增刷新token的有效期
        stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);
问题2:
     我们可以自定义拦截器,进行路径的拦截,将自定义拦截器,加入到springMVC的默认拦截器中,并且可以通过order设置使用顺序,数值越小越先使用:
@Configuration
public class SpringMVCConfig implements WebMvcConfigurer {
    @Autowired
    StringRedisTemplate redisTemplate;
    /**
     * @Description 添加自定义的拦截器,并排除不需要拦截的请求
     * @Retrurn
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
//       TODO: 暂时将upload放行,方便测试
//        登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**"
                ).order(1);
//        token刷新拦截器
        registry.addInterceptor(new RefreshtokenInterceptor(redisTemplate)).order(0);
    }
}

五、对两个拦截器的说明

一、LoginInterceptor
主要用来进行登录验证的拦截

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        只用进行判断是否需要拦截,依据就是ThreadLocal中是否存在用户,有就放行,没有就拦截
        UserDTO user = UserHolder.getUser();
        if (user == null){
            response.setStatus(401);
            return false;
        }
//        放行
        return true;
    }

    /**
     * @Description 后置拦截器,前置拦截器放行后,删除session保护用户信息
     * @Retrurn
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

二、RefreshtokenInterceptor
在登录拦截器前进行全路径拦截,为了更新redis中的用户信息存活时间,以及将用户信息存入ThreadLocal中便于使用。

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 {
        //        获取请求头中的token
        String token = request.getHeader("authorization");
        boolean blank = StrUtil.isBlank(token);
//        是空表示没有登录,放行交给登录拦截器
        if (blank) {
            return true;
        }
//        基于token获取redis中的用户,entries可以接受一个键值中的所有字段
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
//        取不到也放行,该拦截器只是为了更新redis和保存ThreadLocal
        if (userMap.isEmpty()) {
            return true;
        }
//        如果不为空,则将usermap在转换成userDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//        存在放行,并且保存在ThreadLocal中
        UserHolder.saveUser(userDTO);

//            新增刷新token的有效期
        stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);
//        放行
        return true;
    }
}

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐