短信登录
大约 4 分钟项目实战Redis项目实战
提示
黑马点评Redis实战项目: https://www.bilibili.com/video/BV1cr4y1671t
01.基于session实现登录
发送短信验证码

UserController
/**
* 发送手机验证码
* @param phone 手机号
* @param session session
* @return R
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
// 发送短信验证码并保存验证码
return userService.sendCode(phone, session);
}
@Override
public Result sendCode(String phone, HttpSession session) {
// 1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)){
return Result.fail("手机号格式错误!");
}
// 2.生成验证码
String code = RandomUtil.randomNumbers(6);
// 3.保存验证码
session.setAttribute("code", code);
// 4.发送验证码
log.debug("发送短信验证码成功,验证码: {}", code);
return Result.ok();
}
实现登录注册功能

UserController
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
* @param session session
* @return R
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
// 实现登录功能
return userService.login(loginForm, session);
}
@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(SystemConstants.SESSION_CODE);
String code = loginForm.getCode();
if (null == cacheCode || !cacheCode.toString().equals(code)){
return Result.fail("验证码错误");
}
// 3.根据手机号查询用户
User user = query().eq("phone", phone).one();
// 4.判断是否存在
if (null == user){
// 5.创建新用户
user = createUserWithPhone(phone);
}
// 6.保存用户信息到session
session.setAttribute(SystemConstants.SESSION_USER, user);
log.info("用户登陆成功: {}", user.getPhone());
return Result.ok();
}
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
save(user);
return user;
}
校验登录状态
实现登录校验拦截器:

public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.获取session
HttpSession session = request.getSession();
// 2.获取用户
Object user = session.getAttribute(SystemConstants.SESSION_USER);
// 3.判断用户是否存在
if (null == user){
response.setStatus(401);
return false;
}
User loginUser = (User) user;
UserHolder.saveUser(BeanUtil.copyProperties(loginUser, UserDTO.class));
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
WebMvcConfig
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**"
);
}
}
session共享的问题分析
多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。
session的替代方案应该满足:
- 数据共享。
- 内存存储。
- key、value结构。
02.基于Redis实现短信登录

短信登录
UserController
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码ession
* @return R
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm){
// 实现登录功能
return userService.login(loginForm);
}
@Override
public Result login(LoginFormDTO loginForm) {
// 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 (null == cacheCode || !cacheCode.equals(code)){
return Result.fail("验证码错误");
}
// 3.根据手机号查询用户
User user = query().eq("phone", phone).one();
// 4.判断是否存在
if (null == user){
// 5.创建新用户
user = createUserWithPhone(phone);
}
// 6. 保存用户信息到Redis
// 6.1 随机生成token
String token = UUID.randomUUID().toString(true);
// 6.2 将User对象转为HashMap存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// Long转为String
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(1),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((name, value)-> value.toString()));
// 6.3存储
String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
stringRedisTemplate.expire(tokenKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
log.info("用户登陆成功: {}", user.getPhone());
// 7.返回token
log.info("返回信息: {}", Result.ok(token));
return Result.ok(token);
}
private User createUserWithPhone(String phone) {
User user = new User();
user.setPhone(phone);
user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
save(user);
return user;
}
登录拦截器
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) {
// 1.获取气请求头中的token
String token = request.getHeader(SystemConstants.AUTHORIZATION);
if (StrUtil.isBlank(token)) {
return true;
}
// 2.从Redis获取用户
String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
// 3.判断用户是否存在
if (userMap.isEmpty()){
return true;
}
// 4.用户信息存入ThreadLocal
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
UserHolder.saveUser(userDTO);
// 5.刷新token
stringRedisTemplate.expire(tokenKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
UserHolder.removeUser();
}
}
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;
}
}
WebMvcConfig
@Configuration
public class WebMvcConfig 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/**"
).order(1);
// token刷新拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
.order(0);
}
}
退出登录
清除Redis中的登录用户信息。
UserController
@PostMapping("/logout")
public Result logout(HttpServletRequest request){
return userService.logout(request);
}
UserServiceImpl
@Override
public Result logout(HttpServletRequest request) {
String token = request.getHeader(SystemConstants.AUTHORIZATION);
stringRedisTemplate.delete(RedisConstants.LOGIN_USER_KEY + token);
log.info("用户退出登录: {}", UserHolder.getUser().getId());
return Result.ok();
}
Redis代替session需要考虑的问题
- 选择合适的数据结构。
- 选择合适的key。
- 选择合适的存储粒度。