Redis实战系列(1):短信登录

布鸽不鸽 Lv4

前言

本系列为Redis实战系列,旨在通过实际场景学习Redis相关使用方法。本系列项目使用spring-boot-starter-data-redis(SpringDataRedis)来操作Redis。

原文地址:https://xuedongyun.cn/post/44039/

项目流程

发送验证码:

  1. 随机生成验证码
  2. 保存验证码(之前用Session,现在用Redis)
  3. 发送验证码
  4. 返回ok

登录阶段:

  1. 使用手机号从Redis中获取验证码
  2. 校验验证码是否一致(不一致报错)
  3. 一致,根据手机号查询用户
  4. 判断用户是否存在(不存在则创建用户)
  5. 保存用户信息到Redis,使用token作为key
  6. 返回token

用到的实体类

1
2
3
4
5
public class User {
private String name;
private String phone;
private String password;
}
1
2
3
4
public class UserDto {
private String name;
private String phone;
}
1
2
3
4
public class LoginFormDto {
private String phone;
private String code;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Result {
private Boolean success;
private String errorMsg;
private Object data;
private Long total;

public static Result ok() {
return new Result(true, null, null, null);
}

public static Result ok(Object data) {
return new Result(true, null, data, null);
}

public static Result ok(List<?> data, Long total) {
return new Result(true, null, data, total);
}

public static Result fail(String errorMsg) {
return new Result(false, errorMsg, null, null);
}
}

UserService(发送短信验证码)

用户上传手机号,生成随机验证码,短信发送给用户

Redis中存储验证码

keyvalue(验证码)
login:code:13001234567672636
1
2
3
4
5
6
7
8
9
10
11
12
13
public Result sendCode(String phone) {

// 生成验证码
String code = RandomUtil.randomNumbers(6);

// 验证码存在Redis中,并设置过期时间
stringRedisTemplate.opsForValue().set("login:code:" + phone, code);
stringRedisTemplate.expire("login:code:" + phone, 5, TimeUnit.MINUTES);

log.info("模拟发送短信:{}", code);

return Result.ok();
}

UserService(登录)

登录的主要逻辑

  • 验证用户上传的验证码(根据手机号从Redis中拿)
  • 登录成功后,将User信息存入Redis中,返回token

Redis中存储用户信息

keyvalue(hash格式的用户信息)
login:token:b725075b-1ba4-4658-8c89-597d7c43f965name: xdy, phone: 13001234567
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
public Result login(LoginFormDto loginForm) {

// 获取手机号
String phone = loginForm.getPhone();

// 判断用户上传的验证码,和Redis中缓存的验证码是否一致
String cacheCode = stringRedisTemplate.opsForValue().get("login:code:" + phone);
if (cacheCode == null || !cacheCode.equals(loginForm.getCode())) {
return Result.fail("验证码错误");
}

// 从数据库中查询用户是否存在,存在就直接获取
// 如果不存在,数据库创建新用户
User user = new User("xdy", phone, "123456");

// 随机生成token,作为登录令牌
String token = UUID.randomUUID().toString();
String tokenKey = "login:token:" + token;

// 将User转换为UserDto, 再转化为HashMap
UserDto userDto = BeanUtil.copyProperties(user, UserDto.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDto, false, false);

// 将用户信息存在redis中,并设置token有效期
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
stringRedisTemplate.expire(tokenKey, 10, TimeUnit.DAYS);

// 返回token
return Result.ok(token);
}

UserHolder

工具类,用于将UserDto变量存储在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();
}
}

RefreshTokenInterceptor

该拦截器用于刷新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
public class RefreshTokenInterceptor implements HandlerInterceptor {
private final StringRedisTemplate stringRedisTemplate;

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

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String token = request.getHeader("authorization");

if (Strings.isBlank(token)) {
return true;
}

// 若有token,从redis中获取userMap
String tokenKey = "login:token:" + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);

// 如果用户存在,保存信息到ThreadLocal
if (userMap.isEmpty()) {
return true;
}
UserDto userDto = BeanUtil.fillBeanWithMap(userMap, new UserDto(), false);
UserHolder.saveUser(userDto);

// 刷新token有效期
stringRedisTemplate.expire(tokenKey, 10, TimeUnit.DAYS);

return true;
}
}

LoginInterceptor

该拦截器用于判断登录状态

1
2
3
4
5
6
7
8
9
10
11
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 登录才放行
if (UserHolder.getUser() == null) {
response.setStatus(401);
return false;
}
return true;
}
}

WebConfig

将拦截器注册到SpringBoot中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Resource
StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
var loginInterceptor = new LoginInterceptor();
var refreshTokenInterceptor = new RefreshTokenInterceptor(stringRedisTemplate);

// 先注册refreshTokenInterceptor,后注册loginInterceptor
registry.addInterceptor(refreshTokenInterceptor).addPathPatterns("/**");
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login", "/sendCode");

}
}

UserController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
public class UserController {

@Resource
UserService userService;

@PostMapping("/sendCode")
public Result sendCode(@RequestParam String phone) {
return userService.sendCode(phone);
}

@PostMapping("/login")
public Result login(LoginFormDto loginFormDto) {
return userService.login(loginFormDto);
}

@GetMapping("/test")
public Result test() {
return Result.ok();
}
}

使用效果

测试是否已登录(请求被拦截,响应401)

1
GET		http://localhost:8080/test

首先获取验证码

1
2
3
POST	http://localhost:8080/sendCode
# 请求体
phone 13001234567

使用手机号和验证码登录

1
2
3
4
POST	http://localhost:8080/login
# 请求体
phone 13001234567
code 081256

测试是否已登录(请求被放行)

1
GET		http://localhost:8080/test
  • 标题: Redis实战系列(1):短信登录
  • 作者: 布鸽不鸽
  • 创建于 : 2023-06-25 20:53:30
  • 更新于 : 2023-07-05 18:38:34
  • 链接: https://xuedongyun.cn//post/44039/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论