项目整体介绍
数据库表介绍
基于session的短信验证码登录与注册
controller层
// 获取验证码@PostMapping("code")public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {return userService.sendCode(phone, session);}// 获取验证码之后登录页面@PostMapping("/login")public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){// TODO 实现登录功能return userService.login(loginForm, session);}
service层
@Override
public Result sendCode(String phone, HttpSession session) {// 1. 校验手机号格式是否正确if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式不正确"); // 如果手机号格式不正确,返回失败结果}// 2. 生成6位随机数字验证码String code = RandomUtil.randomNumbers(6);// 3. 将验证码存储到HttpSession中session.setAttribute("code", code);// 4. 模拟发送验证码(实际开发中可以替换为短信发送逻辑)log.debug("发送验证码成功,验证码为:" + code);// 5. 返回成功结果return Result.ok();
}@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {// 1. 校验手机号格式是否正确(防止用户在发送验证码后修改手机号)String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手机号格式不正确"); // 如果手机号格式不正确,返回失败结果}// 2. 从HttpSession中获取存储的验证码Object Cachecode = session.getAttribute("code");// 3. 校验用户输入的验证码是否正确if (!loginForm.getCode().equals(Cachecode.toString()) || Cachecode == null) {return Result.fail("验证码错误"); // 如果验证码不匹配或为空,返回失败结果}// 4. 判断数据库中是否存在该手机号对应的用户User user = lambdaQuery().eq(User::getPhone, phone).one(); // 查询用户// 5. 如果用户不存在,则创建新用户并保存到数据库if (user == null) {user = new User();user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10)) // 设置随机昵称.setPhone(phone); // 设置手机号save(user); // 保存新用户到数据库}// 6. 将用户信息存储到HttpSession中session.setAttribute("user", user);// 7. 返回登录成功结果return Result.ok();
}
基于session登录的拦截器相关配置
创建拦截器
@Slf4j // 使用Lombok自动生成日志对象
@Component // 标识这是一个Spring组件
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1. 获取当前请求的session对象HttpSession session = request.getSession();// 2. 从session中获取用户信息Object user = session.getAttribute("user");// 3. 判断用户是否存在if (user == null) {// 4. 如果用户不存在,拦截请求,返回401状态码(未授权)response.setStatus(401);return false; // 返回false表示请求被拦截,不再继续执行后续的处理器}// 5. 如果用户存在,将用户信息保存到ThreadLocal中,以便在其他地方使用BaseContext.setCurrent((User) user); // 6. 放行请求,继续执行后续的处理器return true;}// 视图渲染完毕后运行@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 记录日志信息log.info("afterCompletion ...."); // 通常移除线程池中的用户信息,防止内存泄漏BaseContext.removeCurrent();}
}
注册拦截器拦截对象
@Configuration // 标识这是一个Spring配置类
public class MvcConfig implements WebMvcConfigurer {@Autowired // 自动注入LoginInterceptor的实例private LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 向Spring MVC注册拦截器registry.addInterceptor(loginInterceptor) // 添加拦截器.addPathPatterns("/**") // 拦截所有请求路径.excludePathPatterns( // 排除不需要拦截的路径"/shop/**", // 排除/shop/下的请求"/voucher/**", // 排除/voucher/下的请求"/shop-type/**", // 排除/shop-type/下的请求"/upload/**", // 排除/upload/下的请求"/blog/hot", // 排除/blog/hot请求"/user/code", // 排除/user/code请求"/user/login" // 排除/user/login请求);}
}
基于redis实现token登录与拦截器刷新token的过期时间
拦截器内获取请求头token同时检验与刷新
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从请求头中获取 "authorization" 的值 String token = request.getHeader("authorization"); // 检查 token 是否为空或者空串 if(StrUtil.isBlank(token)){ // 如果 token 为空,设置响应状态为 401(未授权) response.setStatus(401); return false; // 返回 false,表示请求未被处理 } // 从 Redis 中获取与 token 关联的用户信息 Map<Object, Object> userMap = redisTemplate.opsForHash().entries(LOGIN_USER_KEY + token); // 检查用户信息是否为空 if(userMap.isEmpty()){ // 如果用户信息为空,设置响应状态为 401(未授权) response.setStatus(401); return false; // 返回 false,表示请求未被处理 } // 将用户信息填充到 UserDTO 对象中 UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); // 将当前用户信息设置到上下文中 BaseContext.setCurrent(userDTO); // 刷新 token 的过期时间String key = LOGIN_USER_KEY + token; redisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES); // 返回 true,表示请求可以继续处理 return true;
}
service层业务逻辑
@Autowired
private RedisTemplate redisTemplate; // 发送验证码的方法
@Override
public Result sendCode(String phone, HttpSession session) { // 1、校验手机号格式 if(RegexUtils.isPhoneInvalid(phone)){ return Result.fail("手机号格式不正确"); // 返回失败结果,提示手机号格式不正确 } // 生成一个随机的6位验证码 String code = RandomUtil.randomNumbers(6); // 将验证码存储到 Redis 中,设置过期时间 redisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES); // 模拟发送验证码(此处仅为日志记录,实际应用中应调用短信发送服务) log.debug("发送验证码成功,验证码为:" + code); return Result.ok(); // 返回成功结果
} // 登录的方法
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) { // 校验手机号,可能在收到验证码后修改了手机号 String phone = loginForm.getPhone(); if(RegexUtils.isPhoneInvalid(phone)){ return Result.fail("手机号格式不正确"); // 返回失败结果,提示手机号格式不正确 } // 从 Redis 中获取与手机号相关的验证码 String code = (String) redisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone); // 校验输入的验证码是否与 Redis 中的验证码匹配,且验证码不为空 if(!loginForm.getCode().equals(code) || code == null){ return Result.fail("验证码错误"); // 返回失败结果,提示验证码错误 } // 判断数据库中是否存在此电话号码的用户,如果没有就插入数据库 User user = lambdaQuery().eq(User::getPhone, phone).one(); if(user == null) { // 如果用户不存在,创建新用户 user = new User(); user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10)) // 设置用户昵称 .setPhone(phone); // 设置用户手机号 save(user); // 保存新用户到数据库 } // 生成一个新的 token String token = UUID.randomUUID().toString(); // 将用户信息复制到 UserDTO 对象中 UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); // 将 UserDTO 转换为 Map 以便存储到 Redis Map<String, Object> userMap = BeanUtil.beanToMap(userDTO); // 生成 Redis 中的 token 键 String tokenkey = LOGIN_USER_KEY + token; // 将用户信息存储到 Redis 中 redisTemplate.opsForHash().putAll(tokenkey, userMap); // 设置 token 的过期时间 redisTemplate.expire(tokenkey, LOGIN_USER_TTL, TimeUnit.MINUTES); // 返回成功结果,携带生成的 token return Result.ok(token);
}
双拦截器实现登录与未登录功能差别
第一层拦截器
@Autowiredprivate RedisTemplate redisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token = request.getHeader("authorization");if(StrUtil.isBlank(token)){ // 检查是否为空或者空串return true;}Map<Object, Object> userMap = redisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);if(userMap.isEmpty()){return true;}UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);BaseContext.setCurrent(userDTO);String key = LOGIN_USER_KEY + token;redisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);return true;}//视图渲染完毕后运行@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {log.info("afterCompletion ....RefreshTokenInterceptor");BaseContext.removeCurrent(); // 通常移除线程池}
第二层拦截器
@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if(BaseContext.getCurrent() == null){response.setStatus(401);return false;}return true;}
注册双拦截器
.order();方法用于指定拦截器的优先级,里面的值越小,那么优先级越高
registry.addInterceptor(loginInterceptor).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login").order(1);// 拦截所有请求registry.addInterceptor(refreshTokenInterceptor).addPathPatterns("/**").order(0);