分布式系统的Session怎么处理的?

798 阅读4分钟

只要是当下互联网公司,就要用到分布式Session,哪怕很小的创业公司Session总要用到。下面总结常见的三种session使用方式,记录以方便自己后期回顾,好记性不如烂笔头。。。
了解Session,必须要知道Cookie,在此不做讨论。

常见的三种Session解决方案:

1.Tomcat + Redis方式

这种方式是在Tomcat容器context.xml文件配置RedisSessionManager,具体配置网上很多。
传送门:[blog.csdn.net/qq_35830949…]
这个的不好处就是和Tomcat容器耦合太严重,如果要是后面服务器容器要切换到Jetty,那这个不是很蛋疼,一般这种目前使用很少,除非是单台服务器玩玩而已。

2.Spring全家桶中的Spring Session + Redis

这个方式目前使用比较多,看SpringSession github代码维护也很积极。
和上面的Tomcat容器中配置的session管理方式比较:

SpringSession原理简单理解就是:
将Session从web容器中拆出,存储在独立的存储服务器中。
目前支持多种形式的Session存储服务,如Redis、Database、MogonDB等。Session的管理责任委托给SpringSession承担。
当用户request请求进入web容器,根据request获取session时,由SpringSession负责从存储器如Redis中获取Session,如果存在则返回,如果不存在则创建并持久化至Redis存储中。 用户每次请求登录时将session写入Redis,在用户登出时自动删除。这个过程是SpringSession代理负责。

SpringSession官网维护比较活跃,不用担心不安全,有其他问题官方也会积极fix。下图为证:

使用方式也很简单,下面贴下我Demo中的代码: 使用[start.spring.io/]创建一个SpringBoot,依赖下面的jar

  <dependencies>
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
          <groupId>org.springframework.session</groupId>
          <artifactId>spring-session-data-redis</artifactId>
      </dependency>
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-tomcat</artifactId>
         <scope>provided</scope>
     </dependency>
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
         <scope>test</scope>
     </dependency>

     <dependency>
         <groupId>redis.clients</groupId>
         <artifactId>jedis</artifactId>
         <version>2.9.0</version>
     </dependency>
 </dependencies>

SpringBoot项目application.properties中配置SpringSession连接Redis:

server.port=9099

# Session store type.
spring.session.store-type=redis
# Session timeout. If a duration suffix is not specified, seconds is used.
server.servlet.session.timeout=7200
# Sessions flush mode.
#session更新策略,有ON_SAVE、IMMEDIATE,
# 前者是在调用#SessionRepository#save(org.springframework.session.Session)时,
# 在response commit前刷新缓存,#后者是只要有任何更新就会刷新缓存
spring.session.redis.flush-mode=on-save
# Namespace for keys used to store sessions.
spring.session.redis.namespace=spring:session

# Redis server host.
spring.redis.host=localhost
# Login password of the redis server.
spring.redis.password=
# Redis server port.
spring.redis.port=6379
spring.redis.jedis.pool.max-active=100
spring.redis.jedis.pool.max-wait=10
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.min-idle=10

开启Redis Http SpringSession

/**
 * 开启Redis Http Session
 */
@Configuration
@EnableRedisHttpSession
public class RedisHttpSessionConfiguration {
}

写个Controller测试一下

@RestController
public class SessionController {

    @RequestMapping("/session")
    public Object springSession(@RequestParam("username") String username,
                                HttpServletRequest request, HttpSession session) {
        Cookie[] cookies = request.getCookies();
        if (cookies != null && cookies.length > 0) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().contains("JSESSION")) {
                    System.out.println(cookie.getName() + "=" + cookie.getValue());
                }
            }
        }

        Object value = session.getAttribute("username");
        if (value == null) {
            System.out.println("用户不存在");
            session.setAttribute("username", "{username: '" + username + "', age: 28}");
        } else {
            System.out.println("用户存在");
        }

        return "username=" + value;
    }
}

最后Redis工具查看一下数据:

3.JWT(json web token) + Redis方式

具体的底层原来在官网上有:[jwt.io/]。我这里不做说明。只是使用她的思想。
目前使用的token Redis简单实现版本。
这种方式的实现思路:

(1) 用户登录系统的时候生成一个token

Token=uuid.toString().replact("-","")
然后调用redisClient写入redis,并json返回body{token:"xxxxx222","其他字段":"其他字段"}

redisClient.set("user:id:" + token, userId, timeInterval);
(2) 用户登录访问其他接口注解+拦截器
/**
* 添加此注解的接口不进行token验证,直接访问后台服务
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginIgnore {
    boolean value() default true;
}

下面是拦截器实现:

@Slf4j
@Component
public class LoginInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private UserService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod method = (HandlerMethod) handler;
            LoginIgnore loginIgnore = method.getMethodAnnotation(LoginIgnore.class);
            if (Objects.nonNull(loginIgnore)) {
                return true;
            }
            String token = "";
            //请求httpRequest头中遍历获取token
            Enumeration<String> enumeration = request.getHeaderNames();
            while (enumeration.hasMoreElements()) {
                String name = enumeration.nextElement();
                if ("token".equals(name.toLowerCase())) {
                    token = request.getHeader(name);
                }
            }
            //校验是否已登录及token是否过期,如果过期,则不让访问接口
            if ("".equals(token) || userService.isTokenExpire(token)) {
                log.error("校验token如果失败,token:{},methodName:{}", token, method.getMethod().getName());
                this.sendResponse(response,  "登录失效");
                return false;
            }
            Integer userId = userService.getUserIdByToken(token);
            if (Objects.isNull(userId)) {
                this.sendResponse(response, "登录失效");
                return false;
            }
            request.setAttribute("userId", userId);
            request.setAttribute("token", token);
        }
        return true;
    }

    /**
    * 处理错误
    */
    private void sendResponse(HttpServletResponse response, String error) throws IOException {
        response.setHeader("Content-type", MediaType.APPLICATION_JSON.toString());
        response.setCharacterEncoding("UTF-8");
        Result result = Result.wrapError(error);
        response.getWriter().write(JSON.toJSONString(result));
    }
}

 public Boolean isTokenExpire(String token) {
        Assert.hasText(token, "token不能为空");
        return !redisClient.exists("user:id:" + token);
    }

 public Integer getUserIdByToken(String token) {
     Assert.hasText(token, "token不能为空");
     return redisClient.get("user:id:" + token);
 }
(3) 用户登出,清楚Redis中的token
redisClient.del("user:id:" + token,userId);

总结:

分布式Session会话实现方式很多,Tomcat+Redis是最早期常用的技术,但是此种方式重度耦合容器,不好维护。目前常见使用SpringSession来统一管理Session。这种方式使用Session不够定制话,不够灵活。如果是财大器粗的大公司都是自研Session管理。