黑马点评登录
第一次请求:获取验证码
1
2
3
4
| @PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginFormDTO, HttpSession session){
return userService.login(loginFormDTO, session);
}
|
后端为其生成随机验证码,并且存入 session
1
| session.setAttribute("code", code);
|
后端接着把Session ID通过 set cookie返回给前端浏览器。浏览器保存这个 Session ID, 把它存在 cookie 里面。
第二次请求:登录
前端发送request,request header的cookie里面有这样一个字段,代表Session ID.
JSESSIONID=C53073A1EAAB9BBE8E78256DF1E48AE7
1
| Object cachedCode = session.getAttribute("code");
|
后端从 Session 中获取验证码,并判断与请求体携带的验证码是否一致。
登录状态维护
实现 Interceptor里 preHandle功能,对请求拦截。在MVC Config里面注册这个Interceptor。
判断用户是否存在,如果存在,则放行,并把该用户放进ThreadLocal.
缺点
- 在高并发的情况下,由于服务器端需要存储许多User对象,那么占用资源太多。
- 扩展性差。如果采用分布式集群部署,可能只在一台服务器中存储用户信息。如果负载均衡使得下一次请求被分配到其它服务器,那么就会判断该用户没有登录。Session 粘滞(Sticky Session):通过负载均衡策略(如 IP 哈希)让同一用户的请求始终路由到同一服务器(治标不治本)
- 跨域请求。
基于 Redis 实现共享 Session 登录
在Spring启动的时候,会读取Redis配置,并且创建一个bean
1
2
| @Resource
private StringRedisTemplate stringRedisTemplate;
|
在登录服务里,stringRedisTemplate被注入,然后校验验证码、生成token,并把token作为key,user的详细信息作为value存入Redis 中。
首先在校验验证码的时候,用手机号作为 Key,存储 code。
验证码:login:code:{phone}
接着保存用户信息到Redis中。为用户的这次登录生成一个token,作为Key,用户的信息转换成Hash,存储到Redis中。
用户信息:login:token:{token}
利用 Interceptor,每次有请求发送过去被拦截的时候,就更新一次 token 有效期.
1
| stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
|
1
2
3
| redis-cli
127.0.0.1:6379> keys login*
1) "login:token:6cc2265d09a24ceeb182cf2674f9fa84"
|
可以查看现在登录的用户的 token,发现与前端请求头里的 token 符合
authorization:
6cc2265d09a24ceeb182cf2674f9fa84
登录拦截器
使用两个拦截器。
第一个拦截器拦截所有请求,可以用来刷新Token。如果在Redis里查到该请求的用户,就把它放在ThreadLocal里面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| 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=RedisConstants.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, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
|
第二个拦截器拦截那些需要登录的请求,如果 ThreadLocal 里面没有那个用户,就返回错误。