Bootstrap

JWT鉴权

两种鉴权方式

session 认证

流程

客户端 服务器 客户端使用用户名和密码登录 验证用户名密码,生成对应的Cookie 通过响应头将Cookie给客户端 将Cookie存储在当前域名下 请求头携带Cookie 通过Cookie验证用户信息 将响应返回客户端 客户端 服务器

缺点

  • 服务器保存每个用户信息,随着用户增多,服务器开销增大
  • 在不使用缓存等中间件情况下,不适用于分布式系统
  • 不适用于不保存cookie的客户端或浏览器禁用了cookie的情况
  • cookie被截获,用户很容易收到跨站请求伪造攻击
  • 无法跨域,不适用于单点登录的情况

token 认证

流程

客户端 服务端 使用用户名和密码请求登录 验证用户名和密码 签发一个token返回给客户端 保存token 请求时携带token 验证携带的token 返回请求数据 客户端 服务端

优点

  • 节约服务器资源
  • 对移动端和分布式更加友好
  • 支持跨域访问
  • 无状态,减轻服务端压力
  • 无需考虑CSRF(跨站请求伪造)

JWT 认证

JWT是token的一种具体实现方式,其全称是JSON Web Token,官网地址:https://jwt.io/ 。

通俗地说,JWT的本质就是一个字符串,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token,并且这个JWT token带有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为Json对象传输。

JWT的认证流程如下:

前端 后端 通过POST请求使用HTTPS将用户名密码提交 验证用户名和密码,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个JWT Token JWT Token 前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性 检查JWT Token,验证其有效性,如是否正确、是否过期 响应信息 前端 后端

JWT 的结构

JWT包含3部分:标头(Header)、有效载荷(Payload)和签名(Signature)。这三部分编码后通过.(英文的点)连接成为最后的JWT。

Header

Header由两部分组成:token的类型,也就是JWT,和使用的签名算法,如HMAC SHA256 或 RSA。Header通过Base64Url方式编码后成为JWT的第一部分。

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload

Payload为声明字段等一些需要传递的信息。声明字段可以是关于一个实体(如用户)和附加信息。声明有三种类型:注册声明、公共声明和私有声明。Payload通过Base64Url方式编码后成为JWT的第二部分。

  • 注册声明

注册声明不是强制的,但十分推荐在Payload里含有该声明。注册的声明包含iss (issuer), exp (expiration),sub (subject), aud (audience), nbf(not before),iat(issued at),jti(JWT id),全部是IANA 的 “JSON Web Token Claims” 中声明。

  • 公共声明

公共声明是使用JWT的时候均可以使用的声明,但是为了避免声明题目的冲突,需要先在IANA 的 “JSON Web Token Claims” 中声明或在一个包含声明冲突的URI。

  • 私有声明

在payload中使用的声明较多的是注册声明,同时也可以向payload中加入自己的私有声明以便传递更多的信息。

Payload示例:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

注意!

JWT中的Payload的部分使用Base64Url编码,任何人都可以读取其内容,因此不要包含隐私信息字段(如用户名、密码)。JWT只用于在网络中传输一些非敏感的信息。

Signature

签名部分通过header和payload编码后的内容和密钥经过header中指定的算法加密而成。例如在header中指定了HMAC SHA256算法,则签名通过下面的方式生成。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

签名用来验证信息在传输的过程中没有发生改变,同时,在签名使用私钥的情况下,签名还可以用来验证信息的发送者。

在Java中使用JWT

在JWT的官网推荐java-jwt、jose4j、nimbus-jose-jwt、jjwt-root、fusionauth-jwt、vertx-auth-jwt和inverno-security-jose,其中比较常用的是jwt和jjwt。

java-jwt

  1. 引入依赖
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>4.2.1</version>
</dependency>
  1. 生成token
Map<String, Object> map = new HashMap<>();
map.put("typ", "jwt");
map.put("alg", "HS256");

String token = JWT.create()
	.withHeader(map)  // Header
  .withClaim("sub", "80310620")  // Payload
  .withClaim("userName", "cfn")
  .sign(Algorithm.HMAC256("secret"));  // 签名secret

System.out.println(token);
  1. 解析token
// 创建解析对象,使用的算法和secret要与创建token时保持一致
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("secret")).build();
// 解析指定的token
DecodedJWT decodedJWT = jwtVerifier.verify(token);
// 获取解析后的token中的payload信息
Claim userId = decodedJWT.getClaim("sub");
Claim userName = decodedJWT.getClaim("userName");

jjwt-root

  1. 引入依赖
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-root</artifactId>
    <version>0.11.5</version>
</dependency>
  1. 生成token
String JwtToken = Jwts.builder()
  .setHeaderParam("typ", "JWT")
  .setHeaderParam("alg", "HS256")
  .setSubject("baobao-user")
  .claim("id", "80310620")
  .claim("nickname", "cfn")
  .signWith(SignatureAlgorithm.HS256, "xxx")
  .compact();
  1. 解析token
Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(Keys.hmacShaKeyFor("xxx".getBytes(StandardCharsets.UTF_8))).build().parseClaimsJws(jwtToken);

JWT 鉴权实例

下面以CMBT的鉴权为例演示JWT的用户认证。CMBT为前后端分离,所有前端的请求携带token通过gateway模块的认证后分发到不同的模块。用户第一次登录的时候通过一号通认证,认证完成后获得用户的信息,在gateway模块生成用户信息所对应的token,并将用户信息和token保存到redis。后续用户请求经过gateway时,gateway根据token解析出用户信息,如果redis中的键值仍然有效,则直接转发请求,如果无效则回到一号通重新进行认证。

该JWT使用的是jjwt-root,根据用户信息生成token的代码如下:

    public static String generateToken(String rtcId, String name) {
        Map<String, Object> claims = new HashMap();
        claims.put("sub", rtcId);
        claims.put("name", name);
        claims.put("expiredDate", DateUtils.getOffsetDayMidNight(7));
        return generateToken(claims);
    }
    
    private static String generateToken(Map<String, Object> claims) {
        return Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS512, "xxx").compact();
    }

从token解析出用户信息的代码如下:

    public static Claims getClaimsFromToken(String token) {
        try {
            Claims claims = (Claims)Jwts.parser().setSigningKey("xxx").parseClaimsJws(token).getBody();
            if (claims != null && claims.containsKey("expiredDate") && claims.containsKey("sub")) {
                return claims;
            } else {
                log.info("Token包含信息不完整");
                return null;
            }
        } catch (Exception var2) {
            log.info("Token解析异常");
            return null;
        }
    }a
;