项目整体介绍
数据库表介绍
基于session的短信验证码登录与注册
controller层
java
// 获取验证码
@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层
java
@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登录的拦截器相关配置
创建拦截器
java
@Slf4j // 使用Lombok自动生成日志对象
@Component // 标识这是一个Spring组件
public class LoginInterceptor implements HandlerInterceptor {
@Override
public 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;
}
// 视图渲染完毕后运行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 记录日志信息
log.info("afterCompletion ....");
// 通常移除线程池中的用户信息,防止内存泄漏
BaseContext.removeCurrent();
}
}
注册拦截器拦截对象
java
@Configuration // 标识这是一个Spring配置类
public class MvcConfig implements WebMvcConfigurer {
@Autowired // 自动注入LoginInterceptor的实例
private LoginInterceptor loginInterceptor;
@Override
public 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同时检验与刷新
java
@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层业务逻辑
java
@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);
}
双拦截器实现登录与未登录功能差别
第一层拦截器
java
@Autowired
private RedisTemplate redisTemplate;
@Override
public 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;
}
//视图渲染完毕后运行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion ....RefreshTokenInterceptor");
BaseContext.removeCurrent(); // 通常移除线程池
}
第二层拦截器
java
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(BaseContext.getCurrent() == null){
response.setStatus(401);
return false;
}
return true;
}
注册双拦截器
.order();方法用于指定拦截器的优先级,里面的值越小,那么优先级越高
java
registry.addInterceptor(loginInterceptor)
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
// 拦截所有请求
registry.addInterceptor(refreshTokenInterceptor).addPathPatterns("/**").order(0);