序
前面的一篇文章講了spring security oauth2的client credentials授權模式,一般用於跟使用者無關的,開放平臺api認證相關的授權場景。本文主要講一下跟使用者相關的授權模式之一password模式。
回顧四種模式
OAuth 2.0定義了四種授權方式。
- 授權碼模式(authorization code)
- 簡化模式(implicit)
- 密碼模式(resource owner password credentials)
- 客戶端模式(client credentials)(
主要用於api認證,跟使用者無關
)
maven
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
複製程式碼
配置
security config
支援password模式要配置AuthenticationManager
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//需要正常執行的話,需要取消這段註釋,原因見下面小節
// @Override
// public void configure(HttpSecurity http) throws Exception {
// http.csrf().disable();
// http.requestMatchers().antMatchers("/oauth/**")
// .and()
// .authorizeRequests()
// .antMatchers("/oauth/**").authenticated();
// }
//配置記憶體模式的使用者
@Bean
@Override
protected UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("demoUser1").password("123456").authorities("USER").build());
manager.createUser(User.withUsername("demoUser2").password("123456").authorities("USER").build());
return manager;
}
/**
* 需要配置這個支援password模式
* support password grant type
* @return
* @throws Exception
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
複製程式碼
這個是比client credentials模式新增的配置,主要配置使用者以及authenticationManager
auth server配置
@Configuration
@EnableAuthorizationServer //提供/oauth/authorize,/oauth/token,/oauth/check_token,/oauth/confirm_access,/oauth/error
public class OAuth2ServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer
.tokenKeyAccess("permitAll()") //url:/oauth/token_key,exposes public key for token verification if using JWT tokens
.checkTokenAccess("isAuthenticated()") //url:/oauth/check_token allow check token
.allowFormAuthenticationForClients();
}
/**
* 注入authenticationManager
* 來支援 password grant type
*/
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("demoApp")
.secret("demoAppSecret")
.authorizedGrantTypes("client_credentials", "password", "refresh_token")
.scopes("all")
.resourceIds("oauth2-resource")
.accessTokenValiditySeconds(1200)
.refreshTokenValiditySeconds(50000);
}
複製程式碼
這裡記得注入authenticationManager來支援password模式
否則報錯如下
➜ ~ curl -i -X POST -d "username=demoUser1&password=123456&grant_type=password&client_id=demoApp&client_secret=demoAppSecret" http://localhost:8080/oauth/token
HTTP/1.1 400
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
X-Application-Context: application
Cache-Control: no-store
Pragma: no-cache
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 03 Dec 2017 07:01:27 GMT
Connection: close
{"error":"unsupported_grant_type","error_description":"Unsupported grant type: password"}
複製程式碼
resource server 配置
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
// /**
// * 要正常執行,需要反註釋掉這段,具體原因見下面分析
// * 這裡設定需要token驗證的url
// * 這些需要在WebSecurityConfigurerAdapter中排查掉
// * 否則優先進入WebSecurityConfigurerAdapter,進行的是basic auth或表單認證,而不是token認證
// * @param http
// * @throws Exception
// */
// @Override
// public void configure(HttpSecurity http) throws Exception {
// http.requestMatchers().antMatchers("/api/**")
// .and()
// .authorizeRequests()
// .antMatchers("/api/**").authenticated();
// }
}
複製程式碼
驗證
請求token
curl -i -X POST -d "username=demoUser1&password=123456&grant_type=password&client_id=demoApp&client_secret=demoAppSecret" http://localhost:8080/oauth/token
複製程式碼
返回
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
X-Application-Context: application
Cache-Control: no-store
Pragma: no-cache
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 03 Dec 2017 04:53:40 GMT
{"access_token":"4cfb16f9-116c-43cf-a8d4-270e824ce5d7","token_type":"bearer","refresh_token":"8e9bfbda-77e5-4d97-b061-4e319de7eb4a","expires_in":1199,"scope":"all"}
複製程式碼
攜帶token訪問資源
curl -i http://localhost:8080/api/blog/1\?access_token\=4cfb16f9-116c-43cf-a8d4-270e824ce5d7
複製程式碼
或者
curl -i -H "Accept: application/json" -H "Authorization: Bearer 4cfb16f9-116c-43cf-a8d4-270e824ce5d7" -X GET http://localhost:8080/api/blog/1
複製程式碼
返回
HTTP/1.1 302
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Set-Cookie: JSESSIONID=F168A54F0F3C3D96A053DB0CFE129FBF; Path=/; HttpOnly
Location: http://localhost:8080/login
Content-Length: 0
Date: Sun, 03 Dec 2017 05:20:19 GMT
複製程式碼
出錯原因見下一小結
成功返回
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
X-Application-Context: application
Content-Type: text/plain;charset=UTF-8
Content-Length: 14
Date: Sun, 03 Dec 2017 06:39:24 GMT
this is blog 1
複製程式碼
錯誤返回
HTTP/1.1 401
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Cache-Control: no-store
Pragma: no-cache
WWW-Authenticate: Bearer realm="oauth2-resource", error="invalid_token", error_description="Invalid access token: 466ee845-2d08-461b-8f62-8204c47f652"
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sun, 03 Dec 2017 06:39:28 GMT
{"error":"invalid_token","error_description":"Invalid access token: 466ee845-2d08-461b-8f62-8204c47f652"}
複製程式碼
WebSecurityConfigurerAdapter與ResourceServerConfigurerAdapter
二者都有針對http security的配置,他們的預設配置如下
WebSecurityConfigurerAdapter
spring-security-config-4.2.3.RELEASE-sources.jar!/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java
@Order(100)
public abstract class WebSecurityConfigurerAdapter implements
WebSecurityConfigurer<WebSecurity> {
//......
protected void configure(HttpSecurity http) throws Exception {
logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
//......
}
複製程式碼
可以看到WebSecurityConfigurerAdapter的order是100
ResourceServerConfigurerAdapter
spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/config/annotation/web/configuration/ResourceServerConfigurerAdapter.java
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
}
複製程式碼
它的order是SecurityProperties.ACCESS_OVERRIDE_ORDER - 1 spring-boot-autoconfigure-1.5.5.RELEASE-sources.jar!/org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerProperties.java
/**
* The order of the filter chain used to authenticate tokens. Default puts it after
* the actuator endpoints and before the default HTTP basic filter chain (catchall).
*/
private int filterOrder = SecurityProperties.ACCESS_OVERRIDE_ORDER - 1;
複製程式碼
由此可見WebSecurityConfigurerAdapter的攔截要優先於ResourceServerConfigurerAdapter
二者關係
- WebSecurityConfigurerAdapter用於保護oauth相關的endpoints,同時主要作用於使用者的登入(
form login
,Basic auth
) - ResourceServerConfigurerAdapter用於保護oauth要開放的資源,同時主要作用於client端以及token的認證(
Bearer auth
)
因此二者是分工協作的
- 在WebSecurityConfigurerAdapter不攔截oauth要開放的資源
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.requestMatchers().antMatchers("/oauth/**")
.and()
.authorizeRequests()
.antMatchers("/oauth/**").authenticated();
}
複製程式碼
- 在ResourceServerConfigurerAdapter配置需要token驗證的資源
@Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatchers().antMatchers("/api/**")
.and()
.authorizeRequests()
.antMatchers("/api/**").authenticated();
}
複製程式碼
這樣就大功告成
doc
- 理解OAuth 2.0(
good
) - Secure Spring REST API using OAuth2(
good
) - Handling error: UnsupportedGrantTypeException, Unsupported grant type: password #3
- Spring Oauth2 : Authentication Object was not found in the SecurityContext
- Spring Security OAuth2, which decides security?
- Using WebSecurityConfigurerAdapter with Spring OAuth2 and user-info-uri
- How to define order of spring security filter chain #1024
- Relation between WebSecurityConfigurerAdapter and ResourceServerConfigurerAdapter
- Spring Security OAuth2, which decides security?