JWT之入门使用

170 阅读10分钟

JWT之入门使用

一、JWT概述

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

JWT官网
在这里插入图片描述

JWT令牌的优点:

1、jwt基于json,非常方便解析。
2、可以在令牌中自定义丰富的内容,易扩展。
3、通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
4、资源服务使用JWT可不依赖认证服务即可完成授权。

缺点:

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

令牌结构

JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxx.yyy.zzz

头部(Header)

头部用于描述关于该JWT的最基本的信息,例如其令牌的类型(即JWT)以及签名所用的算法(如HMAC SHA256或RSA)

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

将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。

ewogICAgImFsZyI6ICJIUzI1NiIsCiAgICAidHlwIjogIkpXVCIKfQ==

载荷(playload)

第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。

此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。

{
  "sub": "jwt_sub",
  "name": "jwt_name",
}

将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。

ewogICJzdWIiOiAiand0X3N1YiIsCiAgIm5hbWUiOiAiand0X25hbWUiLAp9

签证(signature)

第三部分是签名,此部分用于防止jwt内容被篡改。

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

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)
base64UrlEncode(header) :jwt令牌的第一部分。

base64UrlEncode(payload):jwt令牌的第二部分。

secret:签名所使用的密钥。

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

二、JJWT签发与验证token

JJWT是一个提供端到端的JWT创建和验证的Java库。

官方文档:https://github.com/jwtk/jjwt

引入依赖

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

创建token

    public static void main(String[] args) {
        JwtBuilder builder = Jwts.builder()
                .setId(UUID.randomUUID().toString())   //设置唯一编号,token的ID
                .setSubject("sub_name")//设置主题,token中携带的数据,可以是JSON数据
                .setIssuedAt(new Date())//设置签发日期,即token的生成时间
                .signWith(SignatureAlgorithm.HS256, "SecretKey");//设置签名 使用HS256算法加密方式,并设置SecretKey(字符串)加密密码
        //构建并返回一个字符串
        System.out.println(builder.compact());
    }
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJhZjdlMjZjMi0yYzE1LTRhNjQtOWQ0ZS01MWE2YjQ4YmIzMDIiLCJzdWIiOiJzdWJfbmFtZSIsImlhdCI6MTYyMjI4MTkxOX0.vCIzHmQAA8f7bR8gJ9ag59gtR8zLNy3hAi5Gw0v697I

解析token

    public static void main(String[] args) {
        //jwt令牌
        String jwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJhZjdlMjZjMi0yYzE1LTRhNjQtOWQ0ZS01MWE2YjQ4YmIzMDIiLCJzdWIiOiJzdWJfbmFtZSIsImlhdCI6MTYyMjI4MTkxOX0.vCIzHmQAA8f7bR8gJ9ag59gtR8zLNy3hAi5Gw0v697I";
        //解析令牌
        Claims claims = Jwts.parser().setSigningKey("SecretKey").parseClaimsJws(jwt).getBody();
        System.out.println(claims);
    }
{jti=af7e26c2-2c15-4a64-9d4e-51a6b48bb302, sub=sub_name, iat=1622281919}

如果token正确则正常解析,如果token不正确或者过期,则通过抛出的异常进行识别

try {
  		//.......
	    return BaseUtils.back(BaseUtils.OK,"success");
	}catch (ExpiredJwtException e){
	    return BaseUtils.back(BaseUtils.LOGIN_FAIL_OVERDUE,"登录过期,请重新登录!");
	}catch (UnsupportedJwtException e){
	    returnBaseUtils.back(BaseUtils.LOGIN_FAIL_NOT,"Tonken不合法!");
	}catch (Exception e){
	    return BaseUtils.back(BaseUtils.LOGIN_FAIL_NOT,"请重新登录!");
	}

设置过期时间

    public static void main(String[] args) {
        //过期时间:10s
        long ttl = 10 * 1000;
        //当前时间毫秒值
        long currentTimeMillis = System.currentTimeMillis();

        JwtBuilder builder = Jwts.builder()
                .setId(UUID.randomUUID().toString())   //设置唯一编号,token的ID
                .setSubject("sub_name")//设置主题,token中携带的数据,可以是JSON数据
                .setIssuedAt(new Date(currentTimeMillis))//设置签发日期,即token的生成时间
                .setExpiration(new Date(currentTimeMillis + ttl)) //设置过期时间
                .signWith(SignatureAlgorithm.HS256, "SecretKey");//设置签名 使用HS256算法加密方式,并设置SecretKey(字符串)加密密码
        //构建并返回一个字符串
        String jwt = builder.compact();
        System.out.println(jwt);
        try {
            Thread.sleep(3000);
            //Thread.sleep(10*1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //解析令牌
        Claims claims = Jwts.parser().setSigningKey("SecretKey").parseClaimsJws(jwt).getBody();
        System.out.println(claims);
    }

时间过期,则报错

Exception in thread "main" io.jsonwebtoken.ExpiredJwtException: JWT expired at 2021-05-29T18:05:53Z. Current time: 2021-05-29T18:05:58Z, a difference of 5509 milliseconds.  Allowed clock skew: 0 milliseconds.

自定义claims

如果需要存储更多的信息,可以使用自定义claims。
JwtBuilder builder = Jwts.builder()
                .setId(UUID.randomUUID().toString())   //设置唯一编号
                .setSubject("sub_name")//设置主题  可以是JSON数据
                .setIssuedAt(new Date(currentTimeMillis))//设置签发日期
                .setExpiration(new Date(currentTimeMillis + ttl)) //设置过期时间
                .claim("roles","{'role1','role2}") //如果需要存储更多的信息,可以使用自定义claims,如存放用户的角色权限信息
                .claim("name","小白白")
                .signWith(SignatureAlgorithm.HS256, "SecretKey");//设置签名 使用HS256算法,并设置SecretKey(字符串)
        //构建并返回一个字符串
        String jwt = builder.compact();
        System.out.println(jwt);
{jti=e25d97a4-bf08-4eab-aa64-a1bae09d4be5, sub=sub_name, iat=1622283121, exp=1622283131, roles={'role1','role2}, name=小白白}

三、拦截器校验Token

创建拦截器

@Component
public class CheckTokenInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String method = request.getMethod();
        //预请求放行
        if ("OPTIONS".equalsIgnoreCase(method)) {
            return true;
        }
        //头信息取出token
        String token = request.getHeader("token");
        if (token == null) {
            doResponse(response, BaseUtils.back(BaseUtils.LOGIN_FAIL_NOT, "请先登录!"));
        } else {
            try {
                JwtParser parser = Jwts.parser();
                //解析token的SigningKey必须和生成token时设置密码一致
                parser.setSigningKey("SecretKey"); 
                //如果token正确(密码正确,有效期内)则正常执行,否则抛出异常
                Jws<Claims> claimsJws = parser.parseClaimsJws(token);
                return true;
            } catch (ExpiredJwtException e) {
                doResponse(response, BaseUtils.back(BaseUtils.LOGIN_FAIL_OVERDUE, "登录过期,请重新登录!"));
            } catch (UnsupportedJwtException e) {
                doResponse(response, BaseUtils.back(BaseUtils.LOGIN_FAIL_NOT, "Token不合法!"));
            } catch (Exception e) {
                doResponse(response, BaseUtils.back(BaseUtils.LOGIN_FAIL_NOT, "请先登录!"));
            }
        }
        return false;
    }

    private void doResponse(HttpServletResponse response, ResultVO resultVO) throws IOException {
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        PrintWriter out = response.getWriter();
        String s = new ObjectMapper().writeValueAsString(resultVO);
        out.print(s);
        out.flush();
        out.close();
    }
}

配置拦截器

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Autowired
    private CheckTokenInterceptor checkTokenInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(checkTokenInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/index");
    }
}

请求头传递token

axios({
    url:"http://localhost:8888/user/login",
    method:"post",
    params:{
        username:"admin",
        password:"admin"
    },
    headers:{
       token:this.token
    },
}).then(function(res){
    console.log(res)
});

四、生成私钥和公钥

生成密钥

该命令生成密钥证书,采用RSA 算法每个证书包含公钥和私钥

keytool -genkeypair -alias aliaskey -keyalg RSA -keypass adminkey -keystore ybzy.jks -storepass adminkeystore

Keytool 是一个java提供的证书管理工具 , 只要有安装JDK环境即可使用

-alias:密钥的别名
-keyalg:使用的hash算法
-keypass:密钥的访问密码
-keystore:密钥库文件名,ybzy.jks保存了生成的证书
-storepass:密钥库的访问密码
D:\>keytool -genkeypair -alias aliaskey -keyalg RSA -keypass adminkey -keystore ybzy.jks  -storepass adminkeystore
您的名字与姓氏是什么?
  [Unknown]:  test
您的组织单位名称是什么?
  [Unknown]:  ybzy
您的组织名称是什么?
  [Unknown]:  您所在的城市或区域名称是什么?
  [Unknown]:
D:\>keytool -genkeypair -alias aliaskey -keyalg RSA -keypass adminkey -keystore ybzy.jks  -storepass adminkeystore
您的名字与姓氏是什么?
  [Unknown]:  test
您的组织单位名称是什么?
  [Unknown]:  ybzy
您的组织名称是什么?
  [Unknown]:  jwt
您所在的城市或区域名称是什么?
  [Unknown]:  sc
您所在的省/市/自治区名称是什么?
  [Unknown]:  cd
该单位的双字母国家/地区代码是什么?
  [Unknown]:  cn
CN=test, OU=ybzy, O=jwt, L=sc, ST=cd, C=cn是否正确?
  [否]:  y


Warning:
JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore ybzy.jks -destkeystore ybzy.jks -deststoretype pkcs12" 迁移到行业标准格式 PKCS12。

D:\>

查询证书信息

keytool -list -keystore ybzy.jks
D:\>keytool -list -keystore ybzy.jks
输入密钥库口令:
密钥库类型: jks
密钥库提供方: SUN

您的密钥库包含 1 个条目

aliaskey, 2021-1-30, PrivateKeyEntry,
证书指纹 (SHA-256): 64:4B:67:5A:F2:B7:33:15:35:DC:B9:CD:98:CF:A8:D0:7E:DF:3F:47:17:3A:04:ED:6E:2A:0B:75:65:2D:BC:F7

Warning:
JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore ybzy.jks -destkeystore ybzy.jks -deststoretype pkcs12" 迁移到行业标准格式 PKCS12。

D:\>

删除别名

keytool -delete -alias aliaskey -keystore ybzy.jks

导出公钥

openssl是一个加解密工具包,使用openssl来导出公钥信息。

安装并配置openssl的path环境变量,在ybzy.jks文件所在目录执行命令:
keytool -list -rfc --keystore ybzy.jks| openssl x509 -inform pem -pubkey
D:\>keytool -list -rfc --keystore ybzy.jks| openssl x509 -inform pem -pubkey
输入密钥库口令:

*****************  WARNING WARNING WARNING  *****************
* 存储在您的密钥库中的信息的完整性  *
* 尚未经过验证!  为了验证其完整性, *
* 必须提供密钥库口令。                  *
*****************  WARNING WARNING WARNING  *****************

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlyu4kW0k9+du8mJuiMqs
B668fExfrxCU8phM6+/tefXbgwNFZ6xgGviC5c1O+DlN+ByBI3kMpVMotSN5Un4i
zdRI9DgHSjCx9jEazwsEzSEnCpDR3A2upQlm3Pdm59c13bDsHetqk57Pp16SLalp
FW1+ET79aTdBzv9Tn3a8Dm2omvE/CftbV48FC/1DZ+0X00dlUpPANrqFtqAV/2ad
CnbdDPtctblN3zMlZKIEjUArRfWctpM5mFYzg7r0Uy+GJwOYUbTUPo0ZeVnDWV98
IyyktVwEnjYPWS0ii9wiXMdQFJl5EMlmA3l3jpmnqyWwpBtb+JohKc0pPwDPeFaD
bQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----

Warning:MIIDRTCCAi2gAwIBAgIEDB+FvDANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJj
bjELMAkGA1UECBMCY2QxCzAJBgNVBAcTAnNjMQ0wCwYDVQQKEwR5Ynp5MQwwCgYD
VQQLEwNqd3QxDTALBgNVBAMTBHRlc3QwHhcNMjEwMTMwMDUzMDA3WhcNMjEwNDMw
MDUzMDA3WjBTMQswCQYDVQQGEwJjbjELMAkGA1UECBMCY2QxCzAJBgNVBAcTAnNj
MQ0wCwYDVQQKEwR5Ynp5MQwwCgYDVQQLEwNqd3QxDTALBgNVBAMTBHRlc3QwggEi
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCXK7iRbST3527yYm6IyqwHrrx8
TF+vEJTymEzr7+159duDA0VnrGAa+ILlzU74OU34HIEjeQylUyi1I3lSfiLN1Ej0
OAdKMLH2MRrPCwTNIScKkNHcDa6lCWbc92bn1zXdsOwd62qTns+nXpItqWkVbX4R
Pv1pN0HO/1OfdrwObaia8T8J+1tXjwUL/UNn7RfTR2VSk8A2uoW2oBX/Zp0Kdt0M
+1y1uU3fMyVkogSNQCtF9Zy2kzmYVjODuvRTL4YnA5hRtNQ+jRl5WcNZX3wjLKS1
XASeNg9ZLSKL3CJcx1AUmXkQyWYDeXeOmaerJbCkG1v4miEpzSk/AM94VoNtAgMB
AAGjITAfMB0GA1UdDgQWBBQ39bf9PfWzwxaVM4FOy49jcG5cfzANBgkqhkiG9w0B
AQsFAAOCAQEAPp8C9M1vkNBfeNFEl+EC+X7MC650LSYsjWoHPvVw0hENWkl9WN8W
prgDTmbxLvZUPT5z/sWluqTTkPbZXSUrrgToYA3s5hKeyZe8eOn+YKbstCt2qgCv
Rlaqe76uB0OIO8H/j4Vsk1Hu3rasTpxYJkyTriGkoFt6LmN4s0GBifuKeAKcBHLD
ICsOYv3TMvVzTHG/6YiZGCA4OjSVTIBHgp3HL20f/Mnfx4qxOik1JC5squuzeIwh
MofA5fxUlBNwxdd4W9aadfpa11C6rYq1+g2SDMB8YimvXNmijwDRxrrWXDXDj4gI
jByQRtpIx7d5VyNC6mf8F6yYx9IrgXuvGA==
-----END CERTIFICATE-----

JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore ybzy.jks -destkeystore ybzy.jks -deststoretype pkcs12" 迁移到行业标准格式 PKCS12。

D:\>

将公钥拷贝到文本文件中,合并为一行。

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlyu4kW0k9+du8mJuiMqs
B668fExfrxCU8phM6+/tefXbgwNFZ6xgGviC5c1O+DlN+ByBI3kMpVMotSN5Un4i
zdRI9DgHSjCx9jEazwsEzSEnCpDR3A2upQlm3Pdm59c13bDsHetqk57Pp16SLalp
FW1+ET79aTdBzv9Tn3a8Dm2omvE/CftbV48FC/1DZ+0X00dlUpPANrqFtqAV/2ad
CnbdDPtctblN3zMlZKIEjUArRfWctpM5mFYzg7r0Uy+GJwOYUbTUPo0ZeVnDWV98
IyyktVwEnjYPWS0ii9wiXMdQFJl5EMlmA3l3jpmnqyWwpBtb+JohKc0pPwDPeFaD
bQIDAQAB
-----END PUBLIC KEY-----

五、JWT结合私钥和公钥的使用

	    <dependency>
	            <groupId>com.alibaba</groupId>
	            <artifactId>fastjson</artifactId>
	            <version>1.2.76</version>
	      </dependency>
	        
	     <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
            <version>2.2.4.RELEASE</version>
        </dependency>

私钥生成jwt令牌

    @Test
    public void createJwtTest() {
        //密钥库文件
        String keystore = "ybzy.jks";
        //密钥库的密码
        String keystore_password = "adminkeystore";

        //密钥库文件路径
        ClassPathResource classPathResource = new ClassPathResource(keystore);
        //密钥别名
        String alias = "aliaskey";
        //密钥的访问密码此密码和别名要匹配
        String key_password = "adminkey";
        //密钥工厂
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(classPathResource, keystore_password.toCharArray());
        //密钥对(公钥和私钥)
        KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias, key_password.toCharArray());
        //获取私钥
        RSAPrivateKey aPrivate = (RSAPrivateKey) keyPair.getPrivate();
        //定义payload信息 jwt令牌的内容
        Map<String, String> body = new HashMap<>();
        body.put("name", "jwt");
        String bodyString = JSON.toJSONString(body);
        //生成jwt令牌
        Jwt jwt = JwtHelper.encode(bodyString, new RsaSigner(aPrivate));
        //取出jwt令牌
        String encoded = jwt.getEncoded();
        System.out.println(encoded);
    }

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiand0In0.iGmIVrHO243Jd46IbZDdQXhEm0I3d1Qlb_P1raHE7ys7G5pc9p8GZZv5B3yumSDg8NvmVD9AzUglnT_PJU1_bz0W5b4F5ZHaF1IiDFyqdopka_dOqrO0DO4v26NrBQeUwGCOh37hcBVSmgFDIYUK0dLw_Mi2oovEEpSt0uYkRGO9ctur0kJngFiXX5WtTg0IIzyDKFpFsgTEaz15Pb1xFmlkM7pZ0h0Lh6QQEhr9WdO5ZPSavZMCd02j3N6VGWDcZgxLFKv2BhMYzmoQtMIISI1ZN0I5kCP1yGuOwT0RboJlU1vtcnqlVyigvIFITmebSZGmGqpUgprRDXof2g

公钥解析jwt令牌

    @Test
    public void testVerify() {
        //公钥
        String publickey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlyu4kW0k9+du8mJuiMqsB668fExfrxCU8phM6+/tefXbgwNFZ6xgGviC5c1O+DlN+ByBI3kMpVMotSN5Un4izdRI9DgHSjCx9jEazwsEzSEnCpDR3A2upQlm3Pdm59c13bDsHetqk57Pp16SLalpFW1+ET79aTdBzv9Tn3a8Dm2omvE/CftbV48FC/1DZ+0X00dlUpPANrqFtqAV/2adCnbdDPtctblN3zMlZKIEjUArRfWctpM5mFYzg7r0Uy+GJwOYUbTUPo0ZeVnDWV98IyyktVwEnjYPWS0ii9wiXMdQFJl5EMlmA3l3jpmnqyWwpBtb+JohKc0pPwDPeFaDbQIDAQAB-----END PUBLIC KEY-----";
        //jwt令牌
        String jwtString = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiand0In0.iGmIVrHO24-3Jd46IbZDdQXhEm0I3d1Qlb_P1raHE7ys7G5pc9p8GZZv5B3yumSDg8NvmVD9AzUglnT_PJU1_bz-0W5b4F5ZHaF1-IiDFyqdopka_dOqrO0DO4v26NrBQeUwGCOh37hcBVSmgFDIYUK0dLw_Mi2oovEEpSt0uYkRGO9ctur0kJngFiXX5WtTg0IIzyDKFpFsgTEaz15Pb1xFmlkM7pZ0h0-Lh6QQEhr9WdO5ZPSavZMCd02j3N6VGWDcZgxLFKv2BhMYzmoQtMIISI1ZN0I5kCP1yGuOwT0RboJlU1vtcnqlVyigvIFITmebSZGmGqpUgprRDXof2g";
        //校验jwt令牌
        Jwt jwt = JwtHelper.decodeAndVerify(jwtString, new RsaVerifier(publickey));
        //获取jwt原始内容
        String claims = jwt.getClaims();
        System.out.println(claims);
    }
}
{"name":"jwt"}