Spring Security OAuth 2 开发指南
本文档翻译自http://projects.spring.io/spring-security-oauth/docs/oauth2.html
简介
这是为支持 OAuth 2.0
的用户指南,OAuth1.0很多都不一样,请访问 OAuth1.0指南 。
该指南分成了两部分,第一部分是OAuth2.0 Provider,第二部分是OAuth2.0 Client。integration tests 和 sample apps 是Provider和Client很好的示例源代码。
OAuth 2.0 Provider
OAuth 2.0 Provider机制主要负责暴露OAuth2.0保护的资源。配置包括建立OAuth2.0 Client用来独立的或者代表用户访问受保护的资源。Provider通过管理和验证OAuth2.0 Token来访问受保护的资源。编程时,Provider必须提供一个接口让用户确认Client可以被授权访问他个人的资源(一个授权确认页面)。
OAuth 2.0 Provider 实现
Provider在OAuth2.0中的角色实际上分为授权服务和资源服务,然而很多时候这两个服务在同一个应用中,使用Spring OAuth你也可以将他们分为两个应用,也可以创建多个资源服务并使用同一个授权服务。对Token的请求被Spring MVC控制器端点处理,访问受保护的资源通过标准的Spring Security请求过滤器处理。为了实现OAuth2.0授权服务器,下面的端点在Spring Security过滤器链中是必需的:
AuthorizationEndpoint
用来处理授权请求,默认URL:/oauth/authorize
TokenEndpoint
用来处理access token请求,默认URL:/oauth/token
要实现OAuth2.0资源服务器,下面的过滤器是必需的:
OAuth2AuthenticationProcessingFilter
用来给请求中的认证access token加载认证信息
对于所有的OAuth2.0 Provider特性,可以使用Spring OAuth @Configuration
adapters简便的配置。同时也有OAuth的XML命名空间,schema位于http://www.springframework.org/schema/security/spring-security-oauth2.xsd
授权服务器配置
在配置授权服务器的时候,你必须要考虑Client用来从用户那儿获取access token的授权类型(还有authrization code,user credentials,refresh token)。服务器的配置提供了client details service和token service的实现,全局性的启用或禁用机制的某些方面。要注意,每个Client可以专门配置权限能够使用某些授权机制和访问的授权类型。相当于,你的Provider只配置了支持"client credentials"的授权类型,并不意味着一个特定的Client可以授权使用该授权类型。
@EnableAuthorizationServer
注解用来配置OAuth2.0 授权服务器机制,和任意个 @Beans
实现 AuthorizationServerConfigurer
(有一些方便的adapter实现类,里面都是空方法)。下面的特性都是委托给各个由Spring创建并注入进 AuthorizationServerConfigurer
的配置器:
ClientDetailsServiceConfigurer
一个定义client details service的配置器,client details 可以被初始化或引用一个已经存在的storeAuthorizationServerSecurityConfigurer
定义了token端点的安全限制AuthorizationServerEndpointsConfigurer
定义了authorization和token端点,token service
Provider配置的一个重要方面是,authorization code提供给OAuth Client的方式(在authorization code grant中)。authorization code是由OAuth Client重写向用户到授权页面来获得的,在这个页面中,用户可以输入他的用户名密码,并带着authorization code从Provider授权服务器重定向回OAuth Client。这样的示例阐述的OAuth2.0 规范。
在XML中的<authorization-server/>
元素以类似的方式来配置OAuth2.0 授权服务器
配置Client Details
ClientDetailsServiceConfigurer
(这是你的AuthorizationServerConfigurer
的回调)用来定义基于内存或者JDBC的client details service实现,一个client重要的属性有:
clientId
(必需)客户端idsecret
(使用信任的客户端时是必需的)客户端密钥,如果有的话scope
客户端被限制的范围,如果scope未定义或为空(默认),客户端不被scope限制authorizedGrantTypes
授权类型是被授权给客户端来使用的,默认是空authroities
授予给客户端的权限(常规的Spring Security权限)
client details 可以在运行的应用中被更新,通过直接访问底层的store(比如直接访问JdbcClientDetailsService
中的数据表)或者通过ClientDetailsManager
接口(它也同时实现了ClientDetailsService
)
注意:JDBC service的schema没有包含在当前库中(因为在练习中有太多你可能想使用的东西了),但这里有一个示例可以帮你开始 test code in github
管理Token
AuthorizationServerTokenServices
接口定义了管理OAuth2.0 token必需的操作,如下:
- 当access token被创建时,认证信息必须被存储了,这样后续的资源服务器接收到的access token才能引用到。
- access token被用来加载认证信息(用来授权access token的创建)
当创建自定义的 AuthorizationServerTokenServices
实现时,你可以考虑使用 DefaultTokenServices
它包含很多可嵌入的策略来改变access token的格式和存储。默认的,通过随机数创建token并把处理token持久化的一切委托给了 TokenStore
。默认的store是 in-memory implementation ,但还有其他一些实现。这里是一些对他们的讨论:
- 默认的
InMemoryTokenStore
对于独立的server(低访问量且失败时不需要热切换到备份服务器)表现的很完美了,大多数工程以这种形式开始,也可以在开发模式下使用,以简单无依赖的方式启动server。 JdbcTokenStore
是相关的JDBC 版本,它把token数据存储到关系型数据库中。如果你可以在多个server间共享数据库,或者你想扩展单一server的实例,或者认证和资源服务器有多个组件,就使用JDBC版本。要使用JdbcTokenStore
你必需在classpath中加入"spring-jdbc"- sotre中的 JSON Web Token (JWT) version 会编码所有关于授权的数据到token中(那么完全不需要后台数据存储,这是一个显著的优势)。一个缺点是你不能轻易的废除token,所以他们经常被授权较短的过期时间,废除是通过refresh token处理的。另一个缺点是如果你存储了大量用户凭证信息,token会变得很大。
JwtTokenStore
并不是真正的存储,它不持久化任何数据,但他在DefaultTokenServices
中的token值和认证信息间的转换中扮演着同样的角色
注意:JDBC service的schema没有包含在当前库中(因为在练习中有太多你可能想使用的东西了),但这里有一个示例可以帮你开始 test code in github。请确保使用了
@EnableTransactionManagement
避免在创建token时client apps间竞争相同行产生冲突。同时注意示例schema中包含明确的PRIMARY KEY
声明,在并发环境中这也是必需的。
JWT Tokens
要使用JWT token你需要在授权服务器中配置JwtTokenStore
,资源服务器同样需要解码token,那么 JwtTokenStore
依赖一个 JwtAccessTokenConverter
,所以授权服务器和资源服务器需要同样的实现。token默认是被签名处理的,所以资源服务器也必须能够验证签名,所以它或者需要与授权服务器有相同的密钥(共享密钥或对称密钥),或者它需要匹配授权服务器私钥(签名密钥)的公钥(验证密钥)。授权服务器通过 /oauth/token_key
端点来暴露公钥,它默认是安全的,访问规则是"denyAll()"。你可以打开它,通过注入标准的SpEL表达式到 AuthorizationServerSecurityConfigurer
(比如,"permitAll()"或许是合理的,因为它是一个公钥)
要使用 JwtTokenStore
你需要在classpath中引入"spring-security-jwt"(你同样可以在Spring OAuth github仓库上找到它,但它有一个不同的发布周期)
授权类型
AuthorizationEndpoint
支持的授权类型可以通过 AuthorizationServerEndpointsConfigurer
来配置。除了password类型(要开启它,请查看下面的详情),其他所有类型都默认是支持的。下面的属性影响grant type:
authenticationManager
通过注入一个AuthenticationManager
来开启password授权userDetailsService
如果你注入了一个UserDetailsService
或者配置了一个全局的(比如在GlobalAuthenticationManagerConfigurer
中),那么refresh token授权会包含一个在user detail上的检查,来确保账户仍然是激活的authorizationCodeServices
为auth code授权定义了authorization code service(AuthorizationCodeServices
实例)implicitGrantService
在隐式授权时管理statetokenGranter
TokenGranter
对象(控制授权的所有方面,并忽略上面属性)
在XML中授权类型在authorization-server
的子元素中
配置端点URL
AuthorizationServerEndpointsConfigurer
有一个pathMapping()
方法,它接受两个参数:
- 端点的默认URL路径(框架实现)
- 想要的自定义路径(以"/"开头)
框架提供的URL路径有:
/oauth/authorize
authorization 端点/oauth/token
token 端点/oauth/confirm_access
用户在这里提交授权批准/oauth/error
用来在授权服务器中展示错误/oauth/check_token
资源服务器用来解码access token/oauth/token_key
在使用JWT token时,暴露公钥以验证token
注意:授权端点 /oauth/authorize
(或者它映射的其他路径)必须使用Spring Security进行保护,使他只能被认证的用户访问。比如使用标准的Spring Security WebSecurityConfigurer
:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().antMatchers("/login").permitAll().and()
// default protection for all resources (including /oauth/authorize)
.authorizeRequests()
.anyRequest().hasRole("USER")
// ... more configuration, e.g. for form login
}
注意:如果你的授权服务器同时也是资源服务器,还有另一个低优先级的安全过滤器链控制API资源。那些被access token保护的请求,就要让他们的路径_不能在面向用户的主过滤器链中匹配上_,那么就要确保在上面的
WebSecurityConfigurer
中包含一个request matcher来挑选出非API资源的路径.
在Spring OAuth @Configuration
配置下,token端点默认是受保护的,支持client secret的HTTP Basic认证。但在XML中并非这样(必须明确它是受保护的)。
在XML中 <authorization-server/>
元素以类似的方式用一些属性来改变默认的端点URL。 /check_token
端点必须明确启用(使用 check-token-enabled
属性)
自定义UI
授权服务器的大多数端点被机器主要使用,但还有一些资源需要UI,这些资源有 /oauth/confirm_access
和 /oauth/error
。在框架中他们使用白板页面的实现,大多现实世界中的授权服务器实现要提供他们自己的页面,来控制样式和内容。你只需要用Spring MVC 控制器的@RequestMappings
提供这些端点,那框架的默认实现在dispatcher中就会处于低优先级。在/oauth/confirm_access
端点中,会给你在session中提供一个 AuthorizationRequest
对象,它里面有查找用户批准授权的所有数据(默认实现是 WhitelabelApprovalEndpoint
,可以参考这个类)。你可以从 AuthorizationRequest
获取所有数据并展示成你喜欢的样子,然后用户要做的就是提交关于批准或者拒绝授权的信息到 /oauth/authorize
。请求参数直接传给 AuthorizationEndpoint
中的 UserApprovalHandler
,你可以任意的拦截数据。默认的 UserApprovalHandler
依赖于你是否在AuthorizationServerEndpointsConfigurer
中提供了 ApprovalStore
(提供了则使用 ApprovalStoreUserApprovalHandler
,否则使用TokenStoreUserApprovalHandler
)。标准的批准处理器接受下面这些:
TokenStoreUserApprovalHandler
: a simple yes/no decision viauser_oauth_approval
equals to "true" or "false".TokenStoreUserApprovalHandler
一个简单的yes/no决定,通过user_oauth_approval
等于"true"或"false"实现ApprovalStoreUserApprovalHandler
一个scope.*
的参数键集合,"*" 等于被请求的scope。参数值可以是"true"或者"approved"(如果用户批准了),否则认为用户拒绝那个scope的授权。授权至少要有一个scope被批准。
注意:不要忘了包含CSRF保护你的为用户提供的表单。Spring Security期望一个默认叫作"_csrf"的参数(他在request attribute中提供了值)。参考Spring Security用户指南获取更多相关信息,或者查看指导中的白板页面实现。
强制 SSL
普通的HTTP可以适用于测试,但一个授权服务器应该只通过SSL使用。你可以在安全的容器中运行应用,或者放置在代理后面,并且在你正确设置了代理和容器后,它能良好工作(这跟OAuth2没什么关系)。你可能也想通过Spring Security的 requiresChannel()
限制来保护端点。对于 /authorize
端点,由你把它当作普通应用安全一样去设置。对于 /token
端点,AuthorizationServerEndpointsConfigurer
中有一个标志,你可以使用 sslOnly()
方法设置。这两种情况,安全通道设置都是可选的,但当Spring Security发现一个请求处于非安全通道时,会重定向到它认为安全的通道。
自定义错误处理
在授权服务器中,错误处理使用标准的Spring MVC特性,在端点中标注 @ExceptionHandler
方法。你也可以为端点提供一个 WebResponseExceptionTranslator
,这是一个很好的方式来改变响应内容。对于token端点,异常的展示委托给 HttpMesssageConverters
(可以在MVC配置中增加),对于authorization端点 ,异常委托给OAuth错误页面(/oauth/error
)。白板错误端点用来响应HTML,但你可能要提供自定义的实现(比如,只需要增加@Controller
和 @RequestMapping("/oauth/error")
)
映射用户角色到Scope
有时不通过client指定的scope来限制token的scope是有用处的,但也要根据用户自己的权限。如果你在 AuthorizationEndpoint
中使用 DefaultOAuth2RequestFactory
,你可以设置 checkUserScopes=true
标识来限制允许的scope只能是匹配用户的角色。你也可以注入一个 OAuth2RequestFactory
到 TokenEndpoint
中(password授权类型) ,但同时你还要设置一个 TokenEndpointAuthenticationFilter
(只需要在HTTP BasicAuthenticationFilter
后增加该filter即可)。当然了,你也可以实现你自己的规则,通过映射scope到角色,并设置自己的 OAuth2RequestFactory
实现。 AuthorizationServerEndpointsConfigurer
允许你注入一个 OAuth2RequestFactory
,如果你使用了 @EnableAuthorizationServer
,你可以利用该特性设置工厂。
资源服务器配置
资源服务器(可以与授权服务器是同一应用,也可以分开)提供对OAuth2 token保护的资源。Spring OAuth提供了Spring Security认证过滤器实现这个保护功能。你可以在一个 @Configuration
类上标注 @EnableResourceServer
来开启,然后使用 ResourceServerConfigurer
来配置它(如果有需要)。可以配置下面这些特性:
tokenServices
该bean定义了token service(ResourceServerTokenServices
的实例)resourceId
资源id(可选,建议配置,授权服务器会去验证)- 资源服务器的其他扩展点(比如,
tokenExtractor
从请求中抽取token) - 对保护资源配置request matchers(默认是保护全部资源)
- 对保护资源的访问规则(默认是普通的"authenticated")
- 其他在Spring Security配置中被
HttpSecurity
允许的资源的自定义配置
@EnableResourceServer
注解会自动增加一个 OAuth2AuthenticationProcessingFilter
过滤器到Sprin Security过滤器链中
在XML中有 <resource-server/>
元素,它的id
属性可以当作servlet Filter
的id,被手动增加到标准的Spring Security链中
ResourceServerTokenServices
是与授权服务器合作的另一重要部分。如果资源服务器和授权服务器在同一应用中,同时你使用了 DefaultTokenServices
,那你就不用考虑太多,因为它实现了所有必需的接口,自动保持一致。如果两个服务器是分开的,那就要确保匹配授权服务器的功能,提供一个知道如何正确解码token的 ResourceServerTokenServices
。在资源服务器中,通常使用 DefaultTokenServices
,主要是通过设置 TokenStore
(后台存储或配置编码)提供可选择性。也可以使用 RemoteTokenServices
,这是一个Spring OAuth特性(不属于规范中的部分),通过对授权服务器的HTTP请求(/oauth/check_token
)来解码token。如果资源服务器没有大量访问(每个请求都需要授权服务器的验证)时,或者你缓存了结果时,RemoteTokenServices
非常方便。要使用 /oauth/check_token
端点,你必须在 AuthorizationServerSecurityConfigurer
中修改暴露它的访问规则(默认是"denyAll()"),比如:
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')").checkTokenAccess(
"hasAuthority('ROLE_TRUSTED_CLIENT')");
}
在示例中,我们配置了 /oauth/check_token
和 /oauth/token_key
端点(信任的Client可以得到公钥来验证JWT)。这两个端点使用client credentials授权类型被HTTP Basic认证进行保护。
配置一个 OAuth-Aware 表达式处理器
你可能想要使用Spring Security的基于表达式的访问控制。@EnableResourceServer
创建时,表达式处理器会默认注册。表达式包括_#oauth2.clientHasRole_, #oauth2.clientHasAnyRole, 和 #oath2.denyClient 可以用来提供基于oauth client角色的访问(更多表达式查看 OAuth2SecurityExpressionMethods
)。在XML中,你可以在<http/>
元素的安全配置下,使用 expression-handler
元素注册一个oauth-aware表达式处理器。
OAuth 2.0 Client
OAuth 2.0 client机制负责访问OAuth2.0保护的资源。配置涉及到确立用户可能访问到的相关的受保护资源。Client可能还需要提供为用户存储authorization code 和 access token的机制。
配置受保护资源
受保护的资源(或者称为"远程资源")可以被OAuth2ProtectedResourceDetails
的bean定义,受保护资源有以下属性:
id
资源id,它只被Client用来查找资源,OAuth协议中不会使用它。还被当作bean的id来使用。clientId
OAuth client的id,这是OAuth Provider给你的Client定义的idclientSecret
与资源相关的密钥,密钥默认不为空accessTokenUri
OAuth Provider提供的access token的端点URLscope
逗号分隔的列表,指定访问资源的范围,默认不会指定scopeclientAuthenticationScheme
你的Client访问access token端点时使用的认证scheme。建议的值有:"http_basic" 和 "form",默认是"http_basic"(参考OAuth2规范的2.1章节)
不同的授权类型有不同的 OAuth2ProtectedResourceDetails
的具体实现(比如,ClientCredentialsResource
对应 "client_credentials" 授权类型)。授权类型要求用户授权有进一步的属性:
userAuthorizationUri
用户如果需要授权访问某个资源,会被重定向去的URI。注意该属性并不总是需要的,依赖于OAuth2协议的支持情况。
在XML中,有 <resource/>
元素用来创建 OAuth2ProtectedResourceDetails
类型的bean,它包含上面所有的属性。
Client 配置
使用 @EnableOAuth2Client
来配置OAuth2.0 Client非常简单,只需要做两件事情:
创建一个过滤器bean(定义id为
oauth2ClientContextFilter
)来存储当前请求和上下文。在请求期间需要认证时,它管理着来自和去往OAuth认证URI的重定向。在request范围中创建一个
AccessTokenRequest
类型的bean,在authorization code 或 implicit 授权类型时持有每个用户相关的state以免冲突。
该过滤器需要被装配到应用中(比如,使用 Servlet initializer 或者 web.xml
配置一个名为 "oauth2ClientContextFilter" 的 DelegatingFilterProxy
。
AccessTokenRequest
可以像下面这样在 OAuth2RestTemplate
中使用:
@Autowired
private OAuth2ClientContext oauth2Context;
@Bean
public OAuth2RestTemplate sparklrRestTemplate() {
return new OAuth2RestTemplate(sparklr(), oauth2Context);
}
OAuth2ClientContext为你放到了session 范围中,为不同用户保持各自的state。如果不是这样,你需要自己在应用中管理这些数据结构,映射请求到用户,并给每个用户关联独立的 OAuth2ClientContext
实例。
在XML中,有 <client/>
元素,id
属性用作servlet Filter
的bean id,在使用 @Configuration
注解时,必须被映射到一个 DelegatingFilterProxy
(以id属性名为名称)。
访问受保护的资源
一旦你提供了所有关于资源的配置,就可以访问那些资源了。推荐访问资源的方法是使用Spring 3中介绍的RestTemplate
。Spring Security OAuth 提供了一个RestTemplate 扩展,只需要提供一个OAuth2ProtectedResourceDetails
实例。和用户token(authorization code授权类型)一起使用,可以认为是@EnableOAuth2Client
配置(相当于XML中的<oauth:rest-template/>
),它创建了一些request和session范围的对象,避免不同用户间的冲突。
一般来说,Webb应用不应该使用password授权类型,如果你赞成使用AuthorizationCodeResourceDetails
就避免使用ResourceOwnerPasswordResourceDetails
。如果强烈要求使用password授权,那可以同样的方式配置 OAuth2RestTemplate
,并把凭证信息添加到 AccessTokenRequest
中(这是一个Map
并且是临时性的)而不是添加到 ResourceOwnerPasswordResourceDetails
中(这个对象是在所有access token间共享的)。
在Client持久化Token
Client_不需要_持久化token,但不想每次Client重启后,都要求用户去批准一个新的token时,持久化token是比较友好的。ClientTokenServices
接口定义了要持久化OAuth2.0 token需要的操作。框架提供了JDBC的实现,但如果可以,你最好实现自己的service来存储access token,并在数据库中关联认证实例。如果要使用这个特性,你需要给 OAuth2RestTemplate
提供专门的TokenProvider
:
@Bean
@Scope(value = "session", proxyMode = ScopedProxyMode.INTERFACES)
public OAuth2RestOperations restTemplate() {
OAuth2RestTemplate template = new OAuth2RestTemplate(resource(), new DefaultOAuth2ClientContext(accessTokenRequest));
AccessTokenProviderChain provider = new AccessTokenProviderChain(Arrays.asList(new AuthorizationCodeAccessTokenProvider()));
provider.setClientTokenServices(clientTokenServices());
return template;
}
为外部的OAuth2 Provider定制Client
一些外部的OAuth2 Provider(比如Facebook)没有正确的实现规范,或者实现的规范的比Spring Security OAuth要老的版本。在你的Client应用中要使用那些provider,你可能需要适应客户端基础设施的各个部分。
以使用Facebook为例,在 tonr2
应用中有一个Facebook特性(你需要修改应用的配置,添加你自己的,有效的clientId和secret,这些在Facebook网站都很容易生成)。
Facebook的tokne响应中包含一个非不标准的JSON条目来表示token的过期时间(使用 expires
,而不是expires_in
),如果你要在应用中使用过期时间,必须使用自定义的 OAuth2SerializationService
手动解码。