本文使用的是 手机号+验证码 的登录方式,其中验证码是通过在控制台输出,并没有真的发送到手机上(太麻烦,主要目的还是学习使用Redis)
重点是看思路,而不是具体的代码实现
UserServiceImpl实现类
整体结构
|
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) {
//...
}
}
|
sendCode方法
这个是发送验证码的方法
|
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;
}
|
login方法
这里使用了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);
}
|
createUserWithPhone方法
在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();
}
}
|
preHandle方法
|
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格式文件创建流程图
优化
目前之后访问被拦截的页面才会刷新有效期,所以这里我们需要优化一下
方式是采用拦截器链,即再加一个拦截器来拦截全部页面,以此来更新有效期
RefreshTokenInterceptor
|
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();
}
}
|
LoginInterceptor
|
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方法是用来设置哪一个拦截器在前,哪一个在后;规则:数字小的在前,数字大的在后
|