前言
本系列为Redis实战系列,旨在通过实际场景学习Redis相关使用方法。本系列项目使用spring-boot-starter-data-redis
(SpringDataRedis)来操作Redis。
原文地址:https://xuedongyun.cn/post/44039/
项目流程
发送验证码:
- 随机生成验证码
- 保存验证码(之前用Session,现在用Redis)
- 发送验证码
- 返回ok
登录阶段:
- 使用手机号从Redis中获取验证码
- 校验验证码是否一致(不一致报错)
- 一致,根据手机号查询用户
- 判断用户是否存在(不存在则创建用户)
- 保存用户信息到Redis,使用token作为key
- 返回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中存储验证码
key | value(验证码) |
---|
login:code:13001234567 | 672636 |
1 2 3 4 5 6 7 8 9 10 11 12 13
| public Result sendCode(String phone) {
String code = RandomUtil.randomNumbers(6);
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中存储用户信息
key | value(hash格式的用户信息) |
---|
login:token:b725075b-1ba4-4658-8c89-597d7c43f965 | name: 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();
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");
String token = UUID.randomUUID().toString(); String tokenKey = "login:token:" + token;
UserDto userDto = BeanUtil.copyProperties(user, UserDto.class); Map<String, Object> userMap = BeanUtil.beanToMap(userDto, false, false);
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap); stringRedisTemplate.expire(tokenKey, 10, TimeUnit.DAYS);
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; }
String tokenKey = "login:token:" + token; Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
if (userMap.isEmpty()) { return true; } UserDto userDto = BeanUtil.fillBeanWithMap(userMap, new UserDto(), false); UserHolder.saveUser(userDto);
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); 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
|