Redis代替session
-
redis中设计key -
在使用session时,每个用户都会有自己的session,这样虽然验证码的键都是“code”,但是相互不影响,从而确保每个用户获取到的验证码只能够自己使用,当使用redis时,redis的key是共享的,不分用户,就要求在redis中存储验证码时,不能直接将验证码的键设置为"code",这样无法保证其唯一性。
-
-
redis中设计value -
到底该使用redis中什么数据类型存储数据,主要需要看数据样式和使用方式,一般会考虑使用String、Hash,String存储时,会多占一点内存空间,则相对来说Hash存储时,会少占用一点内存空间。 -
String结构:以Json字符串来存储,比较直观 -
Hash结构:,每个对象中每个字段独立存储,可以针对单个字段做CRUD
-
-
redis实现登录
发送验证码
-
添加redis
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
-
设置redis的连接信息
spring:
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 -
增加相关常量
/**
* 保存验证码的redis中的key
*/
public static final String LOGIN_CODE_KEY = "login:code:";
/**
* 验证码的过期时间
*/
public static final Long LOGIN_CODE_TTL = 2L; -
修改Service层
@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层
@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);
}
验证码登录、注册
-
增加相关常量
public static final String LOGIN_USER_KEY = "login:token:";
public static final Long LOGIN_USER_TTL = 30L; -
修改Service层
@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;
}String login(LoginFormDTO loginForm, HttpSession session);
-
修改Controller层
@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拦截器
private 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;
}@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拦截器,在此拦截器中刷新过期时间
@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
/**
* 刷新令牌的拦截器
* @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
public 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配置类
@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 多平台发布