1. 前后端分离架构

1.1 简介

核心思想:前端HTML页面通过AJAX调用后端的 RESTful API 接口 并使用JSON数据进行交互。
image.png
参考博客:http://blog.720ui.com/2016/arch_web_server/

1.2 前后端分离的好处

分工明确,职责清晰,前端人员关注界面展现/交互逻辑,服务端人员关注业务/数据接口。

  • 前端职责:页面UI,页面展示、交互、渲染,用户体验等。
  • 后端职责:数据存储和业务逻辑,RESTful API 接口,系统性能、安全性等。

多端应用:

  • 一套服务端RESTful API,可以为 WEB、APP、小程序、公共号等多客户端提供服务

    1.3 会话管理的三种方式

    会话是浏览器和服务器之间的多次请求和响应,从浏览器访问服务器开始,到访问服务器结束,浏览器关闭为止的这段时间内容产生的多次请求和响应,合起来叫做浏览器和服务器之间的一次会话。
    HTTP协议无状态,客户端一次请求结束,发起新的请求时,服务器不记录上一次的请求。如何知道两次请求是同一个用户发出的呢?需要用到会话管理。

    server-session

    image.png
    优点:

  • 经典传统的会话管理方式,安全性好、使用简单。

缺点:

  • 会话信息存储在服务器,同时在线用户比较多时,会话信息会占据比较多的内存;
  • 当应用采用集群部署的时,会遇到多台web服务器之间如何作session共享的问题。由于session是由单个服务器建立的,可是处理用户请求的服务器不一定是那个建立session的服务器,这样就拿不到以前已经放入到session中的登陆凭证之类的信息了;
  • 当多个应用共享session时,会遇到跨域问题,由于不一样的应用可能部署的主机不同,须要在各个应用作好cookie跨域的处理。

    cookie-based

    把用户的登陆凭证存到客户端,用户登录成功后,把登录凭证写到cookie里,并给该cookie设置有效期,后续请求直接验证存有登陆凭证的cookie是否存在以及凭证是否有效,便可判断用户的登录状态。流程如下:
    image.png
    优点:

  • 实现了服务端无状态,会话不再占用服务器内存;

  • 不存在Session共享问题。

缺点:

  • cookie大小有限制,存储不了太多信息;
  • 每次请求都要携带该cookie;
  • cookie跨域问题

    token-based

image.png
这种方式不使用Cookie进行token的传递,而是每次请求的时候,主动把token加到http header里面或请求参数里面。
优点:

  • Cookie的缺点;
  • 可以在不支持Cookie的客户端(如:小程序)使用。

    会话安全**

  • 第一种方式的会话凭证仅仅是一个Sessionid,保证sessionid足够随机,其它人就不可能轻易地冒充别人的sessionid进行操做;

  • 第二种方式的凭证ticket以及第三种方式的凭证token都是一个在服务端作了数字签名和加密处理的字符串,因此只要密钥不泄露,别人也没法轻易地拿到这个串中的有效信息并对它进行篡改。

会话劫持:

  • 使用 HTTPS;
  • ticket/token 定时刷新;

    2. JWT

    2.1 简介

    JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
    应用场景:

  • Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。

  • Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Token是一种很好的方式。因为JWT可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。

    2.2 组成结构

    JSON Web Tokenv由三部分组成,它们之间用点 . 连接。这三部分分别是:

  • Header

  • Payload
  • Signature

完整 JWT 结构如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTYxNDIzMDIsInVzZXJuYW1lIjoiamFjayJ9.l9kjzYelZJXrD7MxKhR2p3vjcEi6JBsmPVY9uxmG2zY

header

JWT 的头部包含两部分信息:

  • typ:token 类型,这里是 JWT
  • alg:加密算法名称, 通常直接使用 HMAC SHA256

    1. {
    2. 'typ': "JWT",
    3. 'alg': "HS256"
    4. }

    然后,用Base64 对这个JSON 编码就得到JWT的第一部分。

    payload

    声明有三种类型:

  • registered: 标准中注册的声明。

  • public: 公共声明。
  • private: 私有声明。

标准中注册的声明 (建议但不强制使用):

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp:jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf:定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共声明:
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。
私有声明:
私有声明是提供者和消费者所共同定义的声明,但不建议添加敏感信息,因为该部分在客户端可解密。
定义一个payload:

  1. {
  2. "sub": "1234567890",
  3. "name": "jack",
  4. "admin": true
  5. }

然后,用 Base64 对这个JSON 编码就得到JWT的第二部分。

signature

JWT 的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header
  • payload
  • secret

这个部分需要 Base64 编码后的 header 和 payload 使用 . 连接组成的字符串,然后通过 header 中声明的加密方式进行加盐 secret 组合加密,然后就构成了 jwt 的第三部分。
伪代码:

  1. String str = Base64(header) + "." + Base64(payload);
  2. String secret = "39kjfeirjokjfffkYEE";
  3. String signature = HMACSHA256(srt,secret);

将这三部分用.连接成一个完整的字符串,构成了最终的 JWT。
注意:

  • secret 保存在服务器端,jwt的签发和验证是在服务器端;
  • secret 用来签发和验证 jwt 。

所以,secret 是服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个 secret, 那就意味着客户端是可以自我签发 jwt了。

2.3 优点

  • 因为json的通用性,所以JWT是可以进行跨语言支持的;
  • 因为有了payload部分,所以JWT可以在自身存储一些其它业务逻辑所必要的非敏感信息;
  • 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的;
  • 它不需要在服务端保存会话信息, 所以它易于应用的扩展。

    2.4 安全相关

  • 不应该在 jwt 的 payload 部分存放敏感信息,因为该部分是客户端可解码的部分;

  • 保护好 secret 私钥,该私钥非常重要;
  • 如果可以,请使用 https 协议。

    2.5 java-jwt

    java-jwt 是 JWT 开放标准 RFC 7519 的 Java 语言实现类库。
    github地址:https://github.com/auth0/java-jwt

    引入类库

    1. <dependency>
    2. <groupId>com.auth0</groupId>
    3. <artifactId>java-jwt</artifactId>
    4. <version>3.14.0</version>
    5. </dependency>

    创建 token

    try {
      String secret = "iuq9339349589589dkjf";
      Algorithm algorithm = Algorithm.HMAC256(secret);
      String token = JWT.create()
          .withIssuer("auth0")
          .sign(algorithm);
    } catch (JWTCreationException exception){
      //Invalid Signing configuration Couldn't convert Claims.
    }
    

    校验 token

    String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE";
    try {
      String secret = "iuq9339349589589dkjf";
      Algorithm algorithm = Algorithm.HMAC256(secret);
      JWTVerifier verifier = JWT.require(algorithm)
          .withIssuer("auth0")
          .build(); //Reusable verifier instance
      DecodedJWT jwt = verifier.verify(token);
    } catch (JWTVerificationException exception){
      //JWTVerificationException
      //Invalid signature/claims
    }
    

    解析 token

    String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE";
    try {
      DecodedJWT jwt = JWT.decode(token);
    } catch (JWTDecodeException exception){
      //Invalid token
    }
    

    JWTUtil 工具类

    import com.auth0.jwt.JWT;
    import com.auth0.jwt.JWTVerifier;
    import com.auth0.jwt.algorithms.Algorithm;
    import com.auth0.jwt.interfaces.DecodedJWT;
    import java.util.Date;
    /**
    * JWT 工具类
    */
    public class JWTUtil {
      /**
       * 生成token
       * @param username   用户名
       * @param secret     私钥
       * @param expires      有效期
       * @param issued 签发时间
       * @return
       */
      public static String generatorToken(String username, String secret, 
                                          Long expires, Date issued) {
          Date date = new Date(System.currentTimeMillis() + expireTime * 1000);
          Algorithm algorithm = Algorithm.HMAC256(username + secret);
          return JWT.create()
                  .withClaim("username", username)
                  .withExpiresAt(date)
                  .withIssuedAt(issued)
                  .sign(algorithm);
      }
      /**
       * 校验 token
       * @param token
       * @param secret
       * @return
       */
      public static boolean verifyToken(String token, String secret) {
          Algorithm algorithm = Algorithm.HMAC256(getUsername(token) + secret);
          JWTVerifier verifier = JWT.require(algorithm).build();
          verifier.verify(token);
          return true;
      }
    
      /**
       * 从token中获取用户名
       * @param token
       * @return
       */
      public static String getUsername(String token) {
          DecodedJWT jwt = JWT.decode(token);
          return jwt.getClaim("username").asString();
      }
    
      /**
       * 从token中获取签发时间
       * @param token
       * @return
       */
      public static Date getIssuedTime(String token) {
          DecodedJWT jwt = JWT.decode(token);
          return jwt.getIssuedAt();
      }
    }