JWT学习

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JWT学习相关的知识,希望对你有一定的参考价值。


常见的认证机制

HTTP Basic Auth

HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放的RESTful API时,尽量避免采用HTTP Basic Auth。

Cookie Auth

Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使cookie在一定时间内有效。

OAuth

OAuth(开放授权,Open Authorization)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。如网站通过微信、微博登录等,主要用于第三方登录。

OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。

下面是OAuth2.0的流程:


这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用。

缺点:过重。


Token Auth

使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:

  1. 客户端使用用户名跟密码请求登录

  2. 服务端收到请求,去验证用户名与密码

  3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端

  4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里

  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token

  6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据


比第一种方式更安全,比第二种方式更节约服务器资源,比第三种方式更加轻量。

具体,Token Auth的优点(Token机制相对于Cookie机制又有什么好处呢?):

  1. 支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.

  2. 无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.

  3. 更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascripthtml,图片等),而你的服务端只要提供API即可.

  4. 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.

  5. 更适用于移动应用: 当你的客户端是一个原生平台(ios, android,Windows 10等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。

  6. CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。

  7. 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算的Token验证和解析要费时得多.

  8. 不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理.

  9. 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, php)和多家公司的支持(如:Firebase,Google, Microsoft).


JWT简介

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。

官网: https://jwt.io/

标准: https://tools.ietf.org/html/rfc7519

JWT令牌的优点:

  1. jwt基于json,非常方便解析。

  2. 可以在令牌中自定义丰富的内容,易扩展。

  3. 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。

  4. 资源服务使用JWT可不依赖认证服务即可完成授权。

缺点:

  1. JWT令牌较长,占存储空间比较大。

JWT组成

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

头部(Header)

头部用于描述关于该JWT的最基本的信息,例如其类型(即JWT)以及签名所用的算法(如HMAC SHA256或RSA)等。这也可以被表示成一个JSON对象。

{
  "alg": "HS256",
  "typ": "JWT"
}
  • typ:是类型。

  • alg:签名的算法,这里使用的算法是HS256算法

我们对头部的json字符串进行BASE64编码(网上有很多在线编码的网站),编码后的字符串如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9  

Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中提供了非常方便的 BASE64EncoderBASE64Decoder,用它们可以非常方便的完成基于 BASE64 的编码和解码。


负载(Payload)

第二部分是负载,就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:

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

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

  • 私有的声明

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

这个指的就是自定义的claim。比如下面那个举例中的name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

其中sub是标准的声明,name是自定义的声明(公共的或私有的)

然后将其进行base64编码,得到Jwt的第二部分:

 eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbWVzIiwiYWRtaW4iOnRydWV9  

提示:声明中不要放一些敏感信息。


签证、签名(signature)

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

  1. header (base64后的)

  2. payload (base64后的)

  3. secret(盐,一定要保密)

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

8HI-Lod0ncfVDnbKIPJJqLH998duF9DSDGkx3gRPNVI

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

eyJhbGciOiJIUzI1NiIsInR9cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
8HI-Lod0ncfVDnbKIPJJqLH998duF9DSDGkx3gRPNVI  

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。


JJWT简介

JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJW很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

规范官网:https://jwt.io/

快速入门

token的创建

创建SpringBoot工程,引入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.2.2.RELEASE</version>
      <relativePath/> <!-- lookup parent from repository -->
   </parent>
   <groupId>com.yjxxt</groupId>
   <artifactId>jwtdemo</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>jwtdemo</name>
   <description>Demo project for Spring Boot</description>

   <properties>
      <java.version>1.8</java.version>
   </properties>

   <dependencies>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <!--JWT依赖-->
      <dependency>
         <groupId>io.jsonwebtoken</groupId>
         <artifactId>jjwt</artifactId>
         <version>0.9.0</version>
      </dependency>

      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
         <scope>test</scope>
         <exclusions>
            <exclusion>
               <groupId>org.junit.vintage</groupId>
               <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
         </exclusions>
      </dependency>
   </dependencies>
</project>

创建测试类JwtTest,用于生成token:

@SpringBootTest
public class JwtdemoApplicationTests {

   /**
    * 创建token
    */
   @Test
   public void testCreatToken() {
      //创建一个JwtBuilder对象
      JwtBuilder jwtBuilder = Jwts.builder()
            //声明的标识{"jti":"888"}
            .setId("888")
            //主体,用户{"sub":"Rose"}
            .setSubject("Rose")
            //创建日期{"ita":"yjxxtxx"}
            .setIssuedAt(new Date())
            //签名手段,参数1:算法,参数2:盐
            .signWith(SignatureAlgorithm.HS256,"yjxxt");
      //获取jwt的token
      String token = jwtBuilder.compact();
      System.out.println(token);

      //三部分的base64解密
      System.out.println("--------------------");
      //按照点分割
      String[] split = token.split("\\\\.");
      System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
      System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
      //无法解密---因为是非对成性加密,并且如果轻易解密了,那么盐就没啥用了
      System.out.println(Base64Codec.BASE64.decodeToString(split[2]));
   }
}

运行结果如下

再次运行,会发现每次运行的结果是不一样的,因为我们的载荷中包含了时间


token的验证解析

我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。

@Test
public void testParseToken(){
   //token
   String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNTc4ODE0MjUyfQ" +
         ".-FYFMHyfTcGzq900f_Drfdsges0ge2UjaWvPW9gCDto";
   //解析token获取负载中的声明对象
   Claims claims = Jwts.parser()
         .setSigningKey("yjxxt")
         .parseClaimsJws(token)
         .getBody();
   //打印声明的属性
   System.out.println("id:"+claims.getId());
   System.out.println("subject:"+claims.getSubject());
   System.out.println("issuedAt:"+claims.getIssuedAt());
}


试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析token也就是验证token


token过期校验

有很多时候,我们并不希望签发的token是永久生效的(上节的token是永久的),所以我们可以为token添加一个过期时间。原因:从服务器发出的token,服务器自己并不做记录,就存在一个弊端就是,服务端无法主动控制某token的立刻失效。

测试用例:

@Test
public void testCreatTokenHasExp() {
   //当前系统时间的长整型
   long now = System.currentTimeMillis();
   //过期时间,这里是1分钟后的时间长整型
   long exp = now + 60 * 1000;
   //创建一个JwtBuilder对象
   JwtBuilder jwtBuilder = Jwts.builder()
         //声明的标识{"jti":"888"}
         .setId("888")
         //主体,用户{"sub":"Rose"}
         .setSubject("Rose")
         //创建日期{"ita":"yjxxtxx"}
         .setIssuedAt(new Date())
         //签名手段,参数1:算法,参数2:盐
         .signWith(SignatureAlgorithm.HS256, "yjxxt")
         //设置过期时间
         .setExpiration(new Date(exp));
   //获取jwt的token
   String token = jwtBuilder.compact();
   System.out.println(token);
}
@Test
public void testParseTokenHasExp() {
   //token
   String token = "eyJhbGciOiJIUzI1NiJ9" +
         ".eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNTc4ODE1MDYyLCJleHAiOjE1Nzg4MTUxMjIsInJvbGVzIjoiYWRtaW4iLCJsb2dvIjoic2hzeHQuanBnIn0.hKog0RsZ9_6II_R8kUCp0HLAouUAYXAJVbz3xtLTUh4";
   //解析token获取负载中的声明对象
   Claims claims = Jwts.parser()
         .setSigningKey("yjxxt")
         .parseClaimsJws(token)
         .getBody();
   //打印声明的属性
   System.out.println("id:" + claims.getId());
   System.out.println("subject:" + claims.getSubject());
   System.out.println("issuedAt:" + claims.getIssuedAt());
   DateFormat sf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
   System.out.println("签发时间:"+sf.format(claims.getIssuedAt()));
   System.out.println("过期时间:"+sf.format(claims.getExpiration()));
   System.out.println("当前时间:"+sf.format(new Date()));
}

测试:当未过期时可以正常读取,当过期时会引发io.jsonwebtoken.ExpiredJwtException异常。


自定义claims

我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims

测试用例:

@Test
public void testCreatTokenByClaims() {
   //当前系统时间的长整型
   long now = System.currentTimeMillis();
   //过期时间,这里是1分钟后的时间长整型
   long exp = now + 60 * 1000;
   //创建一个JwtBuilder对象
   JwtBuilder jwtBuilder = Jwts.builder()
         //声明的标识{"jti":"888"}
         .setId("888")
         //主体,用户{"sub":"Rose"}
         .setSubject("Rose")
         //创建日期{"ita":"yjxxtxx"}
         .setIssuedAt(new Date())
         //签名手段,参数1:算法,参数2:盐
         .signWith(SignatureAlgorithm.HS256, "yjxxt")
         //设置过期时间
         .setExpiration(new Date(exp))
         //直接传入map
         // .addClaims(map)
         .claim("roles","admin")
         .claim("logo","yjxxt.jpg");
   //获取jwt的token
   String token = jwtBuilder.compact();
   System.out.println(token);
}
@Test
public void testParseTokenByClaims() {
   //token
   String token = "eyJhbGciOiJIUzI1NiJ9" +
         ".eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNTc4ODE1MDYyLCJleHAiOjE1Nzg4MTUxMjIsInJvbGVzIjoiYWRtaW4iLCJsb2dvIjoic2hzeHQuanBnIn0.hKog0RsZ9_6II_R8kUCp0HLAouUAYXAJVbz3xtLTUh4";
   //解析token获取负载中的声明对象
   Claims claims = Jwts.parser()
         .setSigningKey("yjxxt")
         .parseClaimsJws(token)
         .getBody();
   //打印声明的属性
   System.out.println("id:" + claims.getId());
   System.out.println("subject:" + claims.getSubject());
   System.out.println("issuedAt:" + claims.getIssuedAt());
   DateFormat sf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
   System.out.println("签发时间:"+sf.format(claims.getIssuedAt()));
   System.out.println("过期时间:"+sf.format(claims.getExpiration()));
   System.out.println("当前时间:"+sf.format(new Date()));

   System.out.println("roles:"+claims.get("roles"));
   System.out.println("logo:"+claims.get("logo"));
}

Spring Security Oauth2 整合JWT

整合JWT

我们拿之前Spring Security Oauth2的完整代码进行修改

添加配置文件JwtTokenStoreConfig.java

/**
 * 使用Jwt存储token的配置
 * @author zhoubin
 * @since 1.0.0
 */
@Configuration
public class JwtTokenStoreConfig {

   @Bean
   public TokenStore jwtTokenStore(){
      return new JwtTokenStore(jwtAccessTokenConverter());
   }

   @Bean
   public JwtAccessTokenConverter jwtAccessTokenConverter(){
      JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
      //配置JWT使用的秘钥
      accessTokenConverter.setSigningKey("test_key");
      return accessTokenConverter;
   }
}

在认证服务器配置中指定令牌的存储策略为JWT

/**
 * 授权服务器配置
 * @author zhoubin
 * @since 1.0.0
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    java SpringRetry学习的代码片段

python 机器学习有用的代码片段

flask验证登录学习过程---实践flask_jwt

学习笔记:python3,代码片段(2017)

学习 PyQt5。在我的代码片段中找不到错误 [关闭]

koa-jwt 全面解析,安检利器!