本文记录在学习Redis过程中的关键技术实践与踩坑思考,包含个人在实际学习中的具体过程 与遇到的问题 以及知识点总结
希望可以给一起学习的大家带来帮助ヽ( ̄▽ ̄)ノ
文章目录
黑马点评
项目简介

1.导入基础代码
该项目需要JDK的版本必须为1.8,没装JDK1.8的可以直接在IDEA里找到Project Structure选择SDK版本时点击Download下载1.8版本

2.修改application.yaml文件,把相关信息改成自己的
3.启动项目,在浏览器地址栏输入localhost:8081/shop-type/list

证明代码导入成功
1.短信登录
1.1基于session实现短信登录的流程

1.2发送短信验证码
快速找到接口的实现类:选中接口点击ctrl+alt+B
UserController
实现接口的开发
java
/**
* 发送手机验证码
*/
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
//发送短信验证码并保存验证码
return userService.sendCode(phone,session);
}
IUserService
java
public interface IUserService extends IService<User> {
Result sendCode(String phone, HttpSession session);
}
UserServiceImpl
java
/**
* 发送手机验证码
* @param phone
* @param session
* @return
*/
@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);
//5.发送验证码
log.debug("发送短信验证码成功,验证码:{}",code);
//返回ok
return Result.ok();
}
1.3短信验证码登录和注册
此项目使用了Mybatisplus

@TableName注解的核心作用就是告诉 MyBatis-Plus (MP):这个 Java 类对应数据库里的哪张表
由于项目继承了Mybatisplus提供的ServiceImpl,所以可以直接实现简单的单表查询:
User user = query().eq("phone", phone).one();等同于select * from tb_user where phone = ?
完整代码:
UserController
java
/**
* 登录功能
* @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
*/
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
//实现登录功能
return userService.login(loginForm,session);
}
UserServiceImpl
java
**
* 登录 + 注册
* @param loginForm
* @param session
* @return
*/
@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("code");
String code = loginForm.getCode();
if(cacheCode == null || !cacheCode.toString().equals(code)) {
//不一致,报错
return Result.fail("验证码错误");
}
//3.根据手机号查询用户
User user = query().eq("phone", phone).one();
//4.判断用户是否存在
if(user == null) {
//5.不存在则创建新用户并保存
user = cresteUserWithPhone(phone);
}
//6.保存用户信息到session
session.setAttribute("user",user);
return Result.ok();
}
private User cresteUserWithPhone(String phone) {
//1.创建用户
User user = new User();
user.setPhone(phone);
user.setNickName("user_" + RandomUtil.randomString(10));
//2.保存用户到数据库
save(user);//Mybatisplus
return user;
}
1.4登录校验拦截器
在Util包下新建LoginInterceptor作为登录校验时的拦截器
Ctrl + i实现重写方法
java
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){
//不存在,拦截
response.setStatus(401);
return false;
}
//4.保存用户信息到ThreadLocal
UserHolder.saveUser((User)user);
//5.放行
return true;
}
}
配置拦截器
java
@Configuration
public class MvcConfig implements WebMvcConfigurer {
//添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/user/login",
"/user/code",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/voucher/**"
);
}
}
获取当前登录用户在UserController中增加
java
@GetMapping("/me")
public Result me(){
//获取当前登录的用户并返回
User user = UserHolder.getUser();
return Result.ok(user);
}
1.5隐藏用户敏感信息
修改一下UserServiceImpl里的登录注册
java
//6.保存用户信息到session
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
只保存
java
@Data
public class UserDTO {
private Long id;
private String nickName;
private String icon;
}
1.6集群的session共享问题
session的共享问题:多态Tomcat并不共享session存储空间,当请求切换到不同的Tomcat服务时会导致数据丢失的问题

解决方法:
session替代方案,需满足:
- 数据共享
- 内存存储
- key、value结构
使用Redis代替session
1.7基于Redis实现共享session登录

先注入Redis的API
java
@Autowired
private StringRedisTemplate stringRedisTemplate;
修改发送验证码,把第四步改为把验证码存入Redis,存入时设置有效期2minutes
java
/**
* 发送手机验证码
* @param phone
* @param session
* @return
*/
@Override
public Result sendCode(String phone, HttpSession session) {
//1.校验手机号
if(RegexUtils.isPhoneInvalid(phone)) {
//2.若不符合,返回错误信息
return Result.fail("手机号格式错误");
}
//3.符合,生成验证码
String code = RandomUtil.randomNumbers(6);
//4.保存验证码到Redis
stringRedisTemplate.opsForValue().set("login:code:" + phone, code,2, TimeUnit.MINUTES);
//5.发送验证码
log.debug("发送短信验证码成功,验证码:{}",code);
//返回ok
return Result.ok();
}
修改登录注册
java
/**
* 登录 + 注册
* @param loginForm
* @param session
* @return
*/
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1.校验手机号
String phone = loginForm.getPhone();
if(RegexUtils.isPhoneInvalid(phone)) {
//若不符合,返回错误信息
return Result.fail("手机号格式错误");
}
//2.校验验证码 从Redis里取
String cacheCode = stringRedisTemplate.opsForValue().get("login:code:" + phone);
String code = loginForm.getCode();
if(cacheCode == null || !cacheCode.equals(code)) {
//不一致,报错
return Result.fail("验证码错误");
}
//3.根据手机号查询用户
User user = query().eq("phone", phone).one();
//4.判断用户是否存在
if(user == null) {
//5.不存在则创建新用户并保存
user = cresteUserWithPhone(phone);
}
//6.保存用户信息到redis
//6.1随机生成token,作为登录令牌
String token = UUID.randomUUID().toString();
///6.2将UserDTO对象转为Hash存储
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()));
stringRedisTemplate.opsForHash().putAll("login:token:" + token, userMap);
//设置有效期
stringRedisTemplate.expire("login:token:" + token, 30, TimeUnit.MINUTES);
return Result.ok();
}
修改登录拦截器
java
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)){
//不存在,拦截
response.setStatus(401);
return false;
}
//2.基于token获取Redis中的用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("login:token:" + token);
//3.判断用户是否存在
if(userMap.isEmpty()){
//不存在,拦截
response.setStatus(401);
return false;
}
//将查询到的Hash数据转化为UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//4.保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);
//5.刷新token有效期
stringRedisTemplate.expire("login:token:" + token, 30, TimeUnit.MINUTES);
//6.放行
return true;
}
}
1.8登录拦截器的优化
解决状态登录刷新的问题:因为当前拦截器只拦截部分请求,进行不被拦截的请求时就不刷新token的有效期
此时只需再增添一个拦截器拦截所有路径

增加 RefreshToken拦截器
java
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中的用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("login:token:" + token);
//3.判断用户是否存在
if(userMap.isEmpty()){
//不存在,拦截
response.setStatus(401);
return false;
}
//将查询到的Hash数据转化为UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//4.保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);
//5.刷新token有效期
stringRedisTemplate.expire("login:token:" + token, 30, TimeUnit.MINUTES);
//6.放行
return true;
}
// @Override
// public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
// }
}
修改Login拦截器,只需要判断用户是否存在
java
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;
}
}
配置拦截器
把RefreshToken拦截器加进去
java
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
配置拦截器顺序
在Login拦截器后加.order(1);
在RefreshToken拦截器后加.order(0);
