黑马Redis详细笔记(实战篇---短信登录)
目录
一.短信登录
1.1 导入项目
1.2 Session 实现短信登录
1.3 集群的 Session 共享问题
1.4 基于 Redis 实现共享 Session 登录
一.短信登录
1.1 导入项目
数据库准备
-- 创建用户表
CREATE TABLE `user` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID',
`phone` VARCHAR(20) NOT NULL UNIQUE COMMENT '手机号',
`nick_name` VARCHAR(50) COMMENT '昵称',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) COMMENT='用户表';
导入项目
cd nginx目录
start nginx.exe
1.2 Session 实现短信登录
发送验证码
@Override
public Result sendCode(String phone, HttpSession session) {
// 校验手机号格式是否正确
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式不正确!");
}
// 生成6位随机数字验证码
String code = RandomUtil.randomNumbers(6);
// 将验证码保存到session中
session.setAttribute("code", code);
// 日志记录验证码(实际开发中应发送短信)
log.info("验证码为: " + code);
return Result.ok();
}
登录功能
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 获取手机号
String phone = loginForm.getPhone();
// 校验手机号格式
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误!");
}
// 从session中获取验证码
Object cacheCode = session.getAttribute("code");
String code = loginForm.getCode();
// 校验验证码是否正确
if (code == null || !cacheCode.toString().equals(code)) {
return Result.fail("验证码错误!");
}
// 根据手机号查询用户
User user = query().eq("phone", phone).one();
if (user == null) {
// 如果用户不存在,则自动注册
user = new User();
user.setPhone(phone);
user.setNickName("user_" + RandomUtil.randomString(10));
save(user);
}
// 将用户信息存入session
session.setAttribute("user", user);
return Result.ok();
}
拦截器 LoginInterceptor
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取session
HttpSession session = request.getSession();
// 从session中获取用户信息
Object user = session.getAttribute("user");
// 判断用户是否存在
if (user == null) {
// 用户未登录,返回401状态码
response.setStatus(401);
response.getWriter().write("用户未登录!");
return false;
}
// 用户已登录,放行
return true;
}
}
在MvcConfig加上拦截器
@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/**", // 商户类型
"/upload/**", // 文件上传
"/voucher/**" // 优惠券
);
}
}
1.3 集群的 Session 共享问题
多台Tomcat不共享session存储空间,当请求切换到不同的tomcat服务时导致数据丢失的问题
所以我们把数据存入Redis,集群的Redis可以替代session
1.4 基于 Redis 实现共享 Session 登录
我们应该选择String类型存验证码即可,value:验证码,但是key要区分开来
选择Hash存储用户信息,因为每个字段独立,比较好去DRUD,内存占用少,key用token即可(随机字符串)
之前的session的话,tomcat会自动把session的Id存入Cookie,每次请求都会携带Cookie,所以我们需要手动把token返回给客户端,每次请求客户端都会携带着token
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public Result sendCode(String phone, HttpSession session) {
// 校验手机号格式是否正确
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式不正确!");
}
// 生成6位随机数字验证码
String code = RandomUtil.randomNumbers(6);
// 将验证码保存到Redis中,设置过期时间为2分钟
stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
// 日志记录验证码(实际开发中应发送短信)
log.info("验证码为: " + code);
return Result.ok();
}
}
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 获取手机号
String phone = loginForm.getPhone();
// 校验手机号格式
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误!");
}
// 从Redis中获取验证码
String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
// 校验验证码是否正确
if (cacheCode == null || !cacheCode.equals(code)) {
return Result.fail("验证码错误!");
}
// 根据手机号查询用户
User user = query().eq("phone", phone).one();
if (user == null) {
// 如果用户不存在,则自动注册
user = new User();
user.setPhone(phone);
user.setNickName("user_" + RandomUtil.randomString(10));
save(user);
}
// 生成唯一的Token
String token = UUID.randomUUID().toString(true);
// 将用户信息转换为UserDTO
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// 将用户信息以Hash类型存储到Redis中,设置过期时间为36000分钟
stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, BeanUtil.beanToMap(userDTO));
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
// 返回Token
return Result.ok(token);
}
MvcConfig注入stringRedisTemplate,然后传给LoginInterceptor,因为LoginInterceptor不是bean不能用spring注入其他bean
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
);
}
}
LoginInterceptor
public class LoginInterceptor implements HandlerInterceptor{
private final 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中用户
String key=RedisConstants.LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
//3.判断用户是否存在
if (userMap.isEmpty()){
//4.不存在则拦截,设置响应状态吗为401(未授权)
response.setStatus(401);
return false;
}
//5.将查询到的Hash数据转化为UserDTO对象
UserDTO userDTO=new UserDTO();
BeanUtil.fillBeanWithMap(userMap,userDTO, false);
//6.保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);
//7.更新token的有效时间,只要用户还在访问我们就需要更新token的存活时间
stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);
//8.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//销毁,以免内存泄漏
UserHolder.removeUser();
}
}
用户请求进去拦截器,我们试着去获取请求头内的token,根据token去查询用户信息,判断是否拦截,保存在ThreadLocal,刷新token的有效期
但是,这个拦截器是拦截需要登录之后才需要进行请求的路径,那我如果一直在访问的是不需要拦截的页面的话,我还是会过期?这就不合理。所以我们需要在这个拦截器前面再加个拦截器,然后在新增拦截器上进行保存ThreadLocal和刷新有效期,不理解其他
其实就是对之前的拦截器进行功能拆分
MvcConfig
@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/**",
"/upload/**",
"/voucher/**"
).order(1);//RefreshTokenInterceptor 先于 LoginInterceptor 执行
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);//默认拦截所有请求
}
}
RefreshTokenInterceptor
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) throws Exception {
// 从请求头中获取Token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
// 如果Token为空,直接放行
return true;
}
// 构造Redis中的Key
String key = RedisConstants.LOGIN_USER_KEY + token;
// 从Redis中获取用户信息
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
if (userMap.isEmpty()) {
// 如果用户信息为空,直接放行
return true;
}
// 将用户信息转换为UserDTO对象
UserDTO userDTO = new UserDTO();
BeanUtil.fillBeanWithMap(userMap, userDTO, false);
// 将用户信息保存到ThreadLocal中
UserHolder.saveUser(userDTO);
// 刷新Token的有效期
stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
}
LoginInterceptor
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从ThreadLocal中获取用户信息
UserDTO user = UserHolder.getUser();
if (user == null) {
// 如果用户未登录,返回401状态码
response.setStatus(401);
response.getWriter().write
```