Redis实战篇Day01(短信登录篇)
前言:
在学习苍穹外卖时,我们引入了redis,了解了下它在Java中的应用。而从今天开始,我们将开始真正以项目的方式(黑马点评)深入学习redis,以及如何使用它完成各项接口功能
今日所学:
- 导入黑马点评项目
- session短信登录
- redis代替session短信登录
目录
1. 导入黑马点评项目
1.1 导入数据库
1.2 导入项目代码
1.3 修改项目配置(端口)
1.4 导入前端代码(配置端口)
2. session短信登录
需求分析
代码实现
2.1 发送短信验证码
2.2 短信验证码登录以及注册
2.3 校验登录状态(拦截器)
思考
3. redis替代session进行短信登录
需求分析:
代码实现:
3.1 发送验证码代码修改
3.2 登录注册代码修改
3.3 校验登录状态(拦截器优化)
问题:
最后:
1. 导入黑马点评项目
1.1 导入数据库
打开黑马学习资料,在mysql中导入hmdp.sql文件
导入完后hmdp数据库一共有11个表
1.2 导入项目代码
复制hm-dianping文件夹,给他放在一个无中文的目录下,我这给它放置在D:/software文件下
使用你亲爱的ideal打开这个文件,打开项目结构,给你的JDK版本调低一点,这里调成18的即可
修改完不要忘了点击应用,再点击确定
之后我们可以使用maven编译(compile)下,正常不报错就是项目导入成功
1.3 修改项目配置(端口)
更改数据库和redis的连接配置
一般来说把这四样东西修改成自己的就行了(如果你没有修改redis端口的话)
运行项目,如果项目报这个错误
那就是你的server所需的8081端口被占用了
这里注意千万不要去随便修改server端口,比如说什么改成8082,这样后续前端界面是显示不出来的。
保持8081端口,打开你的cmd,输入以下指令,查看是哪个应用占据了你的8081端口(这里主要找到PID)
netstat -ano | findstr :"8081"
接着Ctril+shift+ESC打开你的任务管理器
输入相应的PID,找到该应用,右键点击结束任务。
重新运行,后端项目是可以正常启动的。
这里可以打开你的浏览器,输入localhost:8080/shop-type/list,如果能正常返回结果,即代表着运行成功
1.4 导入前端代码(配置端口)
复制黑马学习资料的前端文件(nginx -1.18),同样给它放置在无中文结构的目录下,这里我依旧放在D:/software/)
打开该文件,双击启动(或者该目录下cmd中输入start nginx.exe)
打开浏览器,输入localhost:8080,正常就可以打开项目了(记得给后端项目打开)
如果打不开,可以看下是否是nginx端口冲突的问题
找到error.log文件
翻到最后,查看错误日志,可以看到输出了以下问题
扔给AI,说的是nginx端口冲突
打开cmd,输入指令查看该端口
在任务管理器中,输入PID
可以看到还是nginx,exe(我估摸着是苍穹外卖的nginx忘关了),右键点击结束任务。
重新启动项目
可以看到以成功输出出来了
2. session短信登录
需求分析
业务规则:
基于Session实现短信验证码登录,注册以及校验
逻辑流程图
代码实现
再讲代码实现之前,我们先讲下service层的这个继承关系。这个继承关系是Java中常见的Service层实现方式,结合了Mybatis-Plus框架的特性。其中ServiceImpI是Mybatis-plus提供的通用服务实现基类,内置了大量常用的CRUD方法,通过继承他,就能轻易的去调用这些方法,不需要再格外写mapper层
再加上Controller层基本格式已经写好了(请求路径,请求方式),因此我们只需要关注于service层的代码逻辑实现就好了
2.1 发送短信验证码
具体逻辑思路(依照上面所展示的逻辑流程图):
1.对前端传来的手机号进行校验(使用正则表达式,已经封装到RegexUtils类中)
2.不合格直接返回校验信息
3.合格的话随机生成六位验证码
4.并将校验码保存到session中(方便后续的登录校验)【这里存储到redis不用管,是后面的内容,下面功能也一样】
5.控制台输出验证码
@Override public Result sendCode(String phone, HttpSession session) {// 1.校验手机号if(RegexUtils.isPhoneInvalid(phone)){// 2.如果不符合,返回校验信息return Result.fail("手机号格式错误");}// 3. 符合,生成校验码String code = RandomUtil.randomNumbers(6);// 4.保存验证码到sessionsession.setAttribute("code", code);// 保存到redis// stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);// 5.发送验证码log.info("发送短信验证码成功,验证码;{}",code);// 6.返回okreturn Result.ok(code); }
2.2 短信验证码登录以及注册
具体逻辑思路(依据上述逻辑流程图):
1.获得前端所传的手机号,进行格式校验
2.获取前端的验证码,和保存在session的验证码,进行equals()校验
3.如果手机号无问题,验证码一致的话,查询数据库,判断用户是否存在
这里直接调用继承于ServiceImpl类的查询方法,query()相当于是select * from user,eq()相当于where phone = ?, one()指的是查询一条记录。
4.如果用户不存在,则创建
5.最后添加用户信息到session(校验登录状态时要用)
@Override
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)){
//3.不一致,报错
return Result.fail("验证码错误");
}
//一致,根据手机号查询用户
User user = query().eq("phone", phone).one();
//5.判断用户是否存在
if(user == null){
//不存在,则创建
user = createUserWithPhone(phone);
}
//7.保存用户信息到session中
session.setAttribute("user",user);
return Result.ok();
}
2.3 校验登录状态(拦截器)
具体执行逻辑(依据逻辑流程图):
1.配置拦截器代码
- 获取前端请求携带的session
- 获取其中的用户
- 判断用户是否存在,不存在返回401
- 存在,则将用户信息保存在ThreadLocal,方便获取
2.配置拦截器,使其生效
- 在MVCConfig类中,ait+insert重写addInterceptors()方法
- excludePathPatterns排除一些不需要登录就能查看的功能
- 其他功能查看前都要进行拦截做一个登录校验
拦截器代码
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((User)user);
//6.放行
return true;
}
}
让拦截器生效
```java
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 登录拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login"
).order(1);
// token刷新的拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}
思考
和苍穹外卖登录校验的比较:
一个是将数据储存在JWT令牌中,一个是储存在session,除此之外,并无什么区别
3. redis替代session进行短信登录
需求分析:
集群的session具有数据共享问题
多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。
因此集群时我们需要使用另一种东西去替代session,这个东西应当具有
1.数据共享
2. 内存储存
3.key value结构
显而易见,redis集群很好的满足了这一点
代码实现:
3.1 发送验证码代码修改
基本逻辑保持不变,在保存验证码那给验证码保存到redis中就行,这里记得保存时设置有效期,实现一个数据自动清理的效果
@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);// 保存到redisstringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES); // 5.发送验证码log.info("发送短信验证码成功,验证码;{}",code);// 6.返回okreturn Result.ok(code); }
3.2 登录注册代码修改
基本逻辑保持不变,主要的修改处有两点:
- 从session中获取验证码改为从redis中获取验证码
- 将数据保存在redis中,并给前端返回token
第二点修改中,我们使用hash结构去储存数据,随机生成一个token作为key值,将UserDTO作为一个(filed-value)值,那么为什么使用token作为key值呢,使用phone不行吗(也是唯一的),技术上是可以的,但是将phone这样一个敏感数据作为key值并返回给前端页面终究是不合适的
此外,在将UserDTO进行map转换的时候不要换了给value值从Long类型转换成string类型,不然在储存数据到redis时会有一个类型不兼容的问题
最后,不要忘了给储存数据设置有效期(跟JWT令牌的token是一样的),以便当用户超出一段时间没有访问网站时,会自动登出。后续要访问,得重新登录
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.从redis获取验证码并校验
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.equals(code)) {
// 不一致,报错
return Result.fail("验证码错误");
}
// 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
User user = query().eq("phone", phone).one();
// 5.判断用户是否存在
if (user == null) {
// 6.不存在,创建新用户并保存
user = createUserWithPhone(phone);
}
// 7.保存用户信息到 redis中
// 7.1.随机生成token,作为登录令牌
String token = UUID.randomUUID().toString(true);
// 7.2.将User对象转为HashMap存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// 7.3.存储
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 7.4.设置token有效期
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.返回token
return Result.ok(token);
}
3.3 校验登录状态(拦截器优化)
在上面我们讲了,设置token有效期时为了超出一段时间没有访问的话则自动登出。但是这里有个问题是我们并没有给他设置时间刷新机制,也就是说,不管用户有没有持续访问,时间到了都会自动登出,这显然是不行的。那么如何设置时间刷新机制呢,我们可以很自然想到拦截器,设置一个拦截器,对每个功能进行拦截,只要访问,就刷新token有效期。
这里我们重新定义一个拦截器(拦截所有功能),跟前面定义的登录拦截器区分开,在这个拦截器中,如果没有查到token或者用户,return true放行(说明是未登录下查看首页等功能,这个不用刷新有效期,也不用return 401啥的)。如果查到了用户,则刷新。最后将用户信息保存在ThreadLocal中
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 (StrUtil.isBlank(token)) {
return true;
}
// 2.基于TOKEN获取redis中的用户
String key = LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
// 3.判断用户是否存在
if (userMap.isEmpty()) {
return true;
}
// 5.将查询到的hash数据转为UserDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 6.存在,保存用户信息到 ThreadLocal
UserHolder.saveUser(userDTO);
// 7.刷新token有效期
stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserHolder.removeUser();
}
}
在原先的这个拦截器中,只要使用LocalThread尝试获取用户信息,如果为null,输出401
**LoginInterceptor**
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;
}
}
最后把拦截器配置改下
使用order配置拦截顺序(低值优先)
@Configuration public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 这里LoginInterceptor是手动new的,不受IOC容器去管理registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/shop/**","/voucher/**","/shop-type/**","/upload/**","/blog/hot","/user/code","/user/login").order(1);registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);} }
问题:
在配置拦截器的时候,我遇到了一个空指针异常问题
原因是因为我给LoginInterceptor交给IOC容器去管理了
使用@Resource注入stringRedisTemplate,让它可以在config类中不用传stringredistemplate就可以实现拦截
但是这样是不行的,因为我们拦截器是new 出来的,并不受IOC容器去管理
最后:
今天的分享就到这里。如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!(๑`・ᴗ・´๑)