摘要:Spring Security與Oauth2整合步驟中詳細描述了使用過程
,但它對於入門者有些重量級,比如將用戶信息、ClientDetails、token存入數據庫而非內存
。配置過程比較復雜,經過幾天時間試驗終於成功,下面我將具體的使用
Spring Security Oauth2完成password認證的過程記錄下來與大家分享。
關鍵字: HTTP Authentication, rest, spring security
, spring mvc
前提:IntelliJ IDEA (
13.1.5 版本),
apache maven
(
3.2.3 版本),
Tomcat(7.0.56版本), Spring(3.2.4版本),
spring-security-oauth2(2.0.7
版本
)
一、首先需要使用Spring MVC完成RESTful API的發布,這一步驟的詳細情況可見我的另一博文:應用Spring MVC 發布restful服務是怎樣的一種體驗
二、在/webapp/WEB-INF/web.xml文件中添加相應的filter:
org.springframework.web.filter.DelegatingFilterProxy
,
及
mapping
,具體如以下所示。
<?xml version=
"1.0"
encoding=
"UTF-8"
?>
<web-app xmlns= "http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation= "http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version= "3.1" >
<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>
<context- param >
< param - name >contextConfigLocation</ param - name >
< param - value >
/WEB-INF/security.xml
</ param - value >
</context- param >
<listener>
<listener- class >org.springframework.web.context.ContextLoaderListener</listener- class >
</listener>
<servlet>
<servlet- name >restful</servlet- name >
<servlet- class >org.springframework.web.servlet.DispatcherServlet</servlet- class >
<load-on-startup> 1 </load-on-startup>
</servlet>
<servlet-mapping>
<servlet- name >restful</servlet- name >
< url -pattern>/</ url -pattern>
</servlet-mapping>
</web-app>
<web-app xmlns= "http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation= "http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version= "3.1" >
<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>
<context- param >
< param - name >contextConfigLocation</ param - name >
< param - value >
/WEB-INF/security.xml
</ param - value >
</context- param >
<listener>
<listener- class >org.springframework.web.context.ContextLoaderListener</listener- class >
</listener>
<servlet>
<servlet- name >restful</servlet- name >
<servlet- class >org.springframework.web.servlet.DispatcherServlet</servlet- class >
<load-on-startup> 1 </load-on-startup>
</servlet>
<servlet-mapping>
<servlet- name >restful</servlet- name >
< url -pattern>/</ url -pattern>
</servlet-mapping>
</web-app>
三、在上一步的web.xml文件中可以看到,Spring需要加載
/WEB-INF/下的security.xml文件,因此我們在
/WEB-INF/下創建
security.xml文件,其主要內容如下所示。這里比
使用Spring Security完成RESTful服務用戶認證中的
security.xml文件配置要復雜得多(見我之前的博文:
使用Spring Security完成RESTful服務用戶認證的過程
)。注意,這里的配置文件中的
<security:http pattern=
"/abcs/**"
>
<security:intercept-
url
pattern=
"/abcs/**"
access=
"ROLE_ABCS"
/>
access必須指定一個角色名稱,使用
use-expressions=
"true"
isAuthenticated()
這里是不允許的。因此,我們需要在
實現UserDetails接口、實現
getAuthorities()方法時返回
此角色名稱,在擁有此角色的用戶認證通過后,才可以訪問/abcs/**資源。
<?xml version=
"1.0"
encoding=
"UTF-8"
?>
<beans xmlns= "http://www.springframework.org/schema/beans"
xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
xmlns:oauth2= "http://www.springframework.org/schema/security/oauth2"
xmlns:mvc= "http://www.springframework.org/schema/mvc"
xmlns:security= "http://www.springframework.org/schema/security"
xsi:schemaLocation= "http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2.xsd" >
<mvc:annotation-driven/>
<mvc:default-servlet-handler/>
<bean id = "tokenStore" class = "org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore" />
<bean id = "tokenServices" class = "org.springframework.security.oauth2.provider.token.DefaultTokenServices" >
<property name = "tokenStore" ref= "tokenStore" />
<property name = "supportRefreshToken" value = "true" />
<!--<property name="clientDetailsService" ref="clientDetailsService"/>-->
</bean>
<bean id = "clinetAuthenticationEntryPoint"
class = "org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint" />
<bean id = "accessDeniedHandler"
class = "org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler" />
<bean id = "userApprovalHandler"
class = "org.springframework.security.oauth2.provider.approval.DefaultUserApprovalHandler" />
<!--client-->
<bean id = "clientDetailsService" class = "com.anqi.dp.controllers.MyClientDetailsService" />
<bean id = "clientDetailsUserDetailsService"
class = "org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService" >
<constructor-arg ref= "clientDetailsService" />
</bean>
<bean id = "clientCredentialsTokenEndpointFilter"
class = "org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter" >
<property name = "authenticationManager" ref= "clientAuthenticationManager" />
</bean>
<security:authentication-manager id = "clientAuthenticationManager" >
<security:authentication-provider user-service-ref= "clientDetailsUserDetailsService" />
</security:authentication-manager>
<oauth2:authorization-server client-details-service-ref= "clientDetailsService" token-services-ref= "tokenServices"
user-approval-handler-ref= "userApprovalHandler" >
<oauth2:authorization- code />
<oauth2:implicit/>
<oauth2:refresh-token/>
<oauth2:client-credentials/>
<oauth2:password/>
</oauth2:authorization-server>
<security:http pattern= "/oauth/token" create-session= "stateless"
authentication-manager-ref= "clientAuthenticationManager" >
<security:anonymous enabled = "false" />
<security:http-basic entry-point-ref= "clinetAuthenticationEntryPoint" />
<security:custom-filter ref= "clientCredentialsTokenEndpointFilter" before= "BASIC_AUTH_FILTER" />
<security:access-denied-handler ref= "accessDeniedHandler" />
</security:http>
<!--client-->
<!--user-->
<bean id = "userService" class = "com.anqi.dp.controllers.UserService" />
<security:authentication-manager alias= "authenticationManager" >
<security:authentication-provider user-service-ref= "userService" >
<!--<security:password-encoder hash="md5"/>-->
</security:authentication-provider>
</security:authentication-manager>
<!--user-->
<oauth2:resource-server id = "mobileResourceServer" resource- id = "mobile-resource" token-services-ref= "tokenServices" />
<bean id = "accessDecisionManager" 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>
<security:http pattern= "/abcs/**" create-session= "never" entry-point-ref= "clinetAuthenticationEntryPoint"
access-decision-manager-ref= "accessDecisionManager" >
<security:anonymous enabled = "false" />
<security:intercept- url pattern= "/abcs/**" access= "ROLE_ABCS" />
<security:custom-filter ref= "mobileResourceServer" before= "PRE_AUTH_FILTER" />
<security:access-denied-handler ref= "accessDeniedHandler" />
</security:http>
</beans>
<beans xmlns= "http://www.springframework.org/schema/beans"
xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance"
xmlns:oauth2= "http://www.springframework.org/schema/security/oauth2"
xmlns:mvc= "http://www.springframework.org/schema/mvc"
xmlns:security= "http://www.springframework.org/schema/security"
xsi:schemaLocation= "http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2.xsd" >
<mvc:annotation-driven/>
<mvc:default-servlet-handler/>
<bean id = "tokenStore" class = "org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore" />
<bean id = "tokenServices" class = "org.springframework.security.oauth2.provider.token.DefaultTokenServices" >
<property name = "tokenStore" ref= "tokenStore" />
<property name = "supportRefreshToken" value = "true" />
<!--<property name="clientDetailsService" ref="clientDetailsService"/>-->
</bean>
<bean id = "clinetAuthenticationEntryPoint"
class = "org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint" />
<bean id = "accessDeniedHandler"
class = "org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler" />
<bean id = "userApprovalHandler"
class = "org.springframework.security.oauth2.provider.approval.DefaultUserApprovalHandler" />
<!--client-->
<bean id = "clientDetailsService" class = "com.anqi.dp.controllers.MyClientDetailsService" />
<bean id = "clientDetailsUserDetailsService"
class = "org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService" >
<constructor-arg ref= "clientDetailsService" />
</bean>
<bean id = "clientCredentialsTokenEndpointFilter"
class = "org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter" >
<property name = "authenticationManager" ref= "clientAuthenticationManager" />
</bean>
<security:authentication-manager id = "clientAuthenticationManager" >
<security:authentication-provider user-service-ref= "clientDetailsUserDetailsService" />
</security:authentication-manager>
<oauth2:authorization-server client-details-service-ref= "clientDetailsService" token-services-ref= "tokenServices"
user-approval-handler-ref= "userApprovalHandler" >
<oauth2:authorization- code />
<oauth2:implicit/>
<oauth2:refresh-token/>
<oauth2:client-credentials/>
<oauth2:password/>
</oauth2:authorization-server>
<security:http pattern= "/oauth/token" create-session= "stateless"
authentication-manager-ref= "clientAuthenticationManager" >
<security:anonymous enabled = "false" />
<security:http-basic entry-point-ref= "clinetAuthenticationEntryPoint" />
<security:custom-filter ref= "clientCredentialsTokenEndpointFilter" before= "BASIC_AUTH_FILTER" />
<security:access-denied-handler ref= "accessDeniedHandler" />
</security:http>
<!--client-->
<!--user-->
<bean id = "userService" class = "com.anqi.dp.controllers.UserService" />
<security:authentication-manager alias= "authenticationManager" >
<security:authentication-provider user-service-ref= "userService" >
<!--<security:password-encoder hash="md5"/>-->
</security:authentication-provider>
</security:authentication-manager>
<!--user-->
<oauth2:resource-server id = "mobileResourceServer" resource- id = "mobile-resource" token-services-ref= "tokenServices" />
<bean id = "accessDecisionManager" 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>
<security:http pattern= "/abcs/**" create-session= "never" entry-point-ref= "clinetAuthenticationEntryPoint"
access-decision-manager-ref= "accessDecisionManager" >
<security:anonymous enabled = "false" />
<security:intercept- url pattern= "/abcs/**" access= "ROLE_ABCS" />
<security:custom-filter ref= "mobileResourceServer" before= "PRE_AUTH_FILTER" />
<security:access-denied-handler ref= "accessDeniedHandler" />
</security:http>
</beans>
四、當然要新建com.anqi.dp.UserService類了,其在
security.xml文件中配置其為
authenticationManager
的
authentication-provider。
com.anqi.dp.UserService類實現自UserDetailsService接口,它需要實現一loadUserByUsername方法,在實現此方法的過程中,又需要新建MyUserDetails類來實現UserDetails接口。實現
loadUserByUsername方法時,可以自己依需要從關系數據庫、NoSQL
或者其它存放用戶信息的地方獲取。示例代碼可以查看
之前的博文:
使用Spring Security完成RESTful服務用戶認證的過程
圖1 UserService示例及用戶名密碼登陸
。注意,因為配了
access=
"ROLE_ABCS"
,因此需要
在擁有相應角色的用戶
getAuthorities()方法內返回
此角色名稱:
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_ABCS"); 。
還要新建com.anqi.dp.MyClientDetailsService類,其
在
security.xml文件中配置其為
clientAuthenticationManager
的
authentication-provider。
com.anqi.dp.MyClientDetailsService類實現自
ClientDetailsService接口
,它需要實現一loadClientByClientId方法,在實現此方法的過程中,又需要新建MyClientDetails類來實現ClientDetails接口。實現
loadClientByClientId
方法時,可以自己依需要從關系數據庫、NoSQL
或者其它存放客戶端信息的地方獲取。示例代碼可以查看
圖1
MyClientDetailsService
示例及token獲取。注意,
這里的
getAuthorities()方法對
配的
access=
"ROLE_ABCS"
沒有影響。

圖1
MyClientDetailsService
示例及token獲取
五、經過以上的步驟,我們就可以進行RESTful服務發布了,發布成功后,需要進行用戶認證的試驗。
1、如圖1
MyClientDetailsService
示例及token獲取
所示,我們使用REST Client工具對
http://127.0.0.1:8088/restfulservice/oauth/token
路徑發出POST請求,其中需要在Request Parameters中添加client_id、client_secret、grant_type與user_name、password鍵值對。
如此,
即進行了模擬的通過用戶名密碼獲取token的過程
。圖2中是client認證失敗時的Response,我將client_secret更改后的結果。圖3是user認證失敗時的
Response,我將password更改后的結果。
圖4是認證成功時的
Response。可以看出,在認證成功時的
Response中存在access_token字段,這就是我們獲取到的token
。

圖2 client認證失敗

圖3 user認證失敗

圖4 認證成功
2、我們在認證成功的條件下,使用上面步驟中返回的
access_token
對
http://localhost:8088/restfulservice/abcs/6?access_token=a7f3e13e-cbb0-417d-a9f8-9764d11db00f
進行
GET
請求,即可以成功得到返回結果,
我是使用瀏覽器進行HTTP請求的
。(具體的邏輯使用Spring MVC的Control完成,見我之前的博文:)。在調試時,可以看到每次請求,進入對應Controller后,代碼均會轉入
UserDetails的String getUsername()方法中。
如果對請求路徑里的
access_token值
稍作修改,如再
對
http://localhost:8088/restfulservice/abcs/6?access_token=a7f3e13e-cbb0-417d-a9f8-9764d11db008
進行
GET
請求,則返回不到正確結果,如圖5所示,即返回Invalid access token錯誤。

圖5
帶正確的access_token值請求返回的結果

圖6 帶不正確的access_token值請求返回的結果
如果刪除access_token,不帶
access_token值對
http://localhost:8088/restfulservice/abcs/6
進行GET請求時,返回的錯誤信息如圖7所示。

圖7 不帶
access_token值請求返回的結果
這就說明,我們的用戶認證配置達到了預期效果。
最近有各種之前沒有碰到過的問題、技術,有時間整理好分享給大家。