欢迎来到"雪碧聊技术"CSDN博客!
在这里,您将踏入一个专注于Java开发技术的知识殿堂。无论您是Java编程的初学者,还是具有一定经验的开发者,相信我的博客都能为您提供宝贵的学习资源和实用技巧。作为您的技术向导,我将不断探索Java的深邃世界,分享最新的技术动态、实战经验以及项目心得。
让我们一同在Java的广阔天地中遨游,携手提升技术能力,共创美好未来!感谢您的关注与支持,期待在"雪碧聊技术"与您共同成长!
目录
2、如何解决上述问题?答:使用redis代替session。
②用户输入手机号、验证码,请求短信验证登录。后端从redis中取出正确的验证码,与用户输入的验证码核对。
①如果验证该手机号、验证码都没毛病,证明应该让人家登录。此时就需要为该用户生成一个token(唯一的),并以token为key,用户信息为value,存入redis中。
②最关键的一步:把上面生成的token,返回到客户端,客户端再将该token保存到本地,作为以后登录、访问其他后端接口的令牌!
③以后客户端访问任何后端接口,都要携带token,经过后端拦截器的许可后,才能得到后端服务。
③如果验证码一致,则校验登录成功。下面就需要为用户生成jwt令牌(此处用UUID来生成),然后将jwt令牌(作为键)和用户信息(作为值)存入redis中。
④别忘了将该token返回给客户端,让客户端存起来,以后每次请求都要携带该token,来通过后端拦截器的拦截,从而获取后端服务。
⑤重新编写拦截器,从redis中获取token,看看前端是否拥有token令牌,从而决定是否应该拦截该请求
一、短信登录3-集群的session共享问题
1、什么是集群的session共享问题?
当项目足够大时,一台Tomcat服务器的压力比较大,因此就会水平拓展出很多台Tomcat服务器,然后nginx服务器再进行负载均衡,将前端的请求合理地分配给后端的多台Tomcat服务器。
此时就会出现Session共享问题:多台Tomcat服务器之间,并不共享session存储空间,当前端请求被nginx服务器分配到不同的tomcat服务器时会导致数据丢失。
举例:当我填写手机号,要求后端生成一个验证码时,此时后端就会生成一个验证码,并存放到session中。
然后用户的手机短信,会收到刚刚后端生成的验证码"043018",然后会输入到页面上,在请求验证码登录:
然后后端会拿出刚刚在session中存入的验证码,和用户现在输入的验证码进行对比,如果一致,说明确实是该手机号的主人,如果不一致,则说明不是本人。
那此时如果该请求被nginx服务器,分配到了其他Tomcat服务器,此时由于不同的Tomcat服务器之间是不共享session的,所以后端此时就无法拿到刚刚存入session中的正确验证码,因此就无法验证登录。这就叫"集群的session共享问题"。
2、如何解决上述问题?答:使用redis代替session。
此时我们用redis来存储用户信息,如:验证码。
此时就能解决不同的Tomcat的session共享问题了。
二、短信登录4-基于Redis实现共享session登录
1、使用Redis,存储验证码
①用户输入手机号,请求获取验证码
然后后端将该验证码,存入redis中,以手机号为键,验证码为值,如下:
②用户输入手机号、验证码,请求短信验证登录。后端从redis中取出正确的验证码,与用户输入的验证码核对。
2、如何将用户信息存入Redis中(关键※)
思路:
①如果验证该手机号、验证码都没毛病,证明应该让人家登录。此时就需要为该用户生成一个token(唯一的),并以token为key,用户信息为value,存入redis中。
②最关键的一步:把上面生成的token,返回到客户端,客户端再将该token保存到本地,作为以后登录、访问其他后端接口的令牌!
举例:
③以后客户端访问任何后端接口,都要携带token,经过后端拦截器的许可后,才能得到后端服务。
3、根据1和2的逻辑,完成具体的代码开发
①完成验证码存储到redis中
验证功能:
后端生成验证码947864
去redis中查看,是否存入了该验证码?
此时就完成了验证码存储到redis中。
②使用redis,完成登录验证功能
③如果验证码一致,则校验登录成功。下面就需要为用户生成jwt令牌(此处用UUID来生成),然后将jwt令牌(作为键)和用户信息(作为值)存入redis中。
上面设置了token的有效期,时长为30分钟。也就是说,你登录成功并进入内部页面,30分钟回来以后,会显示你登录过期,请重新登陆,很多应用都是这么做的。
④别忘了将该token返回给客户端,让客户端存起来,以后每次请求都要携带该token,来通过后端拦截器的拦截,从而获取后端服务。
⑤重新编写拦截器,从redis中获取token,看看前端是否拥有token令牌,从而决定是否应该拦截该请求
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、获取session(废弃)
//HttpSession session = request.getSession();
//1、获取请求头中的token(前端携带过来的)
String token = request.getHeader("authorization");
if(StrUtil.isBlank(token)){//判断token是否为空
//token为空,拦截,返回401状态码
response.setStatus(401);
}
//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、将从redis查询到的hash数据,转为UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//6、存在,保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);
//7、刷新token的有效期
stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
//8、放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//从ThreadLocal中,移除用户
//这里是避免内存泄漏:防止ThreadLocal数据明明不再使用,却一直占用JVM内存,这就叫内存泄露
UserHolder.removeUser();
}
}
注意:在拦截器中,无法注入stringRedisTemplate对象,因为这个拦截器类是我们自定义的,因此无法交给Spring管理,因此只能通过构造器的方式来获取stringRedisTemplate对象。那么谁来给这个拦截器类的构造器传入一个stringRedisTemplate对象呢?那就是配置类,因为配置类需要使用拦截器对象,如下:
⑥重启后端项目,查看运行效果
查看后端报错:
⑦解决stringRedisTemplate的类型转换异常
这里的userMap在本质上,就是一个userDTO对象。
查看UserDTO类的构成:
因为我们的StringRedisTemplate有一个特点,就是往redis中存数据时,要求key和value都得是String类型的。下面的源码:
可见上述就是Long类型的id,转为String类型的过程中出现了异常。
解决方案:若还想继续使用StringRedisTemplate,就必须自己手动转,如下:
⑧解决上述异常后,再次启动项目,查看运行效果:
可见此时就登入进来了,下面查看一下redis中是否有该用户的token和用户信息:
4、优化登录状态刷新问题
①什么是登录状态刷新?
就是每次请求时,都要刷新redis中的token的有效期。
如果不刷新,那么redis中的有效期,就会一直是在登录时给的那30分钟,若这期间我经常访问该项目,证明我一直在,那么就应该刷新这个token的有效期。
②那目前的登录状态刷新有什么缺点?
没有被拦截的请求路径,如下:
此时这些请求路径,由于根本不执行拦截器的代码,因此根本没机会执行拦截器中刷新redis的token有效期的代码,因此是不合理的。
比如:我一直在访问首页,而这个首页就不是拦截器拦截的范围,因此不走拦截器,于是就不会刷新token的有效期。
③如何优化这个问题?
在原有的拦截器前面,再加一个拦截器,专门用来拦截全部的请求,并刷新token的有效期,此时就完美了,如下:
具体操作如下:
编写代码:
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、获取session(废弃)
//HttpSession session = request.getSession();
//1、获取请求头中的token(前端携带过来的)
String token = request.getHeader("authorization");
if(StrUtil.isBlank(token)){//判断token是否为空
return true;//此处不拦截,第二个拦截器也会拦截下来,因为还没往ThreadLocal中存用户信息呢!
}
//2、根据该token,获取redis中的用户
String key = RedisConstants.LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
.entries(key);
//3、判断用户是否为空
if(userMap.isEmpty()){
return true;//此处不拦截,第二个拦截器也会拦截下来,因为还没往ThreadLocal中存用户信息呢!
}
//5、将从redis查询到的hash数据,转为UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//6、存在,保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);
//7、刷新token的有效期
stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
//8、放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//从ThreadLocal中,移除用户
//这里是避免内存泄漏:防止ThreadLocal数据明明不再使用,却一直占用JVM内存,这就叫内存泄露
UserHolder.removeUser();
}
}
修改原来的拦截器:
java
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1、判断是否需要拦截(依据是ThreadLocal中是否有用户信息)
if(UserHolder.getUser() == null){
//ThreadLocal中没有用户信息,需要拦截
response.setStatus(401);
return false;
}
//由用户,则放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//从ThreadLocal中,移除用户
//这里是避免内存泄漏:防止ThreadLocal数据明明不再使用,却一直占用JVM内存,这就叫内存泄露
UserHolder.removeUser();
}
}
然后再修改下配置类,配置一下这两个拦截器的拦截范围、执行先后顺序等:
java
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
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);
}
}
其中的order,用来指定该拦截器的执行顺序,数字越小,越先执行。
addPathPatterns("/**")方法,表示新添加的拦截器,要拦截全部的请求,从而进行redis中的token有效期的刷新操作。
④重新启动项目,查看运行效果
我们登录成功后,查看此时redis中的token有效期是多少:
下面我们访问一下前端那些没有被拦截的路径,如:首页,看看这个token的有效期会不会刷新:
此时,我们的登录状态刷新就得到了优化,即:访问原先没有被拦截的请求路径,也会刷新redis中的token有效期。
以上就是本篇文章的全部内容,如果感兴趣,请关注本博主吧~~