Spring Securityを利用して、リバースプロキシ型やエージェント型などのSSO認証を行います。
前提
Idpで認証確認した後、リクエストヘッダに認証情報を付加してリクエストしてくるという想定です。
環境
- Java8
- Spring Boot 2.0.4.RELEASE
<dependencies>
<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>
</dependencies>
やり方
Spring SeccurityにはRequestHeaderAuthenticationFilter
というクラスがあり、ヘッダから情報を取得して認証を行います。
認証処理自体はAuthenticationManager
に任せます。
AuthenticationProviderとしては、PreAuthenticatedAuthenticationProvider
を利用します。
その際、UserDetais
を生成するクラスは実装する必要があります。
@SpringBootApplication
@RestController
public class SampleApplication {
public static void main(String[] args) {
SpringApplication.run(SampleApplication.class, args);
}
@GetMapping("/sample")
public String sample() {
return "Hello World!!";
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter() throws Exception {
RequestHeaderAuthenticationFilter filter = new RequestHeaderAuthenticationFilter();
filter.setCredentialsRequestHeader("credentials");
filter.setPrincipalRequestHeader("principal");
filter.setAuthenticationManager(authenticationManager());
return filter;
}
@Bean
public PreAuthenticatedAuthenticationProvider preAuthenticationProvider() {
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(auds);
return provider;
}
@Autowired
private AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> auds;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(preAuthenticationProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilter(requestHeaderAuthenticationFilter()).authorizeRequests().anyRequest().authenticated();
}
}
@Component
public class SampleUserDetailsService implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> {
@Override
public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token) {
return new User(token.getPrincipal().toString(), "N/A", AuthorityUtils.createAuthorityList("ADMIN"));
}
}
確認
Idpを構築するのは面倒なので、ヘッダを付与した状態でリクエストを送信してみます。
まずはヘッダなし。
エラーになっていることがわかります。
C:\Users\d-yosh> curl -I http://localhost:8080/sample
HTTP/1.1 500
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
Content-Type: text/html;charset=utf-8
Content-Language: en
Content-Length: 820
Date: Fri, 31 Aug 2018 01:23:02 GMT
Connection: close
続いてヘッダあり。
正常にリクエストが完了しています。
C:\Users\d-yosh> curl -I -H "principal:fugafuga" -H "credentials:key" localhost:8080/sample
HTTP/1.1 200
Set-Cookie: JSESSIONID=AD66429DA4978F23BE0A0B0F2C214EA3; Path=/; HttpOnly
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
Content-Type: text/plain;charset=UTF-8
Content-Length: 13
Date: Fri, 31 Aug 2018 01:37:25 GMT
解説
RequestHeaderAuthenticationFilter
リクエストヘッダから情報を取得して、AuthenticationManagerに認証処理を依頼します。
ヘッダからは、principalとcredentialsを取得します。
認証処理を行う前に、SecurityContextに認証情報が格納されている場合は、認証済みとして認証処理を行いません。
なお、checkForPrincipalChanges
をtrue
にすると、リクエストヘッダに設定されたprincipalsの値と、SecurityContextに格納されている値が異なる場合には再度認証処理が行われます。
(デフォルトではfalse
です。)
このとき、ヘッダの値とUserDetails
のgetUserName()の値が等しいことを確認します。
PreAuthenticatedAuthenticationProvider
実際に認証処理を行います。
実はそんなに大したことは行っていなくて、principalとcredentialsを確認して、nullであれば例外をスロー、nullでない場合は、AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken>
を実装したクラスを利用してUserDetails
を取得します。
このように最低限の処理しか行っていないので、たとえばcredentialsの中身を確認するような処理が必要なのであれば、このクラスを拡張して実装する必要があります。
AuthenticationUserDetailsService
PreAuthenticatedAuthenticationToken
を利用してUserDetails
を取得する処理を実装します。
今回のサンプルでは適当なデータを詰めて返却していますが、DBなどからユーザ情報を取り出すななどする必要があると思います。
使ってみた感想として、ある程度クラスが用意されているので、結構手軽に実装できるなーという印象でした。
しかし、細かいことをやろうとすると拡張は必須なのかなと感じます。