
其中的表有:
注意:Mysql的版本采用5.7及以上版本






start nginx.exe





主要代码:
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService userService;
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// 发送短信验证码并保存验证码
return userService.sendCode(phone, session);
}
}
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Override
public Result sendCode(String phone, HttpSession session) {
// 1.使用工具类校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.符合,生成验证码
String code = RandomUtil.randomNumbers(6);
// 4.保存验证码到 session
session.setAttribute("code",code);
// 5.模拟发送验证码
log.debug("发送短信验证码成功,验证码:{}", code);
// 返回ok
return Result.ok();
}
}

主要代码:
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// 实现登录功能
return userService.login(loginForm, session);
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 2.校验验证码
Object cacheCode = session.getAttribute("code");
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.toString().equals(code)) {
// 3.验证码不一致,则报错
return Result.fail("验证码错误");
}
// 4.验证码一致,根据手机号查询用户
User user = query().eq("phone", phone).one();
// 5.判断用户是否存在
if (user == null) {
// 6.用户不存在,则创建用户并保存
user = createUserWithPhone(phone);
}
// 7.保存用户信息到session中,UserDTO只包含简单的用户信息,
// 而不是完整的User,这样可以隐藏用户的敏感信息(例如:密码等),还能减少内存使用
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
// 8.返回ok
return Result.ok();
}
private User createUserWithPhone(String phone) {
// 1.创建用户
User user = new User();
user.setPhone(phone);
// 随机设置昵称 user_mrkuw05lok
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
// 2.保存用户
save(user);
return user;
}


主要代码:
public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
public static void saveUser(UserDTO user){
tl.set(user);
}
public static UserDTO getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
public class LoginInterceptor implements HandlerInterceptor {
/**
* 前置拦截
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@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;
}
// 5.存在,保存用户信息到ThreadLocal
UserHolder.saveUser((User)user);
// 6.放行
return true;
}
/**
* 后置拦截器
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
// 请求结束后移除用户,防止ThreadLocal造成内存泄漏
UserHolder.removeUser();
}
}
@Configuration
public class MvcConfig implements WebMvcConfigurer {
/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
// 排除不需要拦截的路径
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
);
}
}
@GetMapping("/me")
public Result me(){
// 获取当前登录的用户并返回
UserDTO user = UserHolder.getUser();
return Result.ok(user);
}





@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result sendCode(String phone, HttpSession session) {
// 1.使用工具类校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.符合,生成验证码
String code = RandomUtil.randomNumbers(6);
// 4.保存验证码到 session
// session.setAttribute("code",code);
// 4.保存验证码到 redis
// "login:code:"是业务前缀,以"login:code:" + 手机号为key,过期时间2分钟
stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 5.模拟发送验证码
log.debug("发送短信验证码成功,验证码:{}", code);
// 返回ok
return Result.ok();
}
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// // 2.校验验证码
// Object cacheCode = session.getAttribute("code");
// String code = loginForm.getCode();
// if (cacheCode == null || !cacheCode.toString().equals(code)) {
// // 3.验证码不一致,则报错
// return Result.fail("验证码错误");
// }
// 2.从Redis获取验证码并校验
String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.equals(code)) {
// 3.验证码不一致,则报错
return Result.fail("验证码错误");
}
// 4.验证码一致,根据手机号查询用户
User user = query().eq("phone", phone).one();
// 5.判断用户是否存在
if (user == null) {
// 6.用户不存在,则创建用户并保存
user = createUserWithPhone(phone);
}
// // 7.保存用户信息到session中,UserDTO只包含简单的用户信息,而不是完整的User,这样可以隐藏用户的敏感信息(例如:密码等),还能减少内存使用
// session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
// 7.保存用户信息到redis中
// 7.1随机生成token,作为登录令牌
// 使用hutool工具中的UUID,true表示不带“-”符号的UUID
String token = UUID.randomUUID().toString(true);
// 7.2将User对象转为Hash类型进行存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// 由于使用的是stringRedisTemplate,所以存入的value中的值必须都是String类型的
// 但是UserDTO中的id是Long类型的,所以进行对象属性拷贝时,需要自定义实现转换规则
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// 7.3存入redis, "login:token:"是业务前缀,以 "login:token:" + token作为key
stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap);
// 7.4设置token有效期,有效期为30分钟
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.返回token
return Result.ok(token);
}
private User createUserWithPhone(String phone) {
// 1.创建用户
User user = new User();
user.setPhone(phone);
// 随机设置昵称 user_mrkuw05lok
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
// 2.保存用户
save(user);
return user;
}

刷新token的拦截器代码:
public class RefreshTokenInterceptor implements HandlerInterceptor {
// 因为LoginInterceptor不是通过Spring进行管理的Bean,所以不能再LoginInterceptor中进行注入StringRedisTemplate
// 可以通过构造方法传入StringRedisTemplate
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 前置拦截
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@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;
// }
// // 5.存在,保存用户信息到ThreadLocal
// UserHolder.saveUser((UserDTO)user);
// // 6.放行
// return true;
// 1.获取请求头中的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
// 不存在,则拦截,返回401状态码
response.setStatus(401);
return false;
}
// 2.通过token获取redis中的用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
.entries(RedisConstants.LOGIN_USER_KEY + token);
// 3.判断用户是否存在
if (userMap.isEmpty()) {
// 4.用户不存在,则拦截,返回401状态码
response.setStatus(401);
return false;
}
// 5.将redis中Hash类型数据转换成UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 6.用户存在,保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);
// 7.刷新token有效期
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.放行
return true;
}
/**
* 后置拦截器
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
// 请求结束后移除用户,防止ThreadLocal造成内存泄漏
UserHolder.removeUser();
}
}
登录拦截器的代码:
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.判断是否需要拦截(ThreadLocal中是否有用户)
if (UserHolder.getUser() == null) {
// 没有,需要拦截,设置状态码
response.setStatus(401);
// 拦截
return false;
}
// 有用户,则放行
return true;
}
}
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
// 排除不需要拦截的路径
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
// token刷新的拦截器,order越小,执行优先级越高,所以token刷新的拦截器先执行
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**")
.excludePathPatterns(
// RefreshTokenInterceptor拦截器也需要放行"/user/code","/user/login",不然token过期后再重新登录就会一直被拦截
"/user/code",
"/user/login")
.order(0);
}
}