发布时间:2023-03-01 10:30
在传统的项目中,用户登录成功,将用户信息保存在session中,这种方式在微服务架构中会产生一系列问题。例如在购物车服务具有多台服务器,当一个请求落在购物车1号服务器后,其session保存了用户信息,另一个请求落在了购物车2号服务器,发现没有用户信息,则重新需要进行登录。服务器之间有session不共享的问题。为了解决这一问题,tomcat提出了内存拷贝,即只需要配置一些信息即可实现多台服务器之间的session拷贝,但是这种解决方案也有缺陷,例如:
因为在用户登录注册时,服务器会获取到手机号,所以可以使用手机号作为key,进行验证手机号和验证码时也方便进行匹对,那么在保存用户信息到Redis时为什么要使用随机token呢?因为在用户独立成功后,用户的每次请求都会携带cookie,如果将保存用户信息的key设置为含手机号的,那么用户的请求中的cookie也需要携带手机号,这样就会有一定的安全风险,所以在用户登录成功后,我们随机生成token,用token作为key,并且返回给前端token,这样前端请求时就会携带token,也避免了安全隐患。
@Override
public Result sedCode(String phone, HttpSession session) {
//1. 校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
//2.如果不符合,返回错误信息
return Result.fail(\"手机号格式错误\");
}
// 3.从redis里获取验证码是否存在
if(null==stringRedisTemplate.opsForValue().get(\"loginCode\" + phone)){
log.info(\"请勿重复获取验证码\");
return Result.fail(\"请勿重复获取验证码\");
}
//4. 符合,生成验证码
String code = RandomUtil.randomNumbers(6);
//5. 保存验证码到redis,并设置有效期1分钟,在设置key的时候,可以提前设置一个常量,然后在这里引用即可
stringRedisTemplate.opsForValue().set(\"loginCode:\"+phone,code,1, TimeUnit.MINUTES);
//5. 发送验证码 模拟发送
log.debug(\"发送短信验证码成功,验证码:{}\",code);
//返回ok
return Result.ok();
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1. 校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail(\"手机号格式错误\");
}
//2. 获取Redis中的校验验证码
String cacheCode = stringRedisTemplate.opsForValue().get(\"loginCode\" + phone);
// 3.获取表单中的验证码
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.toString().equals(code)){
//3. 不一致,报错
return Result.fail(\"验证码错误\");
}
//4.一致,根据手机号查询用户
User user = query().eq(\"phone\", phone).one();
//5. 判断用户是否存在
if (user == null){
//6. 不存在,创建新用户
user = createUserWithPhone(phone);
}
//7.保存用户信息到session
// 生成token
String token = UUID.randomUUID().toString();
// 将User转为Map
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userDtoMap = BeanUtil.beanToMap(userDTO);
// 存储
stringRedisTemplate.opsForHash().putAll(\"login:token:\"+token,userDtoMap);
// 设置有效期30分钟
stringRedisTemplate.expire(\"login:token:\"+token,30, TimeUnit.MINUTES);
// 返回token给前端
return Result.ok(token);
}
有些请求是需要用户登录才能进行访问的,所以我们设置一个登录拦截器先拦截请求,判断用户是否登录,如果登录了就进行放行即可。
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:token:\" + 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();
}
}
preHandle方法是在controller之前运行,在这个方法里面可以进行验证登录状态的操作。
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// ThreadLocal中获取用户信息
if (UserHolder.getUser()==null){
response.setStatus(401);
return false;
}
// 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除用户
UserHolder.removeUser();
}
}
两个拦截器配置了,但是没有生效,需要在配置类里进行配置
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
// 配置不需要被拦截的路径
\"/shop/**\",
\"/voucher/**\",
\"/shop-type/**\",
\"/upload/**\",
\"/blog/hot\",
\"/user/code\",
\"/user/login\"
).order(1);
registry.addInterceptor(new RefreshTokenInterceptor(redisTemplate))
.excludePathPatterns(
\"/user/login\",
\"/user/code\"
).order(0);
}
}
这里配置两个拦截器,两个拦截器是有先后顺序的,上述已经说明,通过设置order属性即可配置先后顺序,值越小,优先级越高。