OAuth2协议和Spring Security OAuth2实现

OAuth2协议在API访问授权中广为使用,Google、Facebook、微博、腾讯的公开API也都使用它。虽然在使用上有些复杂,尤其是服务器端的使用更为繁琐,但它在权限授予、资源访问限制上的优势让其使用广泛。

OAuth2的协议流程如下:

oauth-protocol-flow

以国内的微博为例,

Resource Owner=微博用户;

Client=第三方应用,如Fuubo、Weico;

Authorization Server=微博平台授权服务器;

Resource Server=访问微博、用户信息等资源的服务器

当开发者决定要开发一款基于微博的应用时,需要到 http://open.weibo.com 去填写一些资料,比如应用名称、类型、访问哪些资源,提交相关信息之后,微博后台会创建应用,分配一个client_id和client_secret,这两个字段对你的应用至关重要,以后会派上用场。

下面就开始正式的OAuth2协议流程:

A、开发者构造Authorization Request(请求微博用户授权),如果此时微博用户A没有登录会直接跳转到微博登录页面,如果已经登录会跳转到授权页面,如图所示。

OAuth2_intro

此时会列出开发者在申请应用时选择要访问的权限,除了必须的权限以外微博用户A可以不勾选部分权限,这样开发者就无法读取相对应的资源。

B、 Authorization Grant(微博用户授予访问权限),如果微博用户A点击“授权”,开发者将获得这个微博用户A的微博、用户信息等资源的访问,开发者将获取到一枚临时code。点击“取消”将拒绝资源的访问。

C、Authorization Grant(请求微博平台授权)开发者拿着微博用户A授权之后获取的临时code,访问微博平台授权服务器,获取“永久”(也有时间限制)访问微博用户A的授权。

D、Get Access Token(微博平台授予微博用户A的访问)微博平台授权服务器检查微博用户A的临时code,检查通过后给开发者返回一个“永久”的Token。

E、Take in Access Token(发起携带Token的请求),开发者在访问资源的请求中带上Token。

F、Protected Resource(返回微博用户A的微博、用户信息等资源)资源服务器检查Token,根据请求URL的不同返回微博用户A的相应资源信息。

通过这几步就实现了微博用户、微博平台、第三方开发者之间的访问授权,当然如果是用户B使用开发者开发的这款应用还需要进行如上所述的步骤。

以上简单介绍了OAuth协议中最复杂的一种授权访问方式(Authorization Code Grant),也是第三方应用用的最多的一种,其他三种对于特定环境下建议使用,比如:

Implicit Grant-多用于浏览器内的应用,可直接回调获取到Token,简单方便。

Resource Owner Password Credentials Grant-用于第三方应用处于可控的风险之下,多用于内部系统的互访问。

Client Credentials Grant-顾名思义,多用于客户端的授权访问,客户端必须保证在开发者手中。

OAuth2协议的可以访问地址查看。

=========================================================================

Spring在security project的基础上实现了OAuth2 的Server端,同时支持四种授权方式,参考了网上的不同示例但都没有完美支持所有方式,阅读官方文档参考源码后完整构建了OAuth2的Server端。

1、代码示例基于

spring-core v4.2.3

spring-security v3.2.x

spring-security-oauth v2.0.8

2、自定义Endpoint用于回调、跳转、登录等功能:

/login-用户form登录action[POST]

/msg/code-授权回调页

/msg/error-授权访问错误页

/msg/login_success-用户form登录成功回调页

/msg/login_error-用户form登录失败回调页

3、OAuth ClientDetail、Token数据库表脚本

访问地址下载。

4、web.xml中除了Spring基础配置还需要添加Spring Security相关配置

<!--Spring Security-->
   <filter>
       <filter-name>springSecurityFilterChain</filter-name>
       <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
   </filter>

   <filter-mapping>
       <filter-name>springSecurityFilterChain</filter-name>
       <url-pattern>/*</url-pattern>
   </filter-mapping>

5、Spring token存储、读写验证服务。

<bean id="tokenStore" class="org.springframework.security.oauth2.provider.token.store.JdbcTokenStore">
       <!-- dataSource引用需要自己定义数据源,Hibernate、DHCP均可 -->
       <constructor-arg ref="dataSource"/>
   </bean>

   <bean id="tokenServices" class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
       <property name="tokenStore" ref="tokenStore"/>
       <!-- 是否支持刷新Token -->
       <property name="supportRefreshToken" value="true"/>
   </bean>

6、OAuth2授权时拒绝、授权各自不同的Handler以及触发处理

<!-- 此处均为默认处理 -->
<bean id="oauth2DeniedHandler"
         class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler"/>
<bean id="oauth2ApprovalHandler"
         class="org.springframework.security.oauth2.provider.approval.DefaultUserApprovalHandler"/>
<bean id="oauth2AuthenticationEntryPoint"
class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint"/>

7、用户form登录时成功、失败各自不同的Handler以及触发处理

<bean id="formLoginSuccessHandler"
         class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
       <!-- 登录成功跳转 -->
       <property name="defaultTargetUrl" value="/msg/login_success"/>
</bean>
<bean id="formLoginFailureHandler"
         class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
        <!-- 登录失败跳转 -->
       <property name="defaultFailureUrl" value="/msg/login_error"/>
</bean>
<bean id="loginUrlAuthenticationEntryPoint"
class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
   <!-- 登录form action -->
   <constructor-arg value="/login"/>
</bean>

8、用户form登录filter捕获、安全规则

<bean id="usernamePasswordAuthenticationFilter"
     class="com.n2hsu.pr.api.auth.MyUsernamePasswordAuthenticationFilter">
   <!-- 登录form action -->
   <constructor-arg value="/login"/>
   <!-- 参见后续定义 -->
   <property name="authenticationManager" ref="userAuthenticationManager"/>
   <!-- 登录结果处理Handler -->
   <property name="authenticationSuccessHandler" ref="formLoginSuccessHandler"/>
   <property name="authenticationFailureHandler" ref="formLoginFailureHandler"/>
  <!-- 登录form 字段名称定义 -->
   <property name="usernameParameter" value="username"/>
   <property name="passwordParameter" value="password"/>
</bean>

<security:http
disable-url-rewriting="true"
entry-point-ref="loginUrlAuthenticationEntryPoint"
authentication-manager-ref="userAuthenticationManager">

<security:custom-filter ref="usernamePasswordAuthenticationFilter" position="FORM_LOGIN_FILTER"/>

<security:intercept-url pattern="/login"/>
<security:intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
</security:http>

9、用户认证

<!-- 用户基础表中读取用户ID等信息返回UserDetails实例 -->
<bean id="userDetailsService"
     class="com.n2hsu.xxx.OAuth2UserService"/>
<!-- 用户密码处理机制,默认不处理,生产环境可能有自己的加密方式 -->
<bean id="userPasswordEncoder"
     class="com.n2hsu.xxx.PasswordEncoder"/>
<!-- 用户密码加盐处理机制 -->
<bean id="userSaltSource"
     class="org.springframework.security.authentication.dao.ReflectionSaltSource">
   <property name="userPropertyToUse" value="salt"/>
</bean>
<!-- 用户认证管理器,OAuth2认证前重要的一环,此处处理较为复杂,考虑到了密码加密、加盐处理等 -->
<security:authentication-manager id="userAuthenticationManager">
   <security:authentication-provider user-service-ref="userDetailsService">
       <security:password-encoder ref="userPasswordEncoder">
           <security:salt-source ref="userSaltSource"/>
       </security:password-encoder>
   </security:authentication-provider>
</security:authentication-manager>

10、应用认证filter捕获

<bean id="clientCredentialsTokenEndpointFilter"
     class="org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter">
   <!-- 参见后续定义 -->
   <property name="authenticationManager" ref="clientAuthenticationManager"/>
</bean>

11、应用认证服务管理

<!-- 应用信息存储服务 -->
<bean id="clientDetailsService"
     class="org.springframework.security.oauth2.provider.client.JdbcClientDetailsService">
   <constructor-arg ref="dataSource"/>
</bean>
<!-- 应用信息读取服务 -->
<bean id="clientDetailsUserDetailsService"
     class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService">
   <constructor-arg ref="clientDetailsService"/>
</bean>
<!-- 应用认证管理器,OAuth2认证前重要的一环 -->
<security:authentication-manager id="clientAuthenticationManager">
   <security:authentication-provider user-service-ref="clientDetailsUserDetailsService"/>
</security:authentication-manager>

12、OAuth2认证服务器

<!-- OAuth2授权检查的方式 -->
<bean id="oauth2AccessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased">
   <constructor-arg>
       <list>
           <bean class="org.springframework.security.oauth2.provider.vote.ScopeVoter"/>
           <bean class="org.springframework.security.access.vote.RoleVoter"/>
           <bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
       </list>
   </constructor-arg>
</bean>
<!-- OAuth2授权配置, 核心的核心 -->
<oauth2:authorization-server
       client-details-service-ref="clientDetailsService"
       token-services-ref="tokenServices"
       authorization-endpoint-url="/oauth/authorize"
       user-approval-handler-ref="oauth2ApprovalHandler"
       <!--用于显示授权页 -->
       user-approval-page="forward:/oauth2_code"
       error-page="forward:/msg/error">
   <!-- 同时支持四种授权方式、以及刷新Token-->
   <oauth2:authorization-code/>
   <oauth2:implicit/>
   <oauth2:refresh-token/>
   <oauth2:client-credentials/>
   <oauth2:password authentication-manager-ref="userAuthenticationManager"/>
</oauth2:authorization-server>
<!-- OAuth2 token对应用认证的捕获规则以及验证-->
<security:http pattern="/oauth/token"
create-session="stateless"
authentication-manager-ref="clientAuthenticationManager">

<security:custom-filter ref="clientCredentialsTokenEndpointFilter" before="BASIC_AUTH_FILTER"/>

<security:intercept-url pattern="/oauth/token" access="IS_AUTHENTICATED_FULLY"/>

<security:anonymous enabled="false"/>
<security:http-basic entry-point-ref="oauth2AuthenticationEntryPoint"/>
<security:access-denied-handler ref="oauth2DeniedHandler"/>
</security:http>

至此,Spring OAuth2的基础配置已经完成。在数据库的oauth_client_details中添加demo应用的详细信息,如client_id、client_secret、redirect_uri。

=========================================================================

验证配置过程:

1、用户Form登录:

POST http://example.com/login

username:xxx

password:XXX

2、OAuth2 implicit方式用户授权

GET http://example.com/oauth/authorize?redirect_uri=http://example.com/msg/code&client_id=ccc&response_type=token

3、用户授权回调action(浏览器会直接跳转,若为Rest工具手动执行以下请求)

POST http://example.com/oauth/authorize

user_oauth_approval:true

4、授权成功,回调http://example.com/msg/code#access_token=xxxxxxxxx&&expired_in=48400

注意回调时不会将access_token作为queryParam回调。

如果能够回调成功,说明配置OAuth2配置成功,其他三种方式仅限于2、3、4步的微小区别。

~如上文配置中提到的Auth Decision Manager,上述配置只是完成了Spring OAuth2的基础部分,具体到哪个应用能够访问哪些资源,资源权限的定义规则还没有给出,这就是授权访问中资源服务器的定义了。

相比于PHP、Python对于OAuth2协议的简单实现,Spring的实现略显笨重,配置复杂。Spring自成一体的闭合,将OAuth2基于Security进行实现,而且又对LADP等协议有不同的接驳,这或许也是Spring中间件越做越重的归宿~

当然Java方向上还有一个OAuth2的实现也可以关注,Apache Oltu

tiantongyuan-south