java"> /**
* 发送验证码
*/
@Override
public Result sendCode(String phone, HttpSession session) {
// 1、判断手机号是否合法
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式不正确");
}
// 2、手机号合法,生成验证码,并保存到Session中
String code = RandomUtil.randomNumbers(6);
session.setAttribute(SystemConstants.VERIFY_CODE, code);
// 3、发送验证码
log.info("验证码:{}", code);
return Result.ok();
}
/**
* 用户登录
*/
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
String code = loginForm.getCode();
// 1、判断手机号是否合法
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式不正确");
}
// 2、判断验证码是否正确
String sessionCode = (String) session.getAttribute(LOGIN_CODE);
if (code == null || !code.equals(sessionCode)) {
return Result.fail("验证码不正确");
}
// 3、判断手机号是否是已存在的用户
User user = this.getOne(new LambdaQueryWrapper<User>()
.eq(User::getPassword, phone));
if (Objects.isNull(user)) {
// 用户不存在,需要注册
user = createUserWithPhone(phone);
}
// 4、保存用户信息到Session中,便于后面逻辑的判断(比如登录判断、随时取用户信息,减少对数据库的查询)
session.setAttribute(LOGIN_USER, user);
return Result.ok();
}
/**
* 根据手机号创建用户
*/
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
this.save(user);
return user;
}
java">public class LoginInterceptor implements HandlerInterceptor {
/**
* 前置拦截器,用于判断用户是否登录
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
// 1、判断用户是否存在
User user = (User) session.getAttribute(LOGIN_USER);
if (Objects.isNull(user)){
// 用户不存在,直接拦截
response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
return false;
}
// 2、用户存在,则将用户信息保存到ThreadLocal中,方便后续逻辑处理
// 比如:方便获取和使用用户信息,session获取用户信息是具有侵入性的
ThreadLocalUtls.saveUser(user);
return HandlerInterceptor.super.preHandle(request, response, handler);
}
}
配置完拦截器后,还需要将我们自定义的拦截器添加到SpringMVC的拦截器列表中,才能生效
通过这种方式,你不需要在XML文件中配置拦截器,而是直接在Java代码中进行配置,
它是一个配置类,用于配置Web MVC相关的设置。
java">@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加登录拦截器
registry.addInterceptor(new LoginInterceptor())
// 设置放行请求
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
);
}
}
数据脱敏 将session里面的数据量减小 去掉敏感信息 session是内存空间,存储信息越多对服务器压力越大
- 封装UserDTO,返回给前端的
Entity
数据使用BeanUtil
工具类转成DTO - 存储到ThreadLocal中的数据也进行数据脱敏
Session集群共享
在分布式集群环境中,会话(Session)共享是一个常见的挑战。默认情况下,Web 应用程序的会话是保存在单个服务器上的,当请求不经过该服务器时,会话信息无法被访问。
服务器之间无法实现会话状态的共享。比如:在当前这个服务器上用户已经完成了登录,Session中存储了用户的信息,能够判断用户已登录,但是在另一个服务器的Session中没有用户信息,无法调用显示没有登录的服务器上的服务
选择数据类型
- String 数据结构是以 JSON 字符串的形式保存,更加直观,操作也更加简单,但是 JSON 结构会有很多非必须的内存开销,比如双引号、大括号,内存占用比 Hash 更高
- Hash 数据结构是以 Hash 表的形式保存,可以对单个字段进行CRUD,更加灵活Redis替代
- Session需要考虑的问题:
- 选择合适的数据结构,了解 Hash 比 String 的区别选择合适的key,为key设置一个业务前缀,方便区分和分组,为key拼接一个UUID,避免key冲突防止数据覆盖选择合适的存储粒度,
- 对于验证码这类数据,一般设置TTL为3min即可,防止大量缓存数据的堆积,而对于用户信息这类数据可以稍微设置长一点,比如30min,防止频繁对Redis进行IO操作
java"> /**
* 发送验证码
*
* @param phone
* @param session
* @return
*/
@Override
public Result sendCode(String phone, HttpSession session) {
// 1、判断手机号是否合法
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式不正确");
}
// 2、手机号合法,生成验证码,并保存到Redis中
String code = RandomUtil.randomNumbers(6);
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code,
RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 3、发送验证码
log.info("验证码:{}", code);
return Result.ok();
}
/**
* 用户登录
*
* @param loginForm
* @param session
* @return
*/
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
String code = loginForm.getCode();
// 1、判断手机号是否合法
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式不正确");
}
// 2、判断验证码是否正确
String redisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
if (code == null || !code.equals(redisCode)) {
return Result.fail("验证码不正确");
}
// 3、判断手机号是否是已存在的用户
User user = this.getOne(new LambdaQueryWrapper<User>()
.eq(User::getPhone, phone));
if (Objects.isNull(user)) {
// 用户不存在,需要注册
user = createUserWithPhone(phone);
}
// 4、保存用户信息到Redis中,便于后面逻辑的判断(比如登录判断、随时取用户信息,减少对数据库的查询)
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// 将对象中字段全部转成string类型,StringRedisTemplate只能存字符串类型的数据
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create().setIgnoreNullValue(true).
setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
String token = UUID.randomUUID().toString(true);
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
return Result.ok(token);
}
/**
* 根据手机号创建用户并保存
*
* @param phone
* @return
*/
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
this.save(user);
return user;
}
如果访问首页这样的不需要验证token的页面 30min以上就要重新登录 麻烦
所以设置两个拦截器 第一个就验证
为什么不把刷新的操作放到一个拦截器中呢,因为之前的那个拦截器只是用来拦截一些需要进行登录校验的请求,对于哪些不需要登录校验的请求是不会走拦截器的,刷新操作显然是要针对所有请求比较合理,所以单独创建一个拦截器拦截一切请求,刷新Redis中的Key
用order设置顺序
刷新token的拦截器:
java">public class RefreshTokenInterceptor implements HandlerInterceptor {
// new出来的对象是无法直接注入IOC容器的(LoginInterceptor是直接new出来的)
// 所以这里需要再配置类中注入,然后通过构造器传入到当前类中
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1、获取token,并判断token是否存在
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)){
// token不存在,说明当前用户未登录,不需要刷新直接放行
return true;
}
// 2、判断用户是否存在
String tokenKey = LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
if (userMap.isEmpty()){
// 用户不存在,说明当前用户未登录,不需要刷新直接放行
return true;
}
// 3、用户存在,则将用户信息保存到ThreadLocal中,方便后续逻辑处理,比如:方便获取和使用用户信息,Redis获取用户信息是具有侵入性的
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
ThreadLocalUtls.saveUser(BeanUtil.copyProperties(userMap, UserDTO.class));
// 4、刷新token有效期
stringRedisTemplate.expire(token, LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
}
将自定义的拦截器添加到SpringMVC的拦截器表中,使其生效:
java">@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
// new出来的对象是无法直接注入IOC容器的(LoginInterceptor是直接new出来的)
// 所以这里需要再配置类中注入,然后通过构造器传入到当前类中
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加登录拦截器
registry.addInterceptor(new LoginInterceptor())
// 设置放行请求
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
).order(1); // 优先级默认都是0,值越大优先级越低
// 添加刷新token的拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}