本文使用的是 手机号+验证码 的登录方式,其中验证码是通过在控制台输出,并没有真的发送到手机上(太麻烦,主要目的还是学习使用Redis)
重点是看思路,而不是具体的代码实现
整体结构
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@Slf4j @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService { @Autowired private StringRedisTemplate stringRedisTemplate; @Override public Result sendCode(String phone, HttpSession session) { //... } @Override public Result login(LoginFormDTO loginForm, HttpSession session) { //... } private User createUserWithPhone(String phone) { //... } } |
这个是发送验证码的方法
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public Result sendCode(String phone, HttpSession session) { // 1. 校验手机号 if (RegexUtils.isPhoneInvalid(phone)) { // 2. 如果不符合,返回错误信息 return Result.fail("手机号格式错误!"); } // 3. 如果符合,生成验证码 String code = RandomUtil.randomNumbers(6); // 4. 保存验证码到redis stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY +phone,code,RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES); // 5. 发送验证码 log.debug("发送短信验证码成功,验证码:{}", code); // 6. 返回结果 return Result.ok(); } |
注:这里的RedisConstants是一个用来存放各种常量的类
|
1 2 3 4 5 6 |
public class RedisConstants { public static final String LOGIN_CODE_KEY = "login:code:"; public static final Long LOGIN_CODE_TTL = 2L; public static final String LOGIN_USER_KEY = "login:token:"; public static final Long LOGIN_USER_TTL = 30L; } |
这里使用了MybatisPlus来操作数据库(User user = query().eq("phone", phone).one();),但是这个不是重点
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public Result login(LoginFormDTO loginForm, HttpSession session) { // 1. 校验手机号 String phone = loginForm.getPhone(); if (RegexUtils.isPhoneInvalid(phone)) { 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. 保存用户信息到redis String token= UUID.randomUUID().toString(true); UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class); Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(), CopyOptions.create() .setIgnoreNullValue(true) .setFieldValueEditor((fieldName, fieldValue)->fieldValue.toString())); stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap); stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES); return Result.ok(token); } |
在login方法中调用了该方法
这里也使用了MybatisPlus来操作数据库(save(user);)
|
1 2 3 4 5 6 7 8 9 |
private User createUserWithPhone(String phone) { // 1. 创建用户 User user = new User(); user.setPhone(phone); user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10)); // 2. 保存用户 save(user); return user; } |
整体框架
其实就是实现了HandlerInterceptor的两个方法
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Slf4j @Component public class LoginInterceptor implements HandlerInterceptor { @Autowired private StringRedisTemplate stringRedisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //... } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 移除用户 UserHolder.removeUser(); } } |
UserHolder是ThreadLocal 持有类
|
1 2 3 4 5 6 7 8 9 10 11 12 |
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(); } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.获取请求头中的token String token = request.getHeader("authorization"); if (StrUtil.isBlank(token)) { // 不存在,拦截 response.setStatus(401); return false; } // 2.基于token获取redis中的用户 String key = RedisConstants.LOGIN_USER_KEY + token; Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key); // 3.判断用户是否存在 if (userMap.isEmpty()) { // 4.不存在,拦截 response.setStatus(401); return false; } // 5.将查询到的Hash数据转换为UserDTO对象 UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); // 6.存在,保存用户信息到ThreadLocal UserHolder.saveUser(userDTO); // 7.刷新token有效期 stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES); // 8.放行 return true; } |
注:authorization 是前端定义的用来传递token的key
配置类
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Configuration public class MvcConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns( "/user/code", "/user/login", "/blog/hot", "/shop/**", "/shop-type/**", "/upload/**", "/voucher/**" ); } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
flowchart TD subgraph A[发送验证码流程] A1["前端请求 发送验证码"] --> A2["校验手机号格式"] A2 -- 不合法 --> A3["返回错误 手机号格式错误"] A2 -- 合法 --> A4["生成6位验证码"] A4 --> A5["保存验证码到Redis"] A5 --> A6["返回成功"] end subgraph B[登录流程] B1["前端请求 登录"] --> B2["校验手机号格式"] B2 -- 不合法 --> B3["返回错误"] B2 -- 合法 --> B4["从Redis获取验证码"] B4 --> B5{"验证码是否正确"} B5 -- 否 --> B6["返回验证码错误"] B5 -- 是 --> B7["根据手机号查询用户"] B7 --> B8{"用户是否存在"} B8 -- 否 --> B9["创建新用户"] B8 -- 是 --> B10["使用已有用户"] B9 --> B11["生成Token"] B10 --> B11 B11 --> B12["用户信息写入Redis"] B12 --> B13["返回Token"] end subgraph C[请求拦截流程] C1["请求到达拦截器"] --> C2["从请求头获取Token"] C2 --> C3{"Token是否存在"} C3 -- 否 --> C4["返回401"] C3 -- 是 --> C5["从Redis获取用户信息"] C5 --> C6{"用户是否存在"} C6 -- 否 --> C4 C6 -- 是 --> C7["保存用户到ThreadLocal"] C7 --> C8["刷新Token有效期"] C8 --> C9["放行请求"] end subgraph D[请求结束] D1["请求完成"] --> D2["清理ThreadLocal"] end B13 --> C1 C9 --> D1 |
复制到未命名绘图 - draw.io中用mermaid格式文件创建流程图
目前之后访问被拦截的页面才会刷新有效期,所以这里我们需要优化一下
方式是采用拦截器链,即再加一个拦截器来拦截全部页面,以此来更新有效期
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
@Slf4j @Component public class RefreshTokenInterceptor implements HandlerInterceptor { @Autowired private StringRedisTemplate stringRedisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.获取请求头中的token String token = request.getHeader("authorization"); if (StrUtil.isBlank(token)) { return true; } // 2.基于token获取redis中的用户 String key = RedisConstants.LOGIN_USER_KEY + token; Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key); // 3.判断用户是否存在 if (userMap.isEmpty()) { return true; } // 5.将查询到的Hash数据转换为UserDTO对象 UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); // 6.存在,保存用户信息到ThreadLocal UserHolder.saveUser(userDTO); // 7.刷新token有效期 stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES); // 8.放行 return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 移除用户 UserHolder.removeUser(); } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Slf4j @Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 判断是否需要拦截(ThreadLocal中是否有用户) if (UserHolder.getUser() == null) { response.setStatus(401); return false; } // 有用户,则放行 return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 移除用户 UserHolder.removeUser(); } } |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
@Configuration public class MvcConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Autowired private RefreshTokenInterceptor refreshTokenInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { // 登录拦截器 registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns( "/user/code", "/user/login", "/blog/hot", "/shop/**", "/shop-type/**", "/upload/**", "/voucher/**" ).order(1); // 刷新token拦截器 registry.addInterceptor(refreshTokenInterceptor) .addPathPatterns("/**").order(0); } } |
注:order方法是用来设置哪一个拦截器在前,哪一个在后;规则:数字小的在前,数字大的在后