redis-短信验证登录

0 目录

image-20240218210105896

1 基于session实现登录

1.1 发送短信验证码

image-20240218210209795

即:将生成的验证码保存到session中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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();
}

1.2 短信验证码注册登录

image-20240218210311253

即:校验完成验证码之后,根据用户手机号去数据库查询,查询用户如果不存在,那么就创建用户,将创建的心用户保存到数据库中;如果查询到用户存在,那么将用户信息保存到session中

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
40
41
42
43
44
@Data
public class LoginFormDTO {
private String phone;
private String code;
private String password;
}


@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.一致,根据手机号查询用户 select * from tb_user where phone = ?
User user = query().eq("phone", phone).one();
//5.判断用户是否存在
if(user==null) {
//6.不存在,创建新用户并保存
user=createUsrWithPhone(phone);
}
//7.保存用户信息到session中
session.setAttribute("user",user);
return null;
}

private User createUsrWithPhone(String phone) {
//1.创建用户
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
//2.保存用户
save(user);
return user;
}

1.3 校验登录状态

image-20240218210437940

即:从拦截器拦截请求之后,从request中获取到cookie,由cookie获取session进而获取到用户信息,此时判断用户是否存在,如果用户不存在进行拦截,用户存在就将用户数据保存到ThreadLocal中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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();
}
}

2 登录校验拦截器

2.1 拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}
//5.存在,保存用户信息到ThreadLocal
UserHolder.saveUser((UserDTO) user);
//6.放行
return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}

2.2 添加拦截器

1
2
3
4
5
6
7
8
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/voucher/**","/upload/**");
}
}

3 session共享问题

session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题

img

4 Redis替换session业务

img

5 基于Redis实现短信登录

5.1 发送验证码

流程修改:生成验证码后,将验证码放入至redis并且设置过期时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
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(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
//5.发送验证码
log.debug("发送短信验证码成功,验证码:{}"+code);
//返回ok
return Result.ok();
}

5.2 验证码登录

修改:校验的时候,客户端传入的验证码和redis中的验证码进行比对,随后需要将用户信息保存到redis中,此时选择使用Hash结构存储用户信息

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
40
41
42
43
44
45
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1.校验手机号
String phone = loginForm.getPhone();
if(RegexUtils.isPhoneInvalid(phone)){
//如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
//2.校验验证码
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if(cacheCode==null ||!cacheCode.equals(code)){
//3.不一致,报错
return Result.fail("验证码错误!");
}
//4.一致,根据手机号查询用户 select * from tb_user where phone = ?
User user = query().eq("phone", phone).one();
//5.判断用户是否存在
if(user==null) {
//6.不存在,创建新用户并保存
user=createUsrWithPhone(phone);
}
//保存用户信息到redis中
//1.随机生成token,作为登录令牌
String token = UUID.randomUUID().toString(true); //true代表isSimple,即不带中划线
//2.将User对象转为Hash存储
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()));
String tokenKey=LOGIN_USER_KEY+token;
//7.存储
stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
//设置token有效期
stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
return Result.ok();
}

private User createUsrWithPhone(String phone) {
//1.创建用户
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));
//2.保存用户
save(user);
return user;
}

5.3 拦截器修改

修改:此时获取的用户信息是从redis中获取而不是从session获取。查询到用户信息之后将Hash数据转为UserDto对象,再存入ThreadLocal中,同时刷新token的有效期

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
40
41
public class LoginInterceptor implements HandlerInterceptor {

private StringRedisTemplate stringRedisTemplate;

public LoginInterceptor(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate=stringRedisTemplate;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取请求头中的token
String token = request.getHeader("authorization");
//判断token是否为空
if(StrUtil.isBlank(token)){
response.setStatus(401);
return false;
}
String key=LOGIN_USER_KEY+token;
//2.基于token获取redis中的用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
//3.判断用户是否存在
if(userMap.isEmpty()){
//不存在,拦截,返回401状态码
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,LOGIN_USER_TTL, TimeUnit.MINUTES);
//8.放行
return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}

6 登录状态刷新问题

img

即:配置两个拦截器

拦截器1:拦截一切的请求,同时刷新token的有效期

拦截器2:拦截登录的请求

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
public class RefreshTokenInterceptor implements HandlerInterceptor {

private StringRedisTemplate stringRedisTemplate;

public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate=stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

//1.获取请求头中的token
String token = request.getHeader("authorization");
//判断token是否为空
if(StrUtil.isBlank(token)){
return true;
}
String key=LOGIN_USER_KEY+token;
//2.基于token获取redis中的用户
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,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
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
@Configuration
public class MvcConfig implements WebMvcConfigurer {

@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/voucher/**",
"/upload/**").order(1);
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);
}
}

拦截器的顺序,order值越小越小执行


redis-短信验证登录
https://baijianglai.cn/redis-短信验证登录/6c096f110ba0/
作者
Lai Baijiang
发布于
2024年2月18日
许可协议