OAuth 2.0系列(六)--- OAuth令牌
本章要解决的疑问:
-
OAuth令牌是什么?
-
如何生成结构化的令牌(JWT)?
-
如何使用JOSE保护令牌数据?
-
如何通过令牌内省实时获取令牌数据?
-
如何撤回令牌?
-
令牌的生命周期是怎样的?
一、OAuth令牌是什么?
令牌是OAuth中的核心组件,它代表了一次授权的结果,包含了资源拥有者、授权服务器、客户端、受保护资源、权限范围等信息,就像考驾照一样,为了拿到最终的那张本本,你可能要经过科一到科四的重重考验。在OAuth系统中,不同服务器对令牌的使用规则各有不同:
- 客户端:客户端只需要获取到令牌并使用它,无需理解令牌所包含的信息,令牌对它是不透明的。
- 授权服务器:授权服务器需要颁发令牌,令牌的内容和规则都是它生成的,所以它是最了解令牌的一方。
- 资源服务器:资源服务器因为需要验证令牌的正确性,所以也需要知道令牌的内容。
二、结构化令牌:JWT
在前面介绍的授权码流程中,访问令牌是一串随机串,不代表任何信息,只是因为保存在共享数据库中,可以提供以它为检索值,查询其他信息的功能,但是如果一个授权服务器对应多个资源服务器,再使用共享数据库就不太合理了。
举个例子🌰 :
比如一家云计算厂商提供了云主机、云硬盘、云数据库等资源,假设有个网关且提供授权服务器的功能,一个客户端要想访问这些资源需要先从网关获取访问令牌,在这种情况下不可能为了让让云主机、云硬盘和云数据库都能直接访问令牌,就它们共享同一个数据库,这是非常荒诞的做法。那么这种场景该怎么解决呢?答案是:结构化令牌和令牌内省。结构化令牌的实现方式是将所有必要的信息都放在令牌内部,使授权服务器可以通过令牌间接地和资源服务器进行通信,而不需要调用任何授权服务器端的API。前面提到过,结构化令牌的实现方式有SAML和JWT两种,本文只介绍JWT,SAML属于另一种协议。
2.1 JWT的结构
声明:JWT的知识点很多,本文只介绍它的基本生成和使用原理,深度学习可以阅读其官方文档:https://jwt.io/
JWT,即JSON Web Token,提供了一种在令牌中携带信息的简单方法。JWT的核心是将一个JSON对象封装为一种用于网络传输的格式。访问官网可以生成一个JWT:
上图是在官网生成的一个JWT,修改右侧的内容,左侧内容也会发生变化,从图中的Decoded部分可以看出,JWT由头部Header、荷载PayLoad和签名Sign 三部分组成,而且三部分之间使用.进分割,JWT一般对简单的形式是未签名的token,就是上图中去掉签名密钥之后的结果。而且每一部分都有自己的生成规则:
- Header:JSON对象,用于描述剩余部分的信息。其中typ代表第二部分PayLoad的类型,比如上图中的JWT;alg代表第三部分的签名算法,比如上图中的HS256。在JWT中要对该JSON对象进行Base64URL编码。
- PayLoad:JSON对象,存放用户数据以及令牌信息,与Header相同,要对该JSON对象进行Base64URL编码。
- Sign:如图中所示,签名的方式是分别对Header和PayLoad进行Base64URL编码,再用句点符号.分割,最后使用密钥进行签名。
所以一个签名的JWT的构成方式为:
base64url(header).base64url(payLoad).sign(base64url(header).base64url(payLoad).secret)
2.2 声明JWT
除了一般的数据结构之外,JWT还提供了一组声明,可以在不同的应用中通用,且所有字段都是可选项
除了上图中给出的标准声明外,开发过程中,还可以自定义所需的其他字段,如用户名userName,角色role等。
2.3 在服务器上实现JWT
JWT在授权服务器颁发,在客户端服务器使用,在资源服务器验证。
2.3.1 授权服务器颁发
授权服务器需要提供获取jwt的方法,虽然JWT也是token,但最好和之前获取不表示任何信息的的token区分,假设为/exchangeJwt。授权服务器一般会调用jwt第三方库,只需自定义声明的值即可,代码如下:
public String jwtWithHs256(String sub, String[] aud, String jid) throws JoseException { JsonWebSignature jws = new JsonWebSignature(); //设置Header jws.setHeader("alg","HS256"); jws.setHeader("typ","JWT"); jws.setHeader("kid","ABCDEFG"); //设置PayLoad JwtClaims jwtClaims = new JwtClaims(); jwtClaims.setIssuer("me"); jwtClaims.setSubject(sub); jwtClaims.setAudience(aud); jwtClaims.setExpirationTime(NumericDate.fromSeconds(2 * 60 * 60)); jwtClaims.setNotBefore(NumericDate.now()); jwtClaims.setIssuedAt(NumericDate.now()); jwtClaims.setJwtId(jid); jws.setPayload(jwtClaims.toJson()); //设置签名key:最小长度为256位 jws.setKey(new HmacKey(SECRET.getBytes())); return jws.getCompactSerialization(); }
这样就能返回一个使用HS256签名的JWT,结果如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkFCQ0RFRkcifQ.eyJpc3MiOiJtZSIsInN1YiI6InpoYW5nc2FuIiwiYXVkIjoiaHR0cHM6Ly9yZXNvdXJjZWVuZHBvaW50LmNvbS91c2VySW5mbyIsImV4cCI6NzIwMCwibmJmIjoxNjM2NDM4OTQxLCJpYXQiOjE2MzY0Mzg5NDEsImp0aSI6ImRmZGZkYWhrZmRoYXRlYXJlYXIifQ.YrJK74vkMOV2SFn3pNeuk2qoz4RGRDuwc4X9dyLsR94
将这个JWT复制到JWT在线生成工具之后结果如下:
上图中除了签名校验失败之外,Header和PayLoad中的值都与在代码中设置的值完全一致,签名失败的问题只需输入颁发时的密钥即可,如下图:
接下来,客户端就能拿着JWT去请求资源服务器了。
2.3.2 客户端服务器使用
上面的令牌中设置了过期时间,但是客户端不用关心这个时间,只要将返回的jwt发送给资源服务器即可,资源服务器会对其进行校验,如果校验结果中提示jwt已过期,则客户端重新获取可用的jwt重新请求即可。请求方式与token方式一样,使用Authorization:Bearer请求头,请求如下:
curl --location --request GET 'https://resourceendpoint.com/userInfo' \ --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkFCQ0RFRkcifQ.eyJpc3MiOiJtZSIsInN1YiI6InpoYW5nc2FuIiwiYXVkIjoiaHR0cHM6Ly9yZXNvdXJjZWVuZHBvaW50LmNvbS91c2VySW5mbyIsImV4cCI6NzIwMCwibmJmIjoxNjM2NDM4OTQxLCJpYXQiOjE2MzY0Mzg5NDEsImp0aSI6ImRmZGZkYWhrZmRoYXRlYXJlYXIifQ.YrJK74vkMOV2SFn3pNeuk2qoz4RGRDuwc4X9dyLsR94'
2.3.3 资源服务器验证
资源服务器接收到上面的请求之后,先从Bearer中解析出JWT,然后按照颁发时的逆向流程进行解析,取出PayLoad,并验证里面内容,验证通过就能返回客户端请求的资源了。验证方法也很简单,jose提供了验证方法:
public Boolean jwtWithHs256Verify(String jwt) throws JoseException { JsonWebSignature jsonWebSignature = new JsonWebSignature(); jsonWebSignature.setCompactSerialization(jwt); jsonWebSignature.setKey(new HmacKey(SECRET.getBytes())); return jsonWebSignature.verifySignature(); }
三、令牌的保护:JOSE
JOSE(JSON Object Signature and Encryption)即JSON对象的签名与加密标准。这套规范以JSON为基础数据模型,提供了签名(JSON Web 签名,简称JWS)、加密(JSON Web 加密,简称JWE)以及密钥存储格式(JSON Web 密钥,简称JWK)的标准
3.1 使用HS256的对称签名
对称签名的实现方式中,授权服务器和资源服务器使用共享密钥,授权服务器进行签名,资源服务器验证签名,在上文的例子中使用的就是HS256对称签名,这里不再赘述。需要注意的一点是:因为使用的是公共密钥,所以授权服务器和资源服务器都能生成令牌!
3.2 使用RS256的非对称签名
使用RS256算法时(底层使用的是RSA算法),授权服务器会拥有一对密钥对:公钥和私钥,授权服务器使用私钥加密,资源服务器从授权服务器获取公钥,用于验证令牌,但是因为获取不到私钥,也就无法生成令牌!
首先,需要在授权服务器上添加一对公钥和私钥。
public String jwk() throws JoseException { return RsaJwkGenerator.generateJwk(2048).toJson(JsonWebKey.OutputControlLevel.INCLUDE_PRIVATE); }
返回格式如下:
{ "kty": "RSA", "n": "sM1GB03U8yzrCkPSw7lAmwfhZtQ3q0w72yx4ydPcbvAvasKSoDB-tAaec_rHfaRzKiQNsTzmA0FQbk625mOXY9lClzT5J9P8XmqhUIlEqH04-PLsizqvWFoKlfsFwv4GEh-k2w3VAo9XBDHtnkThnugbvsM8QOGtBpZ-cN5oiXe9jb-ljUawtqjCk39idLc6mPogEC-uipYe0Aiv_W79WSZ8RtOuh3R8_RN8Sr9ukzR6_lfYx5joX41I_UEPBTyMyku2eFk87BUAJZZTUe25bmnC6yhO09noKhvYkZfsMtLfeY3luHTyWd88gRu62h5RHlss-JW50FqWM-vdTTG6gw", "e": "AQAB", "d": "FmmvWvWu7TTghuiaK12spvqUxGhatkhvvhUhKtTEuPuRx0LrO4tqRIAiTimYaIEUaF8xrSo_LmJ1Q8aOwR4W7v13x5tbioUBFScHVCJSpdlaA5UoD25dFCI1_VVZIaL6Ognw6CQUwMJTEaESsmGhCHf8LG6rkL4LJS6m0MAhGGvzEWUGSTWE3zK8qcZCnpZ73Aa0l4fq_d4D2U4k1rLrrjZF49SvOhcwaorSptsnPvsrzlMzcNv2bMTekHb-lPMWlbooJRu1OWAjM-6hQq1OrcgOD_EvaY1IUFUtFtK8TUJ_mxqdYg9zJLK5ywNmZAUWS7s3b-uM1sYOh3SuoIbnwQ", "p": "9Hp25bFhwLJIOA7wmEdEJ0aWVuwNshbCcio4Oo9IsluVvV9f12LUYWOE4JcKCo_eTnPcvWF3Pw9P8LNydGeq4hdzp8smZESgrtWP4t6Lpe12ssVd8cCqr_0ynnC6R7WUk9lQOranQwhIqYH4I1zx5D78QF0sLtp2DKXiNXBBcrk", "q": "uSJRgqfjBk3ZVb_A0auBTDArUOYetCuRjpgx4xnZzTxdd_fEG7XeRjFOmivThfcu5KMplM2PgN0EqxMIr7Gbqi4y9ivossgTLNO6Wre12PxdznP1B5M5g-1jkz3WBxfXeW1kO1FG7iyN3p8WPKsKVewE-VQzx8bX7Nn8pljDKRs", "dp": "n3w4jhUOYQesxy0v1RdApaKNtrydHp1sUc-rCMCqOvg2Eeji-_5T8AhdCapeeY9rBaDd0ol_ohqaGrrlonxyZLXJ1B9ZtzVx4TwednCZhzAHLA5G_8uhTdeOKv_89YTGHUE57mNzb-46gKHxvxgGENDp_A8MILCRLCUXEadeerk", "dq": "ImvQDePbIPvucbQCTLl_g8Pc-eCfSs5i9Mk1VU0kIrWbh0eozaIl3pUiUSXe4SSRMm9ntsP1b3cofApA7jGuiJioXv7Q-BSdBBOlrWJEzEA3zL_gifUEl5PWlLTFi3ISXQBKx4CYGIZuJjsb7lG6zTjhv9249ubwlJf_EoqkVos", "qi": "Vk8GYmY2egPuQMxqgntJX2SePvzf5QtLR2pqoDk2wEVNI50gzARCR_Qe7xzqe-QPDMprggN0o3uSwLU6SDelsa1TbfgQmHSq18rQgMrOk3zvAAZsGAP9Fll1V72zGj4TGvlwRr3RkUVhTQ5-yvTBgQa_QtHCT_fHQRYhSh9b8Fc" }
单独的公钥格式如下:
{ "kty": "RSA", "n": "jqMnbkOZfvwFQj1bvCu-TEKRT0at_pMXQ5WF_XVsZ4MaFgadSdzk7HydOWpVNPhhXzjY_Xsg_nS5rFPgYaSoKxhkKZrU6faSGI6qc_xChoUvAPgmkkQgXdwi3ceios4MfeK0Ktlxge-o71Ig5iGRMoe5q0oYdE6OxVD1JtJ564xgJKSwODLAt0AKGf5NVPLdu2LFMZcx3pf757fUEp0sZr0jl83nF7obUv1-hHdtwcGIFycrbwVHEPIHJl08uaQGwE-ve3-9ilCgw38-yl6w6adY2VIppxCq4qQdDBR2o2qFDBhJMDJyM_Lu2UF6t5ryLGjAEpKqbBlB7btnfbsgKQ", "e": "AQAB" }
除此之外,密钥对中返回的都是私钥的部分。接下来,使用私钥对对令牌进行签名。
public Map<String, Object> jwtWithRs256(String sub, String[] aud, String jid) throws JoseException { JsonWebSignature jws = new JsonWebSignature(); //设置Header jws.setHeader("alg", "RS256"); jws.setHeader("typ", "JWT"); jws.setHeader("kid", "ABCDEFG"); //设置PayLoad JwtClaims jwtClaims = new JwtClaims(); jwtClaims.setIssuer("rsa-me"); jwtClaims.setSubject(sub); jwtClaims.setAudience(aud); jwtClaims.setExpirationTime(NumericDate.fromSeconds(2 * 60 * 60)); jwtClaims.setNotBefore(NumericDate.now()); jwtClaims.setIssuedAt(NumericDate.now()); jwtClaims.setJwtId(jid); jws.setPayload(jwtClaims.toJson()); //使用私钥进行签名 RsaJsonWebKey jwk = RsaJwkGenerator.generateJwk(2048); jws.setKey(jwk.getPrivateKey()); String jwt = jws.getCompactSerialization(); HashMap<String, Object> result = new HashMap<>(); result.put("jwt", jwt); return result; }
返回结果如下:
{ "jwt": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkFCQ0RFRkcifQ.eyJpc3MiOiJyc2EtbWUiLCJzdWIiOiJIZWxsb1dvcmxkIiwiYXVkIjoiaHR0cHM6Ly9yZXNvdXJjZWVuZHBvaW50LmNvbS91c2VySW5mbyIsImV4cCI6NzIwMCwibmJmIjoxNjM2NDQ0NzA4LCJpYXQiOjE2MzY0NDQ3MDgsImp0aSI6IkdISlNTSkpTRCJ9.WrUdvwcj8u09DOl3iMYT1Sc7ePEEB8vpbIZF31elJTu8mvr7HOWw1OgEVClvI9I82QyoqpgPR7_IWZ9r8p3o632W0C6jrYVE8Z_VRfHG-WB07yecOEUn730i3lHUC_wqZU3giUSdIBYiIrXinHHSSFuCzw3rPUwk0BougLza8b8uyrBRdtQ8toEjMmJtGF-l9wQayMVC6B8oAZKXkDt3CSIO1t_m9W6mGcFasH63P12TuGPZznrVcd41yn4hHHRMcrhVpEgs06ufFvdgmDRipuakec5DlV2ki8h0DYqwUUDlAZQtfdprC1QZVwA7tmeVYStnST1GQDy7hxrWbmqUyw" }
将上面的jwt复制到jwt官网的工具中,就能看到如下结果,从结果来看,解析之后PayLoad中设置的信息与在代码中设置的一模一样,如果将上面获取到的公钥复制到公钥输入框中,就能看到签名成功的提示,自测通过!
当客户端接收到上面的token之后,向资源服务器发送请求:
curl --location --request GET 'https://resourceendpoint.com/userInfo' \ --header 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkFCQ0RFRkcifQ.eyJpc3MiOiJyc2EtbWUiLCJzdWIiOiJIZWxsb1dvcmxkIiwiYXVkIjoiaHR0cHM6Ly9yZXNvdXJjZWVuZHBvaW50LmNvbS91c2VySW5mbyIsImV4cCI6NzIwMCwibmJmIjoxNjM2NDQ0NzA4LCJpYXQiOjE2MzY0NDQ3MDgsImp0aSI6IkdISlNTSkpTRCJ9.WrUdvwcj8u09DOl3iMYT1Sc7ePEEB8vpbIZF31elJTu8mvr7HOWw1OgEVClvI9I82QyoqpgPR7_IWZ9r8p3o632W0C6jrYVE8Z_VRfHG-WB07yecOEUn730i3lHUC_wqZU3giUSdIBYiIrXinHHSSFuCzw3rPUwk0BougLza8b8uyrBRdtQ8toEjMmJtGF-l9wQayMVC6B8oAZKXkDt3CSIO1t_m9W6mGcFasH63P12TuGPZznrVcd41yn4hHHRMcrhVpEgs06ufFvdgmDRipuakec5DlV2ki8h0DYqwUUDlAZQtfdprC1QZVwA7tmeVYStnST1GQDy7hxrWbmqUyw'
当资源服务器接收到请求之后,从header中解析中token,并使用公钥验证签名:
//校验签名 JsonWebSignature jsonWebSignature = new JsonWebSignature(); jsonWebSignature.setKey(jwk.getPublicKey()); jsonWebSignature.setCompactSerialization(jwt); boolean verify = jsonWebSignature.verifySignature();
当签名验证通过后,再根据客户端请求返回资源即可。
3.3 其他的保护措施
上面提到的HS256对称签名算法,是为令牌的内容计算256字节的散列,JOSE还定义了HS384和HS512等,它们计算出的散列值更长,从而生成更安全可靠的令牌;RS256非对称签名算法是对RAS签名结果计算256字节的散列,同样JOSE定义了RS384和RS512,除此之外,还要有PS256、PS384和PS512等,都是基于另一种RAS签名和散列机制,从源代码中可以看出jose4j库中的签名算法有以下这些:
除了签名之外,JOSE还提供了一种叫做JWE的加密机制,包含集中不同的选项和算法。经过JWE加密的JWT不再只由3部分组成,而是由5部分组成,各个部分仍然使用Base64URL编码,这是载荷变成了一个经过加密的对象,没有正确的密钥无法读取其中的内容。
四、令牌内省
如果将所有的信息都放入令牌中也不是一种很好的办法,因为令牌在网络中传输的频率很高,塞过多的信息只会增加网络开销,且一旦颁发很难撤销,于是就有了令牌内省机制。
4.1 内省协议
令牌内省协议定义了一种机制,让受保护资源能够主动向服务器查询令牌状态。由于令牌时授权服务器颁发,所以它知道令牌的所有细节。内省请求是发送给授权服务器内省端点的表单形式的HTTP请求,相当于受保护资源向授权服务器询问:“有人向我出示了这个令牌,它是否有效?”受保护资源在请求过程中需要向授权服务器进行身份认证,以便让授权服务器知道是谁在询问,并可能根据询问者的身份返回不同的响应。
资源服务器请求授权服务器的方式与客户端请求授权服务器的方式相似,只不过授权服务器要给资源服务器提前分配好用户身份认证的resoure_id和resource_secret.
4.2 构建内省端点
为了支持令牌内省,授权服务器需要构建内省端点,为此需要完成以下步骤:
1、为资源服务器添加凭据,用户进行身份认证
- resource_id:资源服务器唯一标识
- resource_secret:资源服务器密钥
2、从资源服务器的请求中解析出其身份信息
3、授权服务器获取到凭据之后,先对资源服务器进行身份认证,认证失败直接返回错误信息,认证成功进行下一步
4、根据资源服务器传过来的token查找令牌,如果令牌蹲在则将所有的信息添加到响应中,并以JSON的形式返回,如果没有找到则返回无效通知。
4.3 发起令牌内省请求
现在资源服务器就可以发起类似下面的内省请求了:
curl --location --request POST 'http://tokenendpoint.com/introspect' \ --header 'Content-Type: application/json' \ --header 'Authorization: basic BASE64(resourceId:resourceSecret)' \ --data-raw '{ "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IkFCQ0RFRkcifQ.eyJpc3MiOiJtZSIsInN1YiI6InpoYW5nc2FuIiwiYXVkIjoiaHR0cHM6Ly9yZXNvdXJjZWVuZHBvaW50LmNvbS91c2VySW5mbyIsImV4cCI6NzIwMCwibmJmIjoxNjM2NDQwNjU1LCJpYXQiOjE2MzY0NDA2NTUsImp0aSI6ImRmZGZkYWhrZmRoYXRlYXJlYXIifQ.2d4J1MIkT6AVzIoLzdEIUFnkpjisu_7tPfd_mCHvPg0" }'
令牌内省结果:
{ "active": true, "scope": "read", "client_id": "client_id", "username": "zhangsan", "iss": "rsa-me", "sub": "HelloWorld", "aud": "https://resourceendpoint.com/userInfo", "exp": 7200, "nbf": 1636445486, "iat": 1636445486 }
内省协议中规定内省响应结果是在JWT的基础上增加了几个声明,最重要的是active,此声明表示当前令牌在授权服务器上是否有效,且是唯一必须返回的声明。
4.4 令牌内省与JWT结合
一般情况下最好将JWT和令牌内省结合起来,JWT只需要携带基本信息,比如颁发者、有效期唯一标识等,用于初步检查,检查通过后执行令牌内省来获取更详细的令牌信息,比如提供授权的用户、被颁发令牌的客户端以及令牌所关联的权限范围等。
五、令牌撤回
OAuth令牌撤销规范,给客户端提供了一种撤销令牌的机制,使得客户端在任何想要撤销的地方,都能调用授权服务器提供的撤销API。比如,客户端可能是一个要从用户设备上卸载的原生应用、或者客户端给用户一共了撤销授权的操作界面、甚至在客户端发现存在可疑行为并希望降低对已授权的资源的损害,只要撤回令牌,则授权服务器之前颁发的令牌将不能被使用。
5.1 令牌撤回协议
OAuth 令牌撤回协议是一个简单的协议,它让客户端可以简单地告诉授权服务器:“我持有这个令牌并希望你将它撤销”,客户端需要向撤销端点发送附带身份认证的HTTP POST请求,并将要撤回的令牌传入请求体。
5.2 构建撤回端点
在授权服务器上,接收到请求后首先获取客户端身份信息并对其进行身份认证,当客户端身份认证通过之后,根据客户端传来的token查询该令牌是否存在,如果存在且确认是颁发给该客户端的令牌之后,将其从数据库中移除即可。
5.3 发起令牌撤回请求
构建完之后,客户端就能发起撤回请求,与令牌内省的请求相同,只不过是客户端发起的,请求示例如下:
curl --location --request GET 'http://tokenendpoint.com/revoke' \ --header 'Content-Type: application/json' \ --header 'Authorization: basic BASE64(clientId:clientSecret)' \ --data-raw '{ "token":"mCHvPg0" }'
客户端可以以同样的方式撤回刷新令牌!
六、OAuth令牌的生命周期
关于令牌的生命周期,话不多说,一张图说明一切
七、总结
OAuth令牌时OAuth系统中最重要的中心组件:
- OAuth令牌可以是任意格式,只要授权服务器和受保护资源能够理解即可
- OAuth客户端没有必要理解令牌的格式
- JWT定义了一种在令牌中存放结构化信息的方式
- JOSE提供了对令牌内容进行加密保护的方法
- 令牌内省让受保护资源可以在运行时查询令牌状态
- 令牌撤回让客户端可以向授权服务发送信号,将不再需要的令牌废弃掉,结束令牌的生命周期。
至此,OAuth中令牌的介绍也差不多了,我想说,写博客真不是一件简单的事情,写了一整天啊~~
本文来自博客园,作者:bug改了我,转载请注明原文链接:https://www.cnblogs.com/hellowhy/p/15527295.html