Redis-Day2实战篇-短信登录(基于Session实现登录, 集群的session共享问题, 基于Redis实现共享session登录)
2023-12-22 08:29:15
Redis-Day2实战篇-短信登录
基于Session实现登录
业务流程
实现发送短信验证码
// Service
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: {}", code);
// 6. 返回ok
return Result.ok();
}
实现短信验证码登录, 注册
// Service
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1. 检验手机号
String phone = loginForm.getPhone();
if(RegexUtils.isPhoneInvalid(phone)){
// 2. 如果不符合, 返回错误信息
return Result.fail("手机号格式错误!");
}
// 3. 检验验证码
Object cacheCode = session.getAttribute("code");
String code = loginForm.getCode();
if(cacheCode == null || !cacheCode.toString().equals(code)){
// 4. 如果不一致, 返回错误信息
return Result.fail("验证码错误!");
}
// 5. 一致, 根据手机号查询用户
User user = query().eq("phone", phone).one();
// 6. 判断用户是否存在
if(user == null){
// 7. 不存在, 创建用户并保持
user = createWithPhone(phone);
}
// 8. 保存用户到session
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
return Result.ok();
}
private User createWithPhone(String phone) {
// 1. 创建用户
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
// 2. 保存用户
save(user);
return user;
}
实现登录检验拦截器
// Interceptor
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();
}
}
// Config
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
);
}
}
集群的session共享问题
- session共享问题: 多台Tomcat并不共享session存储空间, 当请求切换到不同tomcat服务时导致数据丢失的问题
- session的替代方案应该满足:
- 数据共享
- 内存存储
- key, value结构
- redis很适合
基于Redis实现共享session登录
业务流程
- 保存登录的用户信息
- String结构: 以JSON字符串保存, 比较直观
- Hash结构: 将对象中的每个字段独立存储, 可以针对单个字段做CRUD, 并且内存占用更少
项目实现
// Service
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@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);
// 4. 保存验证码到redis
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 5. 发送验证码
log.debug("短信验证码, code: {}", code);
// 6. 返回ok
return Result.ok();
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1. 检验手机号
String phone = loginForm.getPhone();
if(RegexUtils.isPhoneInvalid(phone)){
// 2. 如果不符合, 返回错误信息
return Result.fail("手机号格式错误!");
}
// 3. 检验验证码
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if(cacheCode == null || !cacheCode.equals(code)){
// 4. 如果不一致, 返回错误信息
return Result.fail("验证码错误!");
}
// 5. 一致, 根据手机号查询用户
User user = query().eq("phone", phone).one();
// 6. 判断用户是否存在
if(user == null){
// 7. 不存在, 创建用户并保持
user = createWithPhone(phone);
}
// // 8. 保存用户到session
// session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
// 8. 保存用户到redis
// 8.1. 随机生成token, 作为登录令牌
String token = UUID.randomUUID().toString(true);
// 8.2. 将User对象转为Hash存储
UserDTO userDTO = new UserDTO();
BeanUtils.copyProperties(user, userDTO);
String userJson = JSON.toJSONString(userDTO);
Map userMap = JSON.parseObject(userJson, Map.class);
userMap.forEach((key, value)->{
userMap.put(key, String.valueOf(value));
});
// 8.3. 存储
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 8.4. 设置token有效期
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 9. 返回token
return Result.ok(token);
}
...
}
// Interceptor
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");
if(StrUtil.isBlank(token)){
// 不存在, 拦截, 返回401状态码
response.setStatus(401);
return false;
}
// 2. 基于token获取redis中的用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
.entries(LOGIN_USER_KEY + token);
// 3. 判断用户是否存在
if(userMap.isEmpty()){
// 4. 不存在, 拦截, 返回401状态码
response.setStatus(401);
return false;
}
// 5. 将查询到的Hash数据转为UserDTO对象
String userJson = JSON.toJSONString(userMap);
UserDTO userDTO = JSON.parseObject(userJson, UserDTO.class);
// 6. 存在, 保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);
// 7. 刷新token有效期
stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8. 放行
return true;
}
...
}
- 思考
- 类转map: 类->JSON字符串->map
- 构造器注入: 如果一个类没有交给spring管理, 那么类中成员变量需要注入时, 就要手动的构造器注入
解决状态登录刷新的问题
- 问题: 如果用户一直请求无需拦截的页面, 会导致token不刷新
- 解决: 增加一个拦截器拦截所有请求
// Interceptor
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 判断是否需要拦截(ThreadLocal中是否有用户)
if(UserHolder.getUser() == null){
// 没有, 需要拦截, 设置状态码
response.setStatus(401);
// 拦截
return false;
}
// 有用户, 则放行
return true;
}
}
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");
if(StringUtils.isBlank(token)){
return true;
}
// 2. 基于token获取redis中的用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
.entries(LOGIN_USER_KEY + token);
// 3. 判断用户是否存在
if(userMap.isEmpty()){
return false;
}
// 5. 将查询到的Hash数据转为UserDTO对象
String userJson = JSON.toJSONString(userMap);
UserDTO userDTO = JSON.parseObject(userJson, UserDTO.class);
// 6. 存在, 保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);
// 7. 刷新token有效期
stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8. 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
// Config
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// token刷新拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
}
}
来源
黑马程序员. Redis入门到实战教程
Gitee地址
https://gitee.com/Y_cen/redis
文章来源:https://blog.csdn.net/Y_cen/article/details/134702662
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!