OAuth 2.0授权框架支持第三方支持访问有限的HTTP服务,通过在资源所有者和HTTP服务之间进行一个批准交互来代表资源者去访问这些资源,或者通过允许第三方应用程序以自己的名义获取访问权限。
为了方便理解,可以想象OAuth2.0就是在用户资源和第三方应用之间的一个中间层,它把资源和第三方应用隔开,使得第三方应用无法直接访问资源,从而起到保护资源的作用。为了访问这种受保护的资源,第三方应用(客户端)在访问的时候需要提供凭证。即,需要告诉OAuth2.0你是谁你要做什么。你可以将用户名和密码告诉第三方应用,让第三方应用直接以你的名义去访问,也可以授权第三方应用去访问。可以联想一下微信公众平台开发,在微信公众平台开发过程中当我们访问某个页面,页面可能弹出一个提示框应用需要获取我们的个人信息问是否允许,点确认其实就是授权第三方应用获取我们在微信公众平台的个人信息。这里微信网页授权就是使用的OAuth2.0。
介绍
在传统的client-server认证模型中,客户端通过提供资源所有者的凭证来请求服务器访问一个受限制的资源(受保护的资源)。为了让第三方应用可以访问这些受限制的资源,资源所有者共享他的凭证给第三方应用。
Roles
QAuth定义了四种角色:
-
resource owner(资源所有者)
-
resource server(资源服务器)
-
client(客户端):代表资源所有者并且经过所有者授权去访问受保护的资源的应用程序
-
authorization server(授权服务器):在成功验证资源所有者并获得授权后向客户端发出访问令牌
Protocol Flow
抽象的OAuth2.0流程如图所示:
-
(A) 客户端向资源所有者请求其授权
-
(B) 客户端收到资源所有者的授权许可,这个授权许可是一个代表资源所有者授权的凭据
-
(C) 客户端向授权服务器请求访问令牌,并出示授权许可
-
(D) 授权服务器对客户端身份进行认证,并校验授权许可,如果都是有效的,则发放访问令牌
-
(E) 客户端向资源服务器请求受保护的资源,并出示访问令牌
-
(F) 资源服务器校验访问令牌,如果令牌有效,则提供服务
Authorization Client
一个授权许可是一个凭据,它代表资源所有者对访问受保护资源的一个授权,是客户端用来获取访问令牌的。
授权类型
授权类型有四种:
-
authorization code:用户登录授权,先拿code,用code换token
-
implicit:用户登录授权,不拿code了,直接拿token
-
resource owner password credentials:验证客户端,直接用用户名和密码,拿token
-
client credentials:验证客户端,直接拿token
Authorization Code(授权码)
授权码是授权服务器用来获取并作为客户端和资源所有者之间的中介。代替直接向资源所有者请求授权,客户端定向资源所有者到一个授权服务器,授权服务器反过来指导资源所有者将授权码返回给客户端。在将授权码返回给客户端之前,授权服务器对资源所有者进行身份验证并获得授权。因为资源所有者只对授权服务器进行身份验证,所以资源所有者的凭据永远不会与客户机共享。
Implicit
隐式授权是为了兼顾到在浏览器中用诸如JavaScript的脚本语言实现的客户端而优化的简化授权代码流程。在隐式授权流程中,不是发给客户端一个授权码,而是直接发给客户端一个访问令牌,而且不会对客户端进行认证。隐式授权提高了一些客户端(比如基于浏览器实现的客户端)的响应能力和效率,因为它减少了获得访问令牌所需的往返次数。
Resource Owner Password Credentials(客户端的验证授权)
资源所有者的密码凭据(比如,用户名和密码)可以直接作为授权许可来获取访问令牌。这个凭据只应该用在高度信任的资源所有者和客户端之间(比如,客户端是系统的一部分,或者特许的应用),并且其它授权模式不可用的时候。
Client Credentials(客户端凭据)
客户端凭据通常用作授权许可
Access Token
访问令牌是用来访问受保护的资源的凭据。一个访问令牌是一个字符串,它代表发给客户端的授权。令牌代表资源所有者授予的对特定范围和访问的时间(PS:令牌是有范围和有效期的),并由资源服务器和授权服务器强制执行。访问令牌可以有不同的格式、结构和使用方法。
Refresh Token
Refresh Token是用于获取Access Token的凭据。刷新令牌是授权服务器发给客户端的,用于在当前访问令牌已经失效或者过期的时候获取新的访问令牌。刷新令牌只用于授权服务器,并且从来不会发给资源所有者。
刷新的流程如图所示:
-
(A) 客户端请求获取访问令牌,并向授权服务器提供授权许可
-
(B) 授权服务器对客户端身份进行认证,并校验授权许可,如果校验通过,则发放访问令牌和刷新令牌
-
(C) 客户端访问受保护的资源,并向资源服务器提供访问令牌
-
(D) 资源服务器校验访问令牌,如果校验通过,则提供服务
-
(E) 重复(C)和(D)直到访问令牌过期。如果客户端直到访问令牌已经过期,则跳至(G),否则不能继续访问受保护的资源
-
(F) 自从访问令牌失效以后,资源服务器返回一个无效的令牌错误
-
(G) 客户端请求获取一个新的访问令牌,并提供刷新令牌
-
(H) 授权服务器对客户端进行身份认证并校验刷新令牌,如果校验通过,则发放新的访问令牌(并且,可选的发放新的刷新令牌)
Client Registration(客户端注册)
在使用该协议之前,客户端向授权服务器注册。
Client Types
QAuth定义了两种客户端类型:
-
confidential: 能够维护其凭证的机密性的客户端
-
public:不能维护其凭证的机密性的客户端
Client Password
拥有客户端密码的客户端可以使用HTTP Basic向服务器进行认证,当然前提是授权服务器支持HTTP Basic认证。
例如:Authorization: Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3
二者选其一的,授权服务器可能支持在请求体中用下列参数包含客户端凭据:
-
client_id:必须的,在授权服务器中注册过的客户端标识符。
-
client_secret:必须的,客户端秘钥。如果秘钥是空字符串的话可以省略该参数。
用这两个参数将客户端凭据包含在请求体中这种方式不推荐,并且应该限制客户端不能直接用HTTP Basic认证方案。
Protocal Endpoints(协议端点)
授权处理用两个授权服务器端点:
-
Authorization endpoint:用于客户端从资源所有者那里获取授权
-
Token endpoint:用于客户端用授权许可交互访问令牌
还有一个端点
-
Redirection endpoint:用于资源服务器通过资源所有者用户代理将包含授权凭据的响应返回给客户端
Authorization Endpoint
授权端点用于和资源所有者交互并获取一个授权许可的。授权服务器必须首先校验资源所有者的身份。
Response Type
客户端用以下参数通知授权服务器自己渴望的授权类型:
-
response_type:必须的,为了请求一个授权码这个值必须是"code",为了请求一个访问令牌这个值必须是"token"
Redirection Endpoint
在完成和资源所有者的交互以后,授权服务器直接将资源所有者的user-agent返回给客户端。授权服务器重定向到这个user-agent
Access Token Scope
授权和令牌端点允许客户端使用“scope”请求参数指定访问请求的范围。反过来,授权服务器使用“scope”响应参数通知客户机它所发放的访问令牌的范围。
Obtaining Authorization
为了获得一个访问令牌,客户端需要先从资源所有者那里获得授权。授权是以授权许可的形式来表示的。
OAuth定义了四种授权类型:
-
authorization code
-
Implicit
-
resource owner password credentials
-
client credentials
Authorization Code Grant
授权码流程如图所示:
-
(A) 客户端通过将资源所有者的用户代理指向授权端点来启动这个流程。客户端包含它的客户端标识符,请求范围,本地状态,和重定向URI,在访问被允许(或者拒绝)后授权服务器立即将用户代理返回给重定向URI。
-
(B) 授权服务器验证资源所有者(通过用户代理),并确定资源所有者是否授予或拒绝客户端的访问请求。
-
(C) 假设资源所有者授权访问,那么授权服务器用之前提供的重定向URI(在请求中或在客户端时提供的)将用户代理重定向回客户端。重定向URI包括授权码和前面客户端提供的任意本地状态。
-
(D) 客户端用上一步接收到的授权码从授权服务器的令牌端点那里请求获取一个访问令牌。
-
(E) 授权服务器对客户端进行认证,校验授权码,并确保这个重定向URI和第三步(C)中那个URI匹配。如果校验通过,则发放访问令牌,以及可选的刷新令牌。
Implicit Grant
隐式授权用于获取访问令牌(它不支持刷新令牌),它针对已知的操作特定重定向URI的公共客户端进行了优化。这些客户端通常在浏览器中使用脚本语言(如JavaScript)实现。因为它是基于重定向的流程,所以客户端必须有能力和资源所有者的用户代理(典型地,是一个Web浏览器)进行交互,同时必须有能力接收来自授权服务器的重定向请求。隐士授权类型不包含客户端身份验证,它依赖于资源所有者的存在和重定向URI的注册。由于访问令牌被编码到重定向URI中,所以它可能暴露给资源所有者以及同一台设备上的其它应用。
隐式授权流程如图所示:
-
(A) 客户端引导资源所有者的user-agent到授权端点。客户端携带它的客户端标识,请求scope,本地state和一个重定向URI。
-
(B) 授权服务器对资源所有者(通过user-agent)进行身份认证,并建立连接是否资源所有者允许或拒绝客户端的访问请求。
-
(C) 假设资源所有者允许访问,那么授权服务器通过重定向URI将user-agent返回客户端。
-
(D) user-agent遵从重定向指令
-
(E) web-hosted客户端资源返回一个web页面(典型的,内嵌脚本的HTML文档),并从片段中提取访问令牌。
-
(F) user-agent执行web-hosted客户端提供的脚本,提取访问令牌
-
(G) user-agent将访问令牌传给客户端
Resource Owner Password Credentials Grant
资源所有者密码凭证授予类型适用于资源所有者与客户端(如设备操作系统或高度特权应用程序)存在信任关系的情况。授权服务器在启用这种授予类型时应该特别小心,并且只在其他授权流程不可行的时候才允许使用。这种授权类型适合于有能力维护资源所有者凭证(用户名和密码,典型地,用一个交互式的表单)的客户端。
资源所有者密码凭证流程如图:
-
(A) 资源所有者提供他的用户名和密码给客户端
-
(B) 客户端携带从资源所有者那里收到的凭证去授权服务器的令牌端点那里请求获取访问令牌
-
(C) 授权服务器对客户端进行身份认证,并校验资源所有者的凭证,如果都校验通过,则发放访问令牌
Client Credentials Grant
客户端用它自己的客户单凭证去请求获取访问令牌
客户端凭证授权流程如图所示:
-
(A) 客户端用授权服务器的认证,并请求获取访问令牌
-
(B) 授权服务器验证客户端身份,如果严重通过,则发放令牌
Spring Security OAuth 2.0
OAuth 2.0 Provider 实现
在OAuth 2.0中,provider角色事实上是把授权服务和资源服务分开,有时候它们也可能在同一个应用中,用Spring Security OAuth你可以选择把它们分成两个应用,当然多个资源服务可以共享同一个授权服务。
获取token的请求由Spring MVC的控制端点处理,访问受保护的资源由标准的Spring Security请求过滤器处理。
为了实现OAuth 2.0授权服务器,在Spring Security的过滤器链中需要有以下端点:
-
AuthorizationEndpoint 用于服务授权请求。默认URL是/oauth/authorize
-
TokenEndpoint 用于服务访问令牌请求。默认URL是/oauth/token
在OAuth 2.0的资源服务器中需要实现下列过滤器:
-
OAuth2AuthenticationProcessingFilter 用于加载认证
对于所有的OAuth 2.0 provider特性,最简单的配置是用Spring OAuth @Configuration适配器。
Authorization Server 配置
只要你配置了授权服务器,那么你应该考虑客户端用于获取access token的授权类型(例如,授权码,用户凭证,刷新token)。服务器的配置是用来提供client detail服务和token服务的,并且可以启用或者禁用全局的某些机制。
每个客户端可以配置不同的权限
@EnableAuthorizationServer注解被用来配置授权服务器,也可以和实现了AuthorizationServerConfigurer接口的任意被标记为@Bean的Bean一起来对授权服务器进行配置。
下列特性被委托给AuthorizationServerConfigurer:
-
ClientDetailsServiceConfigurer :a configurer that defines the client details service
-
AuthorizationServerSecurityConfigurer :defines the security constraints on the token endpoint
-
AuthorizationServerEndpointsConfigurer :defines the authorization and token endpoints and the token services
一件重要的事情是,provider配置了将授权码给OAuth客户端的方式(PS:在授权码类型授权过程中)
OAuth客户端通过将end-user(最终用户)导向授权页,用户可用在此输入他的凭证。之后,授权服务器携带授权码通过重定向的方式将授权码返回给客户端。
配置 Client Details
ClientDetailsServiceConfigurer可用使用client details service的两种实现中的任意一种:in-memory 或者 JDBC
客户端重要的属性是:
-
clientId :(必须的)客户端ID
-
secret :(对于信任的客户端需要)客户端秘钥
-
scope :客户端被限定的范围。如果scope为定义或者为空(默认为空)则客户端不受scope限制
-
authorizedGrantTypes :客户端使用到的授权类型
-
authorities :授予客户端的权限
客户端details可以在应用运行时被更新,通过直接访问存储(例如:如果用JdbcClientDetailsService的话可以实时改变数据库表中的数据)或者通过实现ClientDetailsManager接口(它们也都实现了ClientDetailsService接口)。
注意:用于JDBC服务的数据库schema并没有打包到library中(因为你再实际使用的时候可能有诸多差异)
管理Tokens
AuthorizationServerTokenServices定义了管理OAuth 2.0 Token所必须的操作。请注意:
-
当创建一个access token的时候,这个认证必须被存储起来,以便后续访问资源的时候对接收到的access token进行引用校验。
-
access token用来加载认证
当你实现了AuthorizationServerTokenServices接口,你可能考虑用DefaultTokenServices。有许多内置的插件化的策略可以用来改变access token的格式和存储。
默认情况下,用随机值来生成token,并且用TokenService来处理所有(除了token持久化以外)事情。默认的存储是in-memory实现,但是有其它的实现可以使用。
-
对于单服务器而言,默认的InMemoryTokenStore是完美的。大多数的项目是从这里开始的,为了使它很容易启动,也不需要其它依赖,并且可能以开发模式进行操作。
-
JdbcTokenStore是JDBC版本的Token存储。它把Token数据存储到关系型数据库中。为了使用JdbcTokenStore需要classpath下有"spring-jdbc"。
-
JSON Web Token (JWT) 它将授权的token的所有数据进行编码后存储(没有使用后端存储是它最大的优势)。这种方式的一个缺点是你不能很容易的撤销一个access token,因此一般用该方式存储的token的有效期很短,并且在刷新token的时候之前的token会被废除。另一个缺点是,token很长,因为它里面存了很多关于用户凭证的信息。JwtTokenStore不会真的存储数据,它不持久化任何数据。但是在DefaultTokenServices中,它扮演着token值和认证信息转换的角色。
注意:对于JDBC的schema没有打包到library中,确保用@EnableTransactionManagement来防止多个客户端在同一行创建token。注意,示例中的schema都有明确地主键声明,在并发环境中这是必须的。
JWT Tokens
为了使用JWT Tokens,你需要在你的授权服务器中有一个JwtTokenStore。资源服务器也需要解码这个token,所以JwtTokenStore有一个依赖JwtAccessTokenConverter,相同的实现需要被包含在授权服务器和资源服务器中。也就是说,授权服务器和资源服务器中都需要JwtTokenStore实现。默认情况下,token是被签名的,而且资源服务器必须能够校验这个签名,因此需要有相同的对称key,或者需要公钥来匹配授权服务器上的私钥。公钥被授权服务器暴露在/oauth/token_key端点,默认情况下这个端点的访问规则是"denyAll()"。你可以用标准的SpEL表达式(例如:permitAll())到AuthorizationServerSecurityConfigurer来开放它。
为了使用JwtTokenStore,在classpath下需要有"spring-security-jwt"
Grant Types
授权类型通过AuthorizationEndpoint来支持。默认情况下,除了password以外,所有授权类型都支持。下面是授权类型的一些属性:
-
authenticationManager :通过注入一个AuthenticationManager来切换成password授权
-
userDetailsService :如果你注入一个UserDetailsService或者以任意方式配置了一个全局的UserDetailsService(例如:在GlobalAuthenticationManagerConfigurer中),那么一个刷新token将被包含在user detail中,为了强制账户是激活的。
-
authorizationCodeServices :定义授权码服务(AuthorizationCodeServices的实例)
-
implicitGrantService :在隐式授权期间管理状态
-
tokenGranter :tokenGranter
Configuring the Endpoint URLs
AuthorizationServerEndpointsConfigurer有一个pathMapping()方法。它有两个参数:
-
端点的默认URL路径
-
自定义的路径(必须以"/"开头)
下面是框架提供的URL路径:
-
/oauth/authorize 授权端点
-
/oauth/token 令牌端点
-
/oauth/confirm_access 用户批准授权的端点
-
/oauth/error 用于渲染授权服务器的错误
-
/oauth/check_token 资源服务器解码access token
-
/oauth/check_token 当使用JWT的时候,暴露公钥的端点
授权端点/oauth/authorize应该被保护起来,以至于它只能被认证过的用户访问。
注意:如果您的授权服务器同时也是一个资源服务器的话,那么就有另一个具有较低优先级的安全过滤器链来控制API资源。通过访问令牌来保护这些请求,你需要它们的路径不能与主用户过滤器链中的那些相匹配,所以请确保包含一个请求matcher,它只挑选出上面的WebSecurityConfigurer中的非api资源。
Customizing the UI
授权服务器的大多数端点主要都是被机器使用的,但是有两个资源是需要UI,它们分别是/oauth/confirm_access和HTML响应/oauth/error。框架为它们提供的实现是空白页,真实的情况是大多数授权服务器可能想要提供它们自己的实现来控制样式和内容。所以,你需要做的事情就是提供一个Spring MVC 被标注了@RequestMappings注解的Controller来映射这些端点,并且框架将用一个低的优先级来发放请求。在默认的/oauth/confirm_access你期望一个AuthorizationRequest绑定到session。你可以抓取请求的所有数据并按照自己喜欢的方式渲染它们,然后用户需要做的就是向/oauth/authorize发送关于批准或拒绝授予的信息。默认的UserApprovalHandler取决于是否你再AuthorizationServerEndpointsConfigurer中提供了一个ApprovalStore。标准的审批处理器如下:
-
TokenStoreUserApprovalHandler :通过user_oauth_approval做一个简单的yes/no决定等同于“true”或“false”
-
ApprovalStoreUserApprovalHandler :一组"scope*"参数key。参数的值可以是"true"或者"approval"。至少有一个scope是approval才算是授权成功。(A grant is successful if at least one scope is approved.)
强制SSL
纯HTTP对于测试来说是可以的,但是在生成中授权服务器应该使用SSL。你可以在一个安全的容器或代理后面运行应用程序,如果你正确地设置代理和容器(这与OAuth2无关),那么它应该可以正常工作。对于/authorize端点你需要把它当作正常的应用安全的一部分来做,对于/token端点在AuthorizationServerEndpointsConfigurer中有一个标记可以设置,通过用sslOnly()方法。
自定义错误处理
授权服务器用标准的Spring MVC特性来进行错误处理。
你可以提供自己的实现,通过添加@Controller并且带上@RequestMapping("/oauth/error")
Mapping User Roles to Scopes
有时候,为了限制token的scope,不仅仅要根据指定的客户端的范围,也要根据用户自己的权限来进行限制。如果你在你的AuthorizationEndpoint用DefaultOAuth2RequestFactory,你可以设置checkUserScopes=true来限制匹配的用户角色的允许范围。AuthorizationServerEndpointsConfigurer允许你注入一个自定义的OAuth2RequestFactory
资源服务器配置
一个资源服务器(可能与授权服务器是相同的应用,也可能与授权服务器是分开的应用)通过OAuth2 Token服务受保护的资源。Spring OAuth 提供一个Spring Security认证过滤器来实现这个保护。你可以在一个@Configuration类上用@EnableResourceServer来切换它,并且用ResourceServerConfigurer配置它。下列特性可以被配置:
-
tokenServices :一个ResourceServerTokenServices的实例
-
resourceId :资源ID(推荐的,如果存在的话会被授权服务器校验)
-
资源服务器的其它扩展端点
-
request matchers for protected resources (defaults to all)
-
access rules for protected resources (defaults to plain "authenticated")
-
其它通过HttpSecurity配置的自定义的受保护的资源
@EnableResourceServer注释将自动添加一个OAuth2AuthenticationProcessingFilter类型的过滤器到Spring安全过滤器链中。
OAuth 2.0 客户端
受保护的资源配置
受保护的资源(或者叫远程资源)可以用OAuth2ProtectedResourceDetails类型的bean来定义。一个被保护的资源由下列属性:
-
id :资源的id。这个id只是用于客户端查找资源。
-
clientId :OAuth Client id。
-
clientSecret :关联的资源的secret。默认非空
-
accessTokenUri :提供access_token的端点的uri
-
scope :逗号分隔的字符串,代表访问资源的范围。默认为空
-
clientAuthenticationScheme :客户端认证所采用的schema。建议的值:"http_basic"和"form"。默认是"http_basic"。
不同的授权类型有不同的OAuth2ProtectedResourceDetails的具体实现(例如:ClientCredentialsResourceDetails是"client_credentials"类型的具体实现)
-
userAuthorizationUri :用户授权uri,非必需的。
客户端配置
对于OAuth 2.0客户端配置,简化的配置用@EnableOAuth2Client。这个注解做两件事情:
-
创建一个过滤器(ID是oauth2ClientContextFilter)来存储当前的请求和上下文。在请求期间需要进行身份认证时,它管理重定向URI。
-
在请求范围内创建一个AccessTokenRequest类型的bean。对于授权代码(或隐式)授予客户端是很有用的,可以避免与单个用户相关的状态发生冲突。
访问受保护的资源
建议用RestTemplate访问受保护的资源。Spring Security为OAuth提供了一个扩展的RestTemplate只需要你提供一个OAuth2ProtectedResourceDetails的实例即可。为了使它和用户token(授权码方式授权)一起使用,你应该考虑用@EnableOAuth2Client配置。
一般来说,web应用程序不应该使用密码授予,因此如果您可以支持AuthorizationCodeResourceDetails,请避免使用ResourceOwnerPasswordResourceDetails。为了和用户令牌(授权码)一起使用,你应该考虑用@EnableOAuth2Client配置。
客户端持久化Token
客户端不需要持久化令牌,但是最好不要在每次重启客户端应用程序时都要求用户批准新的令牌授予。
ClientTokenServices接口定义了为特定用户保存OAuth 2.0令牌所需的操作。这是一个JDBC实现,但是如果您希望实现自己的服务,以便在持久数据库中存储访问令牌和相关的身份验证实例,则可以这样做。如果你想要使用这个特性,你需要为OAuth2RestTemplate提供一个经过特殊配置的TokenProvider。