OAuth2 + JWT 授权认证服务

OAuth2 + JWT 授权认证服务

参考阅读

JWT 简单介绍

JWT长什么样

JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。就像这样:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDg0OTA1NjMsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJqdGkiOiI3YjNiNTBlMC1hMjhjLTQ4NTQtYWNhMi0zMmQwZTIyZTY1ZmUiLCJjbGllbnRfaWQiOiJhYmMiLCJzY29wZSI6WyJhbGwiXX0.hPhvl1XbBMjoPER7BggWD1UO83o8SNtgg15rlUBndDo

JWT的构成

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).

jwt的头部承载两部分信息
  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HMAC SHA256

完整的头部就像下面这样的JSON:

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

然后将头部进行base64编码,构成了第一部分

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

playload

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

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

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

私有的声明

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

定义一个payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"exp": 1508490563,
"user_name": "admin",
"authorities": [
"ROLE_ADMIN",
"ROLE_USER"
],
"jti": "7b3b50e0-a28c-4854-aca2-32d0e22e65fe",
"client_id": "abc",
"scope": [
"all"
]
}

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

1
eyJleHAiOjE1MDg0OTA1NjMsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJqdGkiOiI3YjNiNTBlMC1hMjhjLTQ4NTQtYWNhMi0zMmQwZTIyZTY1ZmUiLCJjbGllbnRfaWQiOiJhYmMiLCJzY29wZSI6WyJhbGwiXX0

signature

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

  • header (base64后的)
  • payload (base64后的)
  • secret

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

1
2
3
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret');

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

注意

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

OAuth2 介绍及使用

名词定义

  • Third-party application:第三方应用程序,本文中又称”客户端”(client),即上一节例子中的”云冲印”。
  • HTTP service:HTTP服务提供商,本文中简称”服务提供商”,即上一节例子中的Google。
  • Resource Owner:资源所有者,本文中又称”用户”(user)。
  • User Agent:用户代理,本文中就是指浏览器。
  • Authorization server:认证服务器,即服务提供商专门用来处理认证的服务器。
  • Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。

OAuth的思路

OAuth在”客户端”与”服务提供商”之间,设置了一个授权层(authorization layer)。”客户端”不能直接登录”服务提供商”,只能登录授权层,以此将用户与客户端区分开来。”客户端”登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。
“客户端”登录授权层以后,”服务提供商”根据令牌的权限范围和有效期,向”客户端”开放用户储存的资料。

运行流程

  • A 用户打开客户端以后,客户端要求用户给予授权。
  • B 用户同意给予客户端授权。
  • C 客户端使用上一步获得的授权,向认证服务器申请令牌。
  • D 认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
  • E 客户端使用令牌,向资源服务器申请获取资源。
  • F 资源服务器确认令牌无误,同意向客户端开放资源。

客户端的授权模式

客户端必须得到用户的授权(authorization grant), 才能获得令牌(access token)

OAuth 2.0定义了四种授权方式:

  1. 授权码模式 (authorization code)
  2. 简化模式 (implicit)
  3. 密码模式 (resource owner password credentials)
  4. 客户端模式 (client credentials)

验证token接口 /oauth/check_token

参数 token: 访问令牌

1
http://localhost:8080/oauth/check_token?token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDg0NzcyNjgsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJqdGkiOiJmZTk1MTNiZS1kZjEwLTQ2MDEtYWM0Yy0zOTc4Njc2NzQ1M2IiLCJjbGllbnRfaWQiOiJhYmMiLCJzY29wZSI6WyJhcHAiXX0.AOWIDfMfqzvMhV1T6eYDVpnvU03vST8XpNY35gNrNnyy_DQcLtFB0n_KIYieG4CYaVGlKnMGGXL7L592Njx6xy3bCdmSHw5_4MZZxxWN9_hj96ZTvi4H5Yc3vVzlXNHIvTTJxUsst15EbmN5s5ZBlZptv_3T70bG6x1iq6sbmMQ8zjHRgSKsZM1hvZ5pAO6BwYmPgIKFDPFtxB36I0LegLypUmzjgUFU5dAfgY-w00yyOf9aNooisM22CmbR0QXg3NAlzeufe_Jjf5W0jBTRzdhnVFreRUeOMMwlyKUb_rXUf53Yx0AXhcEhZYtYaYQpDyGxazcoO_VWSgM89S3nQuf23r20gp-jxgElNvRUnPhbO_jIuOcn2FQlvm-L37FfzlO6cK7BGqegE4ItVERfchznWFPq4Jm98IEKV5mQgAcutrE393uIL2137cNzfdg-DZ5HjFoPeEpiJ_ZL7IydxXgOsVTgYsLZ8Ccl0mO51kOCRC4tRBQxXVS9vtRQtp0TVjGTpdXK7SfLyohK1Ga0TXOA76HZv_nAoKy1BRg-i2bejV900g7-nkQGgthPdZbFk4rylXZe6t8grxCIDgtdYFGNLXiMjmnUYU9MJ35AFc1yGhqjwMX8lzdGsTNPLwNeKq9rt82rZxZuuKpiK3ph4LZgnQX5th6XiNBZDfnRz1M

响应内容 JSON

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"exp":1508477268,
"user_name":"admin",
"authorities":[
"ROLE_ADMIN",
"ROLE_USER"
],
"jti":"fe9513be-df10-4601-ac4c-39786767453b",
"client_id":"abc",
"scope":[
"all"
]
}

刷新token接口 /oauth/token

1
curl -d 'grant_type=refresh_token&refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiI4MzRmY2FmOS1hOGVhLTQwYWItYjkxMi1jNjU3NDFmOWJkYmMiLCJleHAiOjE1MDg0ODY2MDMsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiXSwianRpIjoiNWJkYmEzNTctZTRjOS00MzdjLWE2Y2EtODdhM2FiNGVlZTUzIiwiY2xpZW50X2lkIjoiYWJjIn0.JEttthKb3Laj6iNeMZFvnj_2CHLW5WjzQF0RA5RMyGs&client_id=abc&client_secret=aaaabbbb' "http://localhost:8080/oauth/token"

响应内容 JSON

1
2
3
4
5
6
7
8
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDg0ODY5NzAsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJqdGkiOiI2ZTQ2Yzg4Yi01YzczLTQxNTktYWY1ZC01YjFlOTVlMGQ2MjIiLCJjbGllbnRfaWQiOiJhYmMiLCJzY29wZSI6WyJhbGwiXX0.3OAl_Tjz-btYjQz5PxrYqc5I1T2iNYPcPrzXeog9eqE",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiI2ZTQ2Yzg4Yi01YzczLTQxNTktYWY1ZC01YjFlOTVlMGQ2MjIiLCJleHAiOjE1MDg0ODY2MDMsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiXSwianRpIjoiNWJkYmEzNTctZTRjOS00MzdjLWE2Y2EtODdhM2FiNGVlZTUzIiwiY2xpZW50X2lkIjoiYWJjIn0.IvO62nDVawLiVv3oqkjS_P2vlsSntviVltULCcOXQzw",
"expires_in": 3599,
"scope": "all",
"jti": "6e46c88b-5c73-4159-af5d-5b1e95e0d622"
}

授权码模式 (authorization code)

授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与”服务提供商”的认证服务器进行互动。

  • A 用户访问客户端,后者将前者导向认证服务器。
  • B 用户选择是否给予客户端授权。
  • C 假设用户给予授权,认证服务器将用户导向客户端事先指定的”重定向URI”(redirection URI),同时附上一个授权码。
  • D 客户端收到授权码,附上早先的”重定向URI”,向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
  • E 认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。

获取code接口 /oauth/authorize

客户端申请认证的URI,包含以下参数:

  • response_type:表示授权类型,必选项,此处的值固定为”code”
  • client_id:表示客户端的ID,必选项
  • redirect_uri:表示重定向URI,可选项
  • scope:表示申请的权限范围,可选项
  • state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。

服务器回应客户端的URI,包含以下参数:

  • code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。
  • state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
1
2
GET http://localhost:8080/oauth/authorize?client_id=abc&response_type=code&redirect_uri=http://localhost:8080/resources/roles
授权之后 302 http://localhost:8080/resources/roles?code=zmpbg2

获取token接口 /oauth/token

D 步骤中,客户端向认证服务器申请令牌的HTTP请求,包含以下参数:

_ grant_type:表示使用的授权模式,必选项,此处的值固定为”authorization_code”。
_ code:表示上一步获得的授权码,必选项。
_ redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
_ client_id:表示客户端ID,必选项。

1
curl -d 'grant_type=authorization_code&redirect_uri=http://localhost:8080/resources/roles&client_id=abc&client_secret=aaaabbbb&code=zmpbg2' "http://localhost:8080/oauth/token"

Response Header content-type: application/json;charset=UTF-8

1
2
3
4
5
6
7
8
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDg0ODEzOTAsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJqdGkiOiIwMDE3NGQyNC1jNWVhLTRhN2ItOTYxZS1jNDdkN2Y4YTNhYmQiLCJjbGllbnRfaWQiOiJhYmMiLCJzY29wZSI6WyJhbGwiXX0.cCAJ-r1qARUQCYjwLswWhui7g48lD_MPaarkPyIZMkM",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiIwMDE3NGQyNC1jNWVhLTRhN2ItOTYxZS1jNDdkN2Y4YTNhYmQiLCJleHAiOjE1MDg0ODEzOTAsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiXSwianRpIjoiMTZjMmNiNzktNDE5My00Y2ZiLWJhODgtZGMyMDU5MzRiNGEzIiwiY2xpZW50X2lkIjoiYWJjIn0.mAZSTghSNFEaoV22jwOQjeD35KVa6Sb-zhxYM_ppimE",
"expires_in": 3599,
"scope": "all",
"jti": "00174d24-c5ea-4a7b-961e-c47d7f8a3abd"
}

简化模式 (implicit)

简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了”授权码”这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。

  • A 客户端将用户导向认证服务器。
  • B 用户决定是否给于客户端授权。
  • C 假设用户给予授权,认证服务器将用户导向客户端指定的”重定向URI”,并在URI的Hash部分包含了访问令牌。
  • D 浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。
  • E 资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。
  • F 浏览器执行上一步获得的脚本,提取出令牌。
  • G 浏览器将令牌发给客户端。

获取token接口 /oauth/token

A步骤中,客户端发出的HTTP请求,包含以下参数:

  • response_type:表示授权类型,此处的值固定为”token”,必选项。
  • client_id:表示客户端的ID,必选项。
  • redirect_uri:表示重定向的URI,可选项。
  • scope:表示权限范围,可选项。
  • state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。

C步骤中,认证服务器回应客户端的URI,包含以下参数:

  • access_token:表示访问令牌,必选项。
  • token_type:表示令牌类型,该值大小写不敏感,必选项。
  • expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
  • scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
  • state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数。
1
2
3
http://localhost:8080/oauth/authorize?client_id=abc&response_type=token&redirect_uri=http://localhost:8080/resources/roles
授权之后 302
http://localhost:8080/resources/roles#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDg0Nzc3MDIsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJqdGkiOiI4ZTA2ZGFlZi0xMGVkLTQ3ZmYtOWY3Ni04ZjM2YmFmNDNiNjciLCJjbGllbnRfaWQiOiJhYmMiLCJzY29wZSI6WyJhcHAiXX0.7ygi5Z64CvZnjThSf2IlZ1PdLkOOTPQbiqXYrbhg8wM&token_type=bearer&expires_in=3599&scope=app&jti=8e06daef-10ed-47ff-9f76-8f36baf43b67

密码模式 (resource owner password credentials)

密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向”服务商提供商”索要授权。
在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。

  • A 用户向客户端提供用户名和密码。
  • B 客户端将用户名和密码发给认证服务器,向后者请求令牌。
  • C 认证服务器确认无误后,向客户端提供访问令牌。

获取token接口 /oauth/token

B步骤中,客户端发出的HTTP请求,包含以下参数:

  • grant_type:表示授权类型,此处的值固定为”password”,必选项。
  • username:表示用户名,必选项。
  • password:表示用户的密码,必选项。
  • scope:表示权限范围,可选项。
1
curl -d 'grant_type=password&username=admin&password=admin&client_id=abc&client_secret=aaaabbbb' "http://localhost:8080/oauth/token"

响应内容 JSON

1
2
3
4
5
6
7
8
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDg0OTAyNTgsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJqdGkiOiIyY2NjYzUyZC0xMTg2LTQ1NzMtOWEyZC1mNzQ3NzM4MTIyZWIiLCJjbGllbnRfaWQiOiJhYmMiLCJzY29wZSI6WyJhbGwiXX0.4ql81lMcnBVUarwIVqcJBGTEQdCMrl1iSdoUsehuH8Y",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiIyY2NjYzUyZC0xMTg2LTQ1NzMtOWEyZC1mNzQ3NzM4MTIyZWIiLCJleHAiOjE1MDg0OTAyNTgsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiXSwianRpIjoiNjE2N2I3MjEtZTYyYS00ZjViLWI2YmQtMWQyZDY3OTIyYWYwIiwiY2xpZW50X2lkIjoiYWJjIn0.y-UBydaYeAbw0ctD3OseHfpT5WrHvpw1bZkVihPGLrY",
"expires_in": 3599,
"scope": "all",
"jti": "2cccc52d-1186-4573-9a2d-f747738122eb"
}

客户端模式 (client credentials)

客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向”服务提供商”进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求”服务提供商”提供服务,其实不存在授权问题。

  • A 客户端向认证服务器进行身份认证,并要求一个访问令牌。
  • B 认证服务器确认无误后,向客户端提供访问令牌。

获取token接口 /oauth/token

A步骤中,客户端发出的HTTP请求,包含以下参数:

  • granttype:表示授权类型,此处的值固定为”clientcredentials”,必选项。
  • scope:表示权限范围,可选项。
1
curl -d 'grant_type=client_credentials&client_id=abc&client_secret=aaaabbbb' "http://localhost:8080/oauth/token"

响应内容 JSON

1
2
3
4
5
6
7
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJhbGwiXSwiZXhwIjoxNTA4NDg3MzAzLCJqdGkiOiJiYTBmMDVmOS1iYTY1LTQ5MTItOGMzOC1lMWRmYjU1NWZkZTAiLCJjbGllbnRfaWQiOiJhYmMifQ.Shxb0filgBLtQEfspWpmTYfrzFbkC48DX4lSrMxRvX4",
"token_type": "bearer",
"expires_in": 3599,
"scope": "all",
"jti": "ba0f05f9-ba65-4912-8c38-e1dfb555fde0"
}