Spring SecurityでSSO（not SAML）

Spring Securityを利用して、リバースプロキシ型やエージェント型などのSSO認証を行います。

# 前提
Idpで認証確認した後、リクエストヘッダに認証情報を付加してリクエストしてくるという想定です。

# 環境
* Java8
* Spring Boot 2.0.4.RELEASE

```xml:pom.xmlのdependencyだけ抜き出し。
<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`を生成するクラスは実装する必要があります。

```java
@SpringBootApplication
@RestController
public class SampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(SampleApplication.class, args);
    }

    @GetMapping("/sample")
    public String sample() {
        return "Hello World!!";
    }
}
```

```java
@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();
    }
}
```
```java
@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などからユーザ情報を取り出すななどする必要があると思います。

------------
使ってみた感想として、ある程度クラスが用意されているので、結構手軽に実装できるなーという印象でした。
しかし、細かいことをやろうとすると拡張は必須なのかなと感じます。