Session登录

黑马点评登录

第一次请求:获取验证码

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.

缺点

  1. 在高并发的情况下,由于服务器端需要存储许多User对象,那么占用资源太多。
  2. 扩展性差。如果采用分布式集群部署,可能只在一台服务器中存储用户信息。如果负载均衡使得下一次请求被分配到其它服务器,那么就会判断该用户没有登录。Session 粘滞(Sticky Session):通过负载均衡策略(如 IP 哈希)让同一用户的请求始终路由到同一服务器(治标不治本)
  3. 跨域请求。

基于 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

登录拦截器

Alt text 使用两个拦截器。

第一个拦截器拦截所有请求,可以用来刷新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 里面没有那个用户,就返回错误。