1.绪论
redis是一款高性能的缓存框架,那它在实际开发过程中有哪些作用呢?今天我们就来聊一聊。
2.session共享
2.1 问题描述
现在所有的网页都有一个功能,就是在用户第一次登陆成功过后,再次进入到这个页面,就不用二次登陆了,那这种场景式如何实现的呢?
2.2 传统登录流程
传统方式是在用户登录过后,用户会校验登录是否成功,如果登录成功,会将用户信息保存到session中,后面用户再次访问的时候,会通过cookie的方式将用户的sessionid携带过来,如果用户在session中根据sessionId查询到用户信息,便表示当前用户已经登录过。步骤如下:
2.2.1 发送短信验证码
发送短信验证码:
1.用户提交手机号,点击登录;
2.查询数据库,校验用户是否存在;
3.如果用户不存在,生成验证码,并将验证码保存到session中;
4.给用户返回验证码
public Result sendCode(String phone, HttpSession session) {
//1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
//2.生成验证码
String code = RandomUtil.randomNumbers(6);
//3.将验证码放入到session中
session.setAttribute("code", code);
//4.给用户发送验证码
log.debug("用户验证码为;{}", code);
//5.返回成功
return Result.ok();
}
2.2.2 登录注册
用户登录注册流程:
1.用户填写手机号和验证码;
2.在session中根据前端携带的cookie中的sessionId查询到当前用户的验证码,并于用户的验证码进行比对;
3.验证通过,根据手机号查询数据库,如果数据库存在该用户,便将用户保存到session中;
4.如果用户不存在,便创建用户并插入到数据库,同时保存到session中;
public Result login(LoginFormDTO loginForm, HttpSession session) {
String phone = loginForm.getPhone();
//1.校验手机号
if (RegexUtils.isPhoneInvalid(loginForm.getPhone())) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 2.获取缓存中的验证码
String code = loginForm.getCode();
Object codeCache = session.getAttribute("code");
//3.比较验证码是否正确
if (codeCache == null || !code.equals(codeCache)) {
return Result.fail("登录失败");
}
//4.如果比对通过,查询数据库
User user = query().eq("phone", phone).one();
if (user == null) {
//5.未查询到用户,表示第一次注册,直接新建一个用户并插入
User user = new User();
user.setPhone(phone);
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
// 2.保存用户
save(user);
return user;
}
//5.保存到session中
session.setAttribute("user", user);
return Result.ok();
}
2.2.3 再次登录
用户再次登录流程:
1.用户调用其他接口,会携带cookie;
2.根据cookie中的sessionId在session中查询;
3.如果session中存在用户,便将用户封装到ThreadLocal中,调用接口;
4.如果不存在用户,便返回登录失败;
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;
}
}
2.3 传统登录流程的缺点
1.用于用户信息是存储在session中的,而session是属于一个Tomcat实例,如果采用分布式部署的话,当请求到tomcat1的话,保存到tomcat1的话,下次登录请求到tomcat2的话,便查询不到用户信息。所以如何将用户信息同步到整个集群,便成了一个问题。
2.4 基于redis实现的登录
redis的登录其实就是将用户信息放到redis中,下次登录的时候,直接从redis的中获取。
2.4.1 发送短信验证码
发送短信验证码:
1.用户提交手机号,点击登录;
2.查询数据库,校验用户是否存在;
3.如果用户不存在,生成验证码,并将验证码保存到redis中;
4.给用户返回验证码
public Result sendCode(String phone) {
//1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
//2.生成验证码
String code = RandomUtil.randomNumbers(6);
//3.将验证码放入到redis中
stringRedisTemplate.opsForValue().set("code:" + phone, code);
//4.给用户发送验证码
log.debug("用户验证码为;{}", code);
//5.返回成功
return Result.ok();
}
2.4.2 登录注册
用户登录注册流程:
1.用户填写手机号和验证码;
2.在redis中根据用户的手机号作为key查询,查询到用户的验证码,并校验验证码
3.验证通过,根据手机号查询数据库,如果数据库存在该用户,便生成一个token用来标识该用户,并且将其作为key,user信息作为value存储到redis中。
4.如果用户不存在,便创建用户并插入到数据库,同时保存到redis中;
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);
}
注:为什么这里要生成一个sessionId作为唯一标识。原因前端请求用户信息的时候,会将token放到请求头中,如果是将phone放到请求头中,会有安全问题。
2.4.3 再次登录
1.获取到用户的token信息
2.根据token从redis中取出userInfo信息
3.如果userInfo信息不存在,便登录失败
4.如果userInfo信息存在,便重新设置toke在redis中的存活时间,并且将userInfo存储到ThreadLocal中。
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;
}