OAuth

micro-services

# 一、OAuth2.0协议

OAuth2.0协议 (opens new window)

OAuth 2.0 文档 (opens new window)

OAuth Open Authorization

OAuth(开放授权)是一个开放标准,为用户资源的授权提供了一个安全、开放而又简易的标准。

允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码供给第三方应用或分享他们数据的所有内容。

很大公司如Google,Yahoo,Microsoft等都提供了OAuth认证服务,这些都足以说明OAuth标准逐渐成为开放资源授权的标准。

OAuth协议目前发展到2.0版本。1.0版本过于复杂,2.0版本已得到广泛应用。OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0,即完全废止了OAuth1.0。

简单来说,OAuth是一种授权协议(一系列的接口),它的主要作用是未来提供认证和授权的标准。

而其实现方案有:

  • Spring Security OAuth2
  • Shiro OAuth2
  • 自定义实现OAuth2

# OAuth2.0特点

  • 简单:不管是OAuth服务提供者还是应用开发者,都很易于理解与使用;
  • 安全:没有涉及到用户密钥等信息,更安全更灵活;
  • 开放:任何服务提供商都可以实现OAuth,任何软件开发商都可以使用OAuth;

# OAuth应用场景

  • 原生App授权:App登录请求后台接口,为了安全认证,所有请求都带Token信息,如果登录验证、请求后台数据。

  • 前后端分离单页面应用:前后端分离框架,前端请求后台数据,需要进行OAuth2安全认证,比如使用VUE、React后者H5开发的App。

  • 第三方应用授权登录,比如QQ,微博,微信的授权登录。

# OAuth角色

  • 资源所有者(resource owner):能够对受保护资源授予访问权限的实体。当资源所有者是⼀个⼈时,它被称为终端⽤户。
  • 资源服务器(resource server):托管受保护资源的服务器,能够接受和响应通过令牌对受保护的资源的请求。
  • 客户端(client):代表资源所有者及其授权进⾏受保护资源请求的应⽤程序。术语“客户端”并不暗⽰任何特定的实现特征(例如,应⽤程序是在服务器、台式机还是其他设备上执⾏)。
  • 授权服务器(authorization server):认证成功后,服务器向客户端发出访问令牌验证资源所有者并获得授权。

简单来说,OAuth角色对应关系:

  • 资源所有者——用户
  • 资源服务器——HTTP服务,用于授权
  • 客户端——第三方应用程序(Web、App、小程序...)
  • 授权服务器——认证服务器,HTTP服务,用于认证

OAuth defines four roles:

resource owner
   An entity capable of granting access to a protected resource.
   When the resource owner is a person, it is referred to as an
   end-user.

resource server
   The server hosting the protected resources, capable of accepting
   and responding to protected resource requests using access tokens.

client
   An application making protected resource requests on behalf of the
   resource owner and with its authorization.  The term "client" does
   not imply any particular implementation characteristics (e.g.,
   whether the application executes on a server, a desktop, or other
   devices).

authorization server
   The server issuing access tokens to the client after successfully
   authenticating the resource owner and obtaining authorization.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# OAuth协议流

Protocol Flow

     +--------+                               +---------------+
     |        |--(A)- Authorization Request ->|   Resource    |
     |        |                               |     Owner     |
     |        |<-(B)-- Authorization Grant ---|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(C)-- Authorization Grant -->| Authorization |
     | Client |                               |     Server    |
     |        |<-(D)----- Access Token -------|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(E)----- Access Token ------>|    Resource   |
     |        |                               |     Server    |
     |        |<-(F)--- Protected Resource ---|               |
     +--------+                               +---------------+

                     Figure 1: Abstract Protocol Flow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

图1所示的抽象OAuth 2.0流程描述了这四个角色之间的交互过程,包括以下步骤:

(A) 客户端(第三方应用)向资源所有者请求授权。这个可以直接向资源所有者发出授权请求(如图所示),或最好通过授权间接进行服务器作为中介。

(B) 客户端收到授权授予authorization grant,这是代表资源所有者授权的凭据。

(C) 客户端通过使用身份验证请求访问令牌授权服务器,并显示authorization grant。

(D) 授权服务器对客户端进行身份验证并验证authorization grant,如果有效,则发出访问令牌。

(E) 客户端从资源请求受保护的资源服务器,并通过提供访问令牌(access token)进行身份验证。

(F) 资源服务器验证访问令牌,如果有效,满足要求。

# 授权类型

Authorization Code 授权类型

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

OAuth 2.0 对于如何颁发令牌的细节,规定得非常详细。具体来说,一共分成四种授权类型(authorization grant),即四种颁发令牌的方式,适用于不同的互联网场景。

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

不管哪一种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。

# 1. 授权码模式

授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。

这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。

授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。

适用场景:

目前市面上主流的第三方验证都是采用这种模式。

实现步骤:

  1. A网站提供一个链接,用户点击后就会跳转到 B 网站,授权用户数据给 A 网站使用。下面就是 A 网站跳转 B 网站的一个示意链接。

    https://b.com/oauth/authorize? 
    response_type=code& #要求返回授权码(code) 
    client_id=CLIENT_ID& #让 B 知道是谁在请求 
    redirect_uri=CALLBACK_URL& #B 接受或拒绝请求后的跳转网址 
    scope=read # 要求的授权范围(这里是只读)
    
    1
    2
    3
    4
    5

    客户端申请授权的URI,包含以下参数:

    • response_type:表示授权类型,必选项,此处的值固定为"code"

    • client_id:表示客户端的ID,必选项

    • redirect_uri:表示重定向URI,可选项

    • scope:表示申请的权限范围,可选项

    • state:表示客户端的当前状态,可以指定任意值,授权服务器会原封不动地返回这个值。

  2. 用户跳转后,B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户表示同意,这时 B 网站就会跳回redirect_uri参数指定的网址。跳转时,会传回一个授权码,就像下面这样。

    https://a.com/callback?code=AUTHORIZATION_CODE #code参数就是授权码 
    
    1
  3. A 网站拿到授权码以后,就可以在后端,向 B 网站请求令牌。 用户不可见,服务端行为。

    https://b.com/oauth/token? 
    # client_id和client_secret用来让B确认A的身份,client_secret参数是保密的,因此只能在后端发请求
    client_id=CLIENT_ID& 
    client_secret=CLIENT_SECRET& 
    grant_type=authorization_code& # 采用的授权方式是授权码 
    code=AUTHORIZATION_CODE& # 上一步拿到的授权码 
    redirect_uri=CALLBACK_URL # 令牌颁发后的回调网址
    
    1
    2
    3
    4
    5
    6
    7
  4. B 网站收到请求以后,就会颁发令牌。具体做法是向redirect_uri指定的网址,发送一段 JSON 数据。

    { 
    "access_token":"ACCESS_TOKEN", # 令牌 
    "token_type":"bearer", 
    "expires_in":2592000, 
    "refresh_token":"REFRESH_TOKEN", 
    "scope":"read", 
    "uid":100101, 
    "info":{...} 
    } 
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

# 2. 简化(隐式)模式

有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。

RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌,这种方式没有授权码这个中间步骤,所以称为(授权 码)"隐藏式"(implicit)。

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

这种方式把令牌直接传给前端,是很不安全的。

因此,只能用于一些安全要求不高的场景,并且令牌的有效 期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。

它的步骤如下:

(A)客户端将用户导向授权服务器。

(B)用户决定是否给于客户端授权。

(C)假设用户给予授权,授权服务器将用户导向客户端指定的"重定向URI",并在URI的Hash部分包含了访问令牌。

(D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。

(E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。

(F)浏览器执行上一步获得的脚本,提取出令牌。

(G)浏览器将令牌发给客户端。

# 3. 密码模式

如果你高度信任某个应用,RFC 6749 也允许用户把用户名密码,直接告诉该应用。该应用就使用你的密码去申请令牌,这种方式称为"密码式"(password)。

在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度 信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。

而授权服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。

适用场景:自家公司搭建的授权服务器。

它的步骤如下:

(A)用户向客户端提供用户名和密码。

(B)客户端将用户名和密码发给授权服务器,向后者请求令牌。

(C)授权服务器确认无误后,向客户端提供访问令牌。

  1. A 网站要求用户提供 B 网站的用户名和密码,拿到以后,A 就直接向 B 请求令牌。整个过程中,客户端不得保存用户的密码。

    https://oauth.b.com/token? 
    grant_type=password& # 授权方式是"密码式" 
    username=USERNAME& 
    password=PASSWORD& 
    client_id=CLIENT_ID 
    
    1
    2
    3
    4
    5
  2. B 网站验证身份通过后,直接给出令牌。注意,这时不需要跳转,而是把令牌放在 JSON 数据里面,作为 HTTP 回应,A 因此拿到令牌。

# 4. 客户端模式

客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行授权。

适用于没有前端的命令行应用,即在命令行下请求令牌。一般用来提供给我们完全信任的服务器端服务。

它的步骤如下:

(A)客户端向授权服务器进行身份认证,并要求一个访问令牌。

(B)授权服务器确认无误后,向客户端提供访问令牌。

  1. A 应用在命令行向 B 发出请求。

     https://oauth.b.com/token? 
     grant_type=client_credentials& 
     client_id=CLIENT_ID& 
     client_secret=CLIENT_SECRET 
    
    1
    2
    3
    4
  2. B 网站验证通过以后,直接返回令牌。

# Oauth令牌

令牌 (Tokens) :授权服务器在接收到客户请求后,颁发的访问令牌。

作用域 (Scopes) :客户请求访问令牌 时,由资源拥有者 额外指定的细分权限(permission)。

# Oauth2令牌类型

  • 授权码(Authorization Code Token) :仅用于授权码授权类型,用于交换获取访问令牌和刷新令牌。
  • Bearer Token:不管谁拿到Token都可以访问资源。
  • Proof of Possession(PoP) Token: 可以校验client是否对Token有明确的拥有权。
  • 刷新令牌(Refresh Token): 用于去授权服务器获取一个新的。
  • 访问令牌(Access Token):用于代表一个用户或服务直接去访问受保护的资。

# 令牌的使用

通过账户、密码登录,oauth通过账户、密码、client、secret等信息会生成access_token和refresh_token。

默认颁发12小时有效期的 access_token 和30天有效期的 refresh_token。

A 网站拿到令牌以后,就可以向 B 网站的 API 请求数据了。 此时,每个发到 API 的请求,都必须带有令牌。具体做法是在请求的头信息,加上一个Authorization字 段,令牌就放在这个字段里面。

curl ‐H "Authorization: Bearer ACCESS_TOKEN" "https://api.b.com" 
1

也可以通过添加请求参数access_token请求数据。

每次请求,鉴权中心(auth服务)都会进行校验令牌的合法性,校验通过后则可以携带该令牌访问所有已授权的资源。

当 access_token 过期后(或者快要过期的时候), 使用 refresh_token 获取一个新的 access_token, 直到 refresh_token 过期, 用户重新登录, 这样整个过程中,用户只需要登录一次, 用户体验好。refresh_token 的有效期即表示用户多久需要通过账户密码登录一次。

  • access_token 泄露了怎么办? 没关系, 它很快就会过期。

  • refresh_token 泄露了怎么办? 没关系, 使用 refresh_token 是需要客户端秘钥 client_secret 的。

# Access Token

访问令牌(Access Token)是客户端访问资源服务器的令牌。拥有这个令牌代表着得到用户的授权。

但该授权是临时的,有一定的时效性。因为在使用该令牌过程种可能发送泄露,所以需要给Access Token限定一个较短的有效期,从而降低使用风险。

oAuth2.0中access_token默认有效时长为12个小时

access_token 可以一直使用, 直到 access_token 过期, 然后重复, 这种是不安全的, access_token 的时效太长, 也就失去了本身的意义。

例:可设定Access Token为2小时

access_token_validity = 7200
1

而当有了有效期之后,客户端使用就不那么方便了,每当Access Token过期后,客户端就必须向资源服务器重新索取用户授权,非常影响用户体验。

基于以上现象,Oauth2.0引入了Refresh Token机制。

# Refresh Token

刷新令牌(Refresh Token)的作用是用来刷新Access Token。该值的有效期可设置较长。

oAuth2.0中refresh_token默认时长为30天

例:可设定Refresh Token值为30天

refresh_token_validity = 2592000
1

认证服务器提供一个刷新令牌的接口:

${oauth_serv_uri}/oauth/refresh?refresh_token=&client_id=
1

传入refresh_tokenclient_id,认证服务器验证通过后,返回一个新的Access Token。

为了安全Oauth2.0引入了2个措施:

  • Oauth2.0要求,Refresh Token一定是保存在客户端的服务器上(后台服务),不能在客户端进行存储;
  • Oauth2.0引入了client_secret机制。每一个client_id都对应唯一的client_secret。该值会在客户端申请client_id时,一起分配给客户端。客户端必须把该值保管在客户端的服务器上。访问刷新令牌接口时,会验证该client_secret。

# /oauth/token源码分析

HTTP请求获取access_token

  1. 访问鉴权中心服务(auth服务),oauth2提供的HTTP接口/oauth/token(POST),获取access_token;

  2. 请求所需参数:client_id、client_secret、grant_type、username、password;

    源码解读:

      (1. 进入地址/oauth/token,即TokenEndpoint的postAccessToken方法中;
    
      (2. 根据clientId,从oauth_client_details表中获取到ClientDetails对象;即JdbcClientDetailsService的loadClientByClientId方法;
    
      (3. 校验scopes;
    
      (4. 调用AbstractTokenGranter中的grant方法,然后通过DefaultTokenServices类从tokenStore中获取OAuth2AccessToken对象。
    
      (5. 若已存在,tokenStore.getAccessToken直接返回;
    
      (6. 若无通过DefaultTokenServices.createAccessToken进行创建,并存储至RedisTokenStore(tokenStore.storeAccessToken、tokenStore.storeRefreshToken);
    
      (7. 最后将OAuth2AccessToken对象包装进响应流返回。
    

参考文档:http://t.zoukankan.com/lexiaofei-p-7152326.html (opens new window)

# oauth_client_details表

字段说明:

字段 说明 描述
client_id 客户端的id 用于唯一标识每一个客户端(client);注册时必须填写(也可以服务端自动生成),这个字段是必须的,实际应用也有叫app_key
resource_ids 资源服务器的id,多个用,(逗号)隔开 客户端能访问的资源id集合,注册客户端时,根据实际需要可选择资源id,也可以根据不同的额注册流程,赋予对应的额资源id
client_secret 客户端的秘钥 注册填写或者服务端自动生成,实际应用也有叫app_secret, 必须要有前缀代表加密方式
scope 用户授予的范围 用来限制客户端访问的权限,在换取的 token 的时候会带上 scope 参数,只有在 scopes 定义内的,才可以正常换取 token。。支持多个用逗号分隔
authorized_grant_types 认证的方式 可选值 授权码模式:authorization_code,密码模式:password,刷新token: refresh_token, 隐式模式: implicit: 客户端模式: client_credentials。支持多个用逗号分隔
web_server_redirect_uri 授权码模式认证成功跳转的地址 客户端重定向uri,authorization_code和implicit需要该值进行校验,注册时填写,
authorities 指定用户的权限范围 如果授权的过程需要用户登陆,该字段不生效,implicit和client_credentials需要
access_token_validity token的过期时间 设置access_token的有效时间(秒),默认(60 * 60 * 12,12小时)
refresh_token_validity 刷新token的过期时间 设置refresh_token有效期(秒),默认(60 *60 * 24 * 30, 30天)
additional_information 值必须是json格式
autoapprove 设置用户是否自动approval操作 默认false,适用于authorization_code模式,设置用户是否自动approval操作,设置true跳过用户确认授权操作页面,直接跳到redirect_uri

# oauth相关表结构

CREATE TABLE `clientdetails` (
  `appId` varchar(128) NOT NULL,
  `resourceIds` varchar(256) DEFAULT NULL,
  `appSecret` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `grantTypes` varchar(256) DEFAULT NULL,
  `redirectUrl` varchar(256) DEFAULT NULL,
  `authorities` varchar(256) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL,
  `refresh_token_validity` int(11) DEFAULT NULL,
  `additionalInformation` varchar(4096) DEFAULT NULL,
  `autoApproveScopes` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`appId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `oauth_access_token` (
  `token_id` varchar(256) DEFAULT NULL,
  `token` blob,
  `authentication_id` varchar(128) NOT NULL,
  `user_name` varchar(256) DEFAULT NULL,
  `client_id` varchar(256) DEFAULT NULL,
  `authentication` blob,
  `refresh_token` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `oauth_approvals` (
  `userId` varchar(256) DEFAULT NULL,
  `clientId` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `status` varchar(10) DEFAULT NULL,
  `expiresAt` timestamp NULL DEFAULT NULL,
  `lastModifiedAt` timestamp NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `oauth_client_details` (
  `client_id` varchar(128) NOT NULL,
  `resource_ids` varchar(256) DEFAULT NULL,
  `client_secret` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `authorized_grant_types` varchar(256) DEFAULT NULL,
  `web_server_redirect_uri` varchar(256) DEFAULT NULL,
  `authorities` varchar(256) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL,
  `refresh_token_validity` int(11) DEFAULT NULL,
  `additional_information` varchar(4096) DEFAULT NULL,
  `autoapprove` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `oauth_client_token` (
  `token_id` varchar(256) DEFAULT NULL,
  `token` blob,
  `authentication_id` varchar(128) NOT NULL,
  `user_name` varchar(256) DEFAULT NULL,
  `client_id` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `oauth_code` (
  `code` varchar(256) DEFAULT NULL,
  `authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `oauth_refresh_token` (
  `token_id` varchar(256) DEFAULT NULL,
  `token` blob,
  `authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 添加客户端配置信息:client/secret
INSERT INTO `oauth_client_details`(`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) VALUES ('client', NULL, '$2a$10$VIRnI.GOR5WQDnMBxfRBkeyR/Oo9J0aUU5SrU2E5JKg/XWYjN11x2', 'admin', 'password,refresh_token', NULL, NULL, 86400, 2592000, NULL, NULL);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

# RBAC

RBAC,基于角色的访问控制,权限控制模型。

用户——角色——权限

  • who:资源所有者(用户)
  • what:能访问哪些资源,静态资源(展示列、功能操作按钮)、动态资源(数据)
  • how:具体怎么访问(CRUD)

# 二、Spring Security

Spring Security (opens new window)

Spring Security是一个安全框架,能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案。

Spring Security基于Servlet过滤器、IoC和AOP,为Web请求方法调用提供身份确认和授权处理。

# 三、Spring Security OAuth2

spring-security-oauth官网 (opens new window)

OAuth2的服务提供方包含两个服务:

  • 认证服务(Authorization Server)
  • 授权服务(Resource Server)

# 代码使用

# 源码解读

# 认证流程图

# 密码模式登录认证流程

  • 登录流程

  • 请求资源流程

# 四、spring-cloud-starter-oauth2

spring-cloud-starter-oauth2 ,其实是 Spring Cloud 按照 OAuth2 的标准并结合 spring-security 封装好的一个具体实现。