Spring Security - Oauth2

今天要跟大家介紹 Spring Security Oauth2 ,前一章跟大家介紹的Spring Security - 土炮法,在實作的時候就發現,難道 Spring Security 沒有提供標準的token做法嗎?登入換取tokentoken到期須更新新的token,難道這沒有所謂的標準動作嗎?後來再去找,發現我想要的效果就是 Spring Security Oauth2 了。

在開始之前,大家可能需要先了解一下 Oauth2 的基本概念,Oauth2分成4種方式,這次主要是跟大家介紹其中的password模式。

下圖節錄IETF中:

(A) The resource owner provides the client with its username and
password.

(B) The client requests an access token from the authorization
server’s token endpoint by including the credentials received
from the resource owner. When making the request, the client
authenticates with the authorization server.

(C) The authorization server authenticates the client and validates
the resource owner credentials, and if valid, issues an access
token.

簡單的說,就是會有個認證 Server,當客戶端需要進行認證時,需要像認證 Server 提供帳號密碼資訊換取token做後續的訪問。

Quick Start

這次的[Demo]為了簡化流程我們先將 Authorization Server 與 Resource owner 進行合併。

加入 Spring Security 設定

  • 首先我們需要在我們的pom.xml中加入我們需要的 dependency。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
<version>1.8.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
  • 建立 Main Class:Applicatioin.java,需要啟用@EnableWebSecurity
1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableWebSecurity
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
  • 建立 WebSecurityConfig 繼承 WebSecurityConfigurerAdapter

允許 URI Path /oauth/* 不需要認證就可以通過。

1
2
3
4
5
6
7
8
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().anyRequest()
.and()
.authorizeRequests()
.antMatchers("/oauth/*").permitAll();
}

認證邏輯判斷在 customUserDetailsService

1
2
3
4
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService);
}

  • 建立 OAuth2AuthorizationServerConfigJwt 繼承 AuthorizationServerConfigurerAdapter

設定clientId, secret, grant_type, scopestoken 期限資訊。

1
2
3
4
5
6
7
8
9
10
@Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient(clientAdmin)
.secret(passwordEncoder().encode(clientAdminSecret))
.authorizedGrantTypes("password", "refresh_token")
.scopes("read", "write")
.accessTokenValiditySeconds(jwtAccessTokenValiditySeconds)
.refreshTokenValiditySeconds(jwtRefreshTokenValiditySeconds);
}

configure 可以設定 token 的儲存方式或是擴充…等。

1
2
3
4
5
6
7
8
9
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
endpoints.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.userDetailsService(customUserDetailsService)
.authenticationManager(authenticationManager);
}

tokenService 這邊使用 DefaultTokenServices

1
2
3
4
5
6
7
8
9
10
@Bean
@Primary
public DefaultTokenServices tokenServices() {
final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
defaultTokenServices.setTokenEnhancer(tokenEnhancer());
defaultTokenServices.setAuthenticationManager(authenticationManager);
return defaultTokenServices;
}

token 的轉換

1
2
3
4
5
6
7
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(jwtSecret);
converter.setAccessTokenConverter(customClaimAccessTokenConverter);
return converter;
}

  • 建立 OAuth2ResourceServerConfig 繼承 ResourceServerConfigurerAdapter

設定 Resource Server 認證條件

1
2
3
4
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
}

  • 建立 CustomClaimAccessTokenConverter 繼承 DefaultAccessTokenConverter 與實作 JwtAccessTokenConverterConfigurer
1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication authentication = super.extractAuthentication(map);
Authentication userAuthentication = authentication.getUserAuthentication();

if (userAuthentication != null) {
Collection<? extends GrantedAuthority> authorities = userAuthentication.getAuthorities();
String username = (String) userAuthentication.getPrincipal();
UserPrincipal principal = (UserPrincipal) customUserDetailsService.loadUserByUsername(username);
userAuthentication = new UsernamePasswordAuthenticationToken(principal, userAuthentication.getCredentials(), authorities);
}
return new OAuth2Authentication(authentication.getOAuth2Request(), userAuthentication);
}
  • 建立 CustomTokenEnhancer 實作 TokenEnhancer

增加 token 參數值

1
2
3
4
5
6
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
final Map<String, Object> additionalInfo = new HashMap<>();
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}

  • 建立 CustomUserDetailsService 實作 UserDetailsService

判斷帳號邏輯可寫在此處

1
2
3
4
5
6
7
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if(username.equals("test")) {
throw new UsernameNotFoundException("not find user");
}
return UserPrincipal.create(username);
}

  • 其他如 CurrentUser, JwtAuthenticationEntryPoint, UserPrincipal 基本上與『土炮法』一樣。

加入 Swagger 設定

  • 在 Main Class 加入 @EnableSwagger2
1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableSwagger2
@EnableWebSecurity
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
  • 增加 SwaggerConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.grd"))
.paths(PathSelectors.any())
.build()
.securitySchemes(Collections.singletonList(securityScheme()))
.securityContexts(Collections.singletonList(securityContext()));
}

private SecurityScheme securityScheme() {
GrantType grantType = new ResourceOwnerPasswordCredentialsGrant( "http://localhost:8080/oauth/token");

return new OAuthBuilder()
.name("Spring Security Oauth2")
.grantTypes(Collections.singletonList(grantType))
.scopes(Arrays.asList(scopes()))
.build();
}

private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(Collections.singletonList(new SecurityReference("Spring Security Oauth2", scopes())))
.forPaths(PathSelectors.any())
.build();
}

private AuthorizationScope[] scopes() {
return new AuthorizationScope[]{
new AuthorizationScope("read", "Read"),
new AuthorizationScope("write", "Write")
};
}

private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Spring Security Oauth2")
.description("Spring Security Oauth2")
.build();
}

驗證

Postman

須先設定 username 與 password 即 resource owner 的 clientId 與 secret

取得 token

更新 token

Swagger

Reference

Sample Code

謝謝您的支持與鼓勵

Ads