流程
前面只是基础功能的实现,后面才是redis共享session应用
生成验证码
实现
public Result sendCode(String phone, HttpSession session) {
// 校验手机号
// 使用的是自己编写的一个工具包进行的正则校验
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机格式错误");
}
// 生成验证码 使用的是hutool包里的工具生成随机数
String code = RandomUtil.randomNumbers(6);
// 保存验证码到session
session.setAttribute("code",code);
// 发送验证码
return Result.ok();
}
手机号登录,注册
实现
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号
// 使用的是自己编写的一个工具包进行的正则校验
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机格式错误");
}
// 2.校验验证码
String code = loginForm.getCode();
Object cacheCode = session.getAttribute("code");
if(code == null || !code.equals(cacheCode)){
return Result.fail("验证码有误");
}
// 3.根据手机号查询用户
User user = lambdaQuery().eq(User::getPhone, phone).one();
// 4.用户是否存在
if(user == null) {
// 4.1不存在就创建
user = createUser(phone,session);
}
// 5.存在就将用户保存到session
session.setAttribute("user",user);
return Result.ok();
}
private User createUser(String phone, HttpSession session) {
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX + phone);
save(user);
// 将user放到session
session.setAttribute("user",user);
return user;
}
校验登录状态
实现
拦截器实现
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.从session中获取用户
HttpSession session = request.getSession();
User user = (User) session.getAttribute("user");
// 2.用户是否存在
if (user == null) {
// 2.1不存在 就拦截
response.setStatus(401);
Result notLogin = Result.fail("未授权");
response.setContentType("text/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(notLogin));
return false;
}
// 2.2存在
// 保存用户到ThreadLocal,使用的是自己编写的工具
/*UserDTO userDTO = new UserDTO();
userDTO.setId(user.getId());
userDTO.setIcon(user.getIcon());
userDTO.setNickName(user.getNickName());*/
UserHolder.saveUser(BeanUtil.copyProperties(user,UserDTO.class));//太麻烦,使用hutool工具
// 3.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清理数据
UserHolder.removeUser();
}
}
@Configuration
public class WebmvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor()).
excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
}
}
使用redis优化
登录生成验证码优化
public Result sendCode(String phone, HttpSession session) {
// 校验手机号
// 使用的是自己编写的一个工具包进行的正则校验
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机格式错误");
}
// 生成验证码 使用的是hutool包里的工具生成随机数
String code = RandomUtil.randomNumbers(6);
// *modify: 将验证码保存到redis中
stringRedisTemplate.opsForValue()
.set(RedisConstants.LOGIN_CODE_KEY + phone,code,
RedisConstants.LOGIN_CODE_TTL,TimeUnit.MINUTES);// 设置过期时间
// 发送验证码
log.info("生成的验证码是:{}",code);
return Result.ok();
}
登录优化
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号
// 使用的是自己编写的一个工具包进行的正则校验
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机格式错误");
}
// 2.校验验证码
String code = loginForm.getCode();
// *modify
// *1.从redis中获取验证
String codeKey = RedisConstants.LOGIN_CODE_KEY + phone;
String cacheCode = stringRedisTemplate.opsForValue()
.get(codeKey);
if(code == null || !code.equals(cacheCode)){
return Result.fail("验证码有误");
}
// *验证码正确则删除redis中缓存的验证码
stringRedisTemplate.delete(codeKey);
// 3.根据手机号查询用户
User user = lambdaQuery().eq(User::getPhone, phone).one();
// 4.用户是否存在
if(user == null) {
// 4.1不存在就创建
user = createUser(phone,session);
}
// 5.存在就将用户保存到session
// session.setAttribute("user",user);
// *modify: 将用户保存到redis中
// *1.将user转换为map
// *1.1 先将user转换为userDto
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(), new CopyOptions()
.setIgnoreNullValue(true) //忽略null值
.setFieldValueEditor((field,value) -> value.toString())//将值变为String类型
);
// *2.将map保存到redis
String tokenKey = RedisConstants.LOGIN_USER_KEY + UUID.randomUUID().toString(true);
stringRedisTemplate.opsForHash().putAll(tokenKey,map);
// *3.设置过期时间
stringRedisTemplate.expire(tokenKey,RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);
return Result.ok(tokenKey);
}
拦截器优化
拦截器配置
@Configuration
public class WebmvcConfig 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"
).order(1);
}
}
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 {
// *modify
// *1.从redis中获取用户
// *1.1.先获取request请求头中的authorization
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
// 1.2token不存在 就拦截
response.setStatus(401);
Result notLogin = Result.fail("未授权");
response.setContentType("text/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(notLogin));
return false;
}
// *从redis获取用户
Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(token);
if (entries.isEmpty()) {
return false;
}
// *2将map转换为userDto
UserDTO userDTO = BeanUtil.fillBeanWithMap(entries, new UserDTO(), false);
// * 将用户保存到ThreadLocal
UserHolder.saveUser(userDTO);
// *3.设置redis中userDto的过期时间
stringRedisTemplate.expire(token,RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清理数据
UserHolder.removeUser();
}
}
问题
当一个用户登录之后,访问的资源都是不需要过滤的,这样的话,拦截器就不能对token进行刷新了。
解决方案的话,就是需要在配置一个拦截器,用来拦截所有请求,对这些请求进行token刷新后,直接放行,交给第二个过滤器去判断是否放行。
优化如下
拦截器1
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 {
// *modify
// *1.从redis中获取用户
// *1.1.先获取request请求头中的authorization
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
return true;
}
// *从redis获取用户
Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(token);
if (entries.isEmpty()) {
return true;
}
// *2将map转换为userDto
UserDTO userDTO = BeanUtil.fillBeanWithMap(entries, new UserDTO(), false);
// * 将用户保存到ThreadLocal
UserHolder.saveUser(userDTO);
// *3.刷新redis中userDto的过期时间
stringRedisTemplate.expire(token,RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清理数据
UserHolder.removeUser();
}
}
拦截器2
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.从Thread Local中获取用户
UserDTO user = UserHolder.getUser();
// 2.判断用户是否存在
if (user == null) {
//2.1不存在
return false;
}
//2.2用户存在
return true;
}
}
配置
@Configuration
public class WebmvcConfig implements WebMvcConfigurer {
@Autowired
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);
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
.addPathPatterns("/**").order(0);
}
}
总结
短信登录使用redis实现需要注意以下几点
- 生成验证码之后需要将生成的验证码存入到redis,并且设置一个ttl
- 登录时,需要从redis中获取验证码,当登录成功的时候需要将redis中的 验证码删除。
- 验证码输入正确后,当从数据库查询到用户之后,需要将用户信息保存到redis中,使用hash保存,需要注意的是序列化时value容易出错,并且设置ttl
- 登录检验时,从redis中获取用户,如果存在,就刷新token时间
- 需要设置两个拦截器,第一个用于拦截所有请求,之后刷新token时间,第二个用来判断这些请求是否需要过滤