Spring Security 使い方メモ　セッション管理


:::note warn
Spring Security は 5.4 以降、設定の書き方に大幅な変更が入っています。
詳しくは @suke_masa さんの [Spring Security 5.7でセキュリティ設定の書き方が大幅に変わる件 - Qiita](https://qiita.com/suke_masa/items/908805dd45df08ba28d8) を参照してください。
:::


[基礎・仕組み的な話](http://qiita.com/opengl-8080/items/c105152c9ca48509bd0c)
[認証・認可の話](http://qiita.com/opengl-8080/items/032ed0fa27a239bdc1cc)
[Remember-Me の話](http://qiita.com/opengl-8080/items/7c34053c74448d39e8f5)
[CSRF の話](http://qiita.com/opengl-8080/items/3720ed415c2df26251a7)
[レスポンスヘッダーの話](http://qiita.com/opengl-8080/items/4e800eb6990653b0395c)
[メソッドセキュリティの話](http://qiita.com/opengl-8080/items/76715fd3a92b6ca680d0)
[CORS の話](http://qiita.com/opengl-8080/items/db0aa1ec0be36886953b)
[Run-As の話](http://qiita.com/opengl-8080/items/49aa6381c16c8b9a7f3c)
[ACL の話](http://qiita.com/opengl-8080/items/24a3118ef36bcf55ba71)
[テストの話](http://qiita.com/opengl-8080/items/eaa8f4eb9286a3df7986)
[MVC, Boot との連携の話](http://qiita.com/opengl-8080/items/4824d4a8c012f7ec53db)

番外編
[Spring Security にできること・できないこと](http://qiita.com/opengl-8080/items/6dc37f8b77abb5ae1642)

# 同時セッション数の制御
デフォルトだと、同じユーザーで複数のセッションをつくることができる。
つまり、同じユーザーで異なる端末からいくつでもログインすることができることになっている。

設定を変更すると、同時セッション数を制限したり、もしくは複数ログインを全くできないようにすることができる。

## 同時セッション数を制限する
### 実装
**namespace**

```xml:web.xml
<?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>
    
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    
    <listener>
        <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
    </listener>
</web-app>
```

```xml:applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
         http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
         http://www.springframework.org/schema/security
         http://www.springframework.org/schema/security/spring-security.xsd">
    
    <sec:http>
        <sec:intercept-url pattern="/login" access="permitAll" />
        <sec:intercept-url pattern="/**" access="isAuthenticated()" />
        <sec:form-login />
        <sec:logout />
        
        <sec:session-management>
            <sec:concurrency-control max-sessions="1" />
        </sec:session-management>
    </sec:http>
    
    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:user-service>
                <sec:user name="hoge" password="hoge" authorities="" />
            </sec:user-service>
        </sec:authentication-provider>
    </sec:authentication-manager>
</beans>
```


**Java Configuration**

```java:MySpringSecurityInitializer.java
package sample.spring.security;

import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;

public class MySpringSecurityInitializer extends AbstractSecurityWebApplicationInitializer {
    public MySpringSecurityInitializer() {
        super(MySpringSecurityConfig.class);
    }

    @Override
    protected boolean enableHttpSessionEventPublisher() {
        return true;
    }
}
```

```java:MySpringSecurityConfig.java
package sample.spring.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import java.util.Collections;

@EnableWebSecurity
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .sessionManagement().maximumSessions(1);
    }
    
    @Autowired
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("hoge")
            .password("hoge")
            .authorities(Collections.emptyList());
    }
}
```

### 動作確認
![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/02fcda63-1759-213c-64eb-ec4273efe545.jpeg)

１つ目のブラウザでログイン。

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/679bc5b0-3694-af21-1eac-7c27301e53f6.jpeg)

別のブラウザで、同じユーザでログイン。

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/cfb004d4-2289-fa9a-49cf-15e8d07ce7f7.jpeg)

１つ目のブラウザを再表示させると、エラーメッセージが表示される。

### 説明
```xml:web.xml
    <listener>
        <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
    </listener>
```

```java:MySpringSecurityInitializer.java
    @Override
    protected boolean enableHttpSessionEventPublisher() {
        return true;
    }
```

- セッションが新たに作成されたことを Spring Security が検知できるようにするため、 `HttpSessionEventPublisher` をリスナーとして登録する必要がある
- `web.xml` を使っているなら、普通に `<listener>` で宣言する。
- `WebApplicationInitializer` を利用しているなら、 `enableHttpSessionEventPublisher()` メソッドをオーバーライドして `true` を返すようにすると、 `HttpSessionEventPublisher` をリスナーをとして登録してくれる。

```xml:applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       ...>
    
    <sec:http>
        ...
        
        <sec:session-management>
            <sec:concurrency-control max-sessions="1" />
        </sec:session-management>
    </sec:http>
    
    ...
</beans>
```

- `<session-management>` タグを追加し、 `<concurrency-control>` タグを追加、 `max-sessions` 属性で最大セッション数を指定する。

```java:MySpringSecurityConfig.java
...

@EnableWebSecurity
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                ...
                .and()
                .sessionManagement().maximumSessions(1);
    }
    
    ...
}
```

- `sessionManagement().maximumSessions(int)` で指定する。

### エラー画面を指定する
デフォルトだとエラーメッセージしか表示されない画面になってしまうので、指定のページ（例えばログインページ）に飛ばすようにする。

**namespace**

```xml:applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       ...>
    
    <sec:http>
        ...
        
        <sec:session-management >
            <sec:concurrency-control max-sessions="1" expired-url="/login" />
        </sec:session-management>
    </sec:http>
    
    ...
</beans>
```

- `expired-url` 属性に URL, パスを指定する。

**Java Configuration**

```java:MySpringSecurityConfig.java
...

@EnableWebSecurity
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                ...
                .and()
                .sessionManagement().maximumSessions(1).expiredUrl("/login");
    }
    
    ...
}
```

- `expiredUrl(String)` メソッドで指定する。

### エラー時の処理を実装で指定する
```java:MySessionInformationExpiredStrategy.java
package sample.spring.security.session;

import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.session.SessionInformationExpiredEvent;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;

import javax.servlet.ServletException;
import java.io.IOException;

public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
    
    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        DefaultRedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
        redirectStrategy.sendRedirect(event.getRequest(), event.getResponse(), "/login");
    }
}
```

- `SessionInformationExpiredStrategy` を実装したクラスを作る。
- `onExpiredSessionDetected()` メソッドで、エラー時の処理を実装する。
    - ここでは、 `/login` にリダイレクトさせている。

**namespace**

```xml:applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       ...>
    
    <bean id="sies" class="sample.spring.security.session.MySessionInformationExpiredStrategy" />
    
    <sec:http>
        ...
        
        <sec:session-management >
            <sec:concurrency-control max-sessions="1" expired-session-strategy-ref="sies" />
        </sec:session-management>
    </sec:http>
    
    ...
</beans>
```

- `expired-session-strategy-ref` で、 `SessionInformationExpiredStrategy` の Bean を指定する。

**Java Configuration**

```java:MySpringSecurityConfig.java
...

import sample.spring.security.session.MySessionInformationExpiredStrategy;

@EnableWebSecurity
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                ...
                .and()
                .sessionManagement()
                    .maximumSessions(1)
                    .expiredSessionStrategy(new MySessionInformationExpiredStrategy());
    }
    
    ...
}
```

- `expiredSessionStrategy(SessionInformationExpiredStrategy)` メソッドで登録する。

## 上限を超えてログインできないようにする
`max-sessions` (`maximumSessions`) を指定しただけだと、上限を超えてログインした場合は古いセッションから破棄されていく。

逆に、上限を超えてログインしようとしたら新しくセッションを作ろうとしたほうをエラーにすることもできる。
ただし、この設定を有効にした場合、古いセッションが切れるか明示的にログアウトするまで新しいログインができなくなる可能性があるので注意。

**namespace**

```xml:applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       ...>
    
    <sec:http>
        ...
        
        <sec:session-management >
            <sec:concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
        </sec:session-management>
    </sec:http>
    
    ...
</beans>
```

- `error-if-maximum-exceeded` に `true` を指定する。

**Java Configuration**

```java:MySpringSecurityConfig.java
...

@EnableWebSecurity
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                ...
                .and()
                .sessionManagement()
                    .maximumSessions(1)
                    .maxSessionsPreventsLogin(true);
    }
    
    ...
}
```

- `maxSessionsPreventsLogin(boolean)` メソッドで `true` を渡す。

**動作確認**

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/a1b9848e-3eda-062c-aff4-43b33ceccaab.jpeg)

１つ目のブラウザでログインする。

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/af48708a-0cb4-dad3-8381-072bc55d7e8e.jpeg)

２つ目のブラウザで、同一ユーザでログインしようとすると、エラーになる。

### エラー時の遷移先を指定する
#### 実装
```jsp:max-session-error.jsp
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>Max Session</title>
    </head>
    <body>
        <h1>セッション数が上限に達しています</h1>
        
        <%@page import="org.springframework.security.core.AuthenticationException" %>
        <%@page import="org.springframework.security.web.WebAttributes" %>
        <%
            AuthenticationException e = (AuthenticationException)session.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
            pageContext.setAttribute("errorMessage", e.getMessage());
        %>
        
        <h2 style="color: red;">${errorMessage}</h2>
    </body>
</html>
```

遷移先にするページ。
エラーメッセージを表示させている。

**namespace**

```xml:applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       ...>
    
    <sec:http>
        <sec:intercept-url pattern="/login" access="permitAll" />
        <sec:intercept-url pattern="/max-session-error.jsp" access="permitAll" />
        <sec:intercept-url pattern="/**" access="isAuthenticated()" />
        <sec:form-login authentication-failure-url="/max-session-error.jsp" />
        <sec:logout />
        
        <sec:session-management>
            <sec:concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
        </sec:session-management>
    </sec:http>
    
    ...
</beans>
```

**Java Configuration**

```java:MySpringSecurityConfig.java
...

@EnableWebSecurity
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login").permitAll()
                .antMatchers("/max-session-error.jsp").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                    .failureUrl("/max-session-error.jsp")
                .and()
                .sessionManagement()
                    .maximumSessions(1)
                    .maxSessionsPreventsLogin(true);
    }
    
    ...
}
```

#### 動作確認
![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/c6bee4ed-8056-9866-1e37-d47341135e3b.jpeg)

１つ目のブラウザでログインする。

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/1985d08e-d0a5-2562-4df2-dfaffb318d22.jpeg)

２つ目のブラウザでログインすると、指定したエラーページ（`max-session-error.jsp`）に遷移する。

#### 説明
```xml:applicationContext.xml
        <sec:intercept-url pattern="/login" access="permitAll" />
        <sec:intercept-url pattern="/max-session-error.jsp" access="permitAll" />
        <sec:intercept-url pattern="/**" access="isAuthenticated()" />
        <sec:form-login authentication-failure-url="/max-session-error.jsp" />
```

- セッション数が上限を超えて Form ログインしようとした場合のエラーページは、 Form ログインの設定である `<form-login>` タグの `authentication-failure-url` 属性で行うことになる。
- 当然、普通にパスワードなどをミスって飛ばされるエラーページもここで指定したページになってしまう。
    - なので、実際はログイン画面の URL を指定して、ログイン画面側（もしくはログイン画面のコントローラ）でエラーを識別してメッセージを切り替えるようなことが必要になりそう。
    - もしくは `authentication-failure-handler-ref` で `AuthenticationFailureHandler` を指定することで、認証エラーをハンドリングするという選択もある(`AuthenticationFailureHandler` については[こちら](http://qiita.com/opengl-8080/items/032ed0fa27a239bdc1cc#%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%AB%E5%A4%B1%E6%95%97%E3%81%97%E3%81%9F%E3%81%A8%E3%81%8D%E3%81%AE%E5%87%A6%E7%90%86%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%99%E3%82%8B))
    - セッション上限エラーの場合は、 `SessionAuthenticationException` がスローされる。

### なんで Form ログインの設定になる？
#### UsernamePasswordAuthenticationFilter でチェックが行われている
セッション上限数チェックでエラーになったときの遷移先の指定が、なぜ `<form-login>` タグの設定になるのか？

```xml:applicationContext.xml
        <sec:session-management>
            <sec:concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
        </sec:session-management>
```

- `<session-management>` タグを追加すると、 `SessionManagementFilter` という Filter が追加される。
- この Filter が持つ `SessionAuthenticationStrategy` というクラスで、同時セッション数のチェックなどが行われる。
    - 具体的には `ConcurrentSessionControlAuthenticationStrategy`。
- ところが、この `SessionAuthenticationStrategy` は、 Form ログイン処理を実行する `UsernamePasswordAuthenticationFilter` も持っている。
    - 厳密には、親クラスの `AbstractAuthenticationProcessingFilter` が持ってる。
- `UsernamePasswordAuthenticationFilter` での認証が成功すると、 `SessionAuthenticationStrategy` にセッションに関する認証処理が委譲される。
- そして、そのときに同時セッション数のチェックが行われる。
- ここで上限数オーバーのエラーになった場合、エラーをハンドリングするのは当然 `UsernamePasswordAuthenticationFilter` になる。
    - `SessionManagementFilter` は、 `UsernamePasswordAuthenticationFilter` の後で待ち構えている。
- よって、セッション上限数オーバーのエラーページ指定は Form ログインの設定の方が採用されることになる。

#### じゃあ SessionManagementFilter はいつ仕事する？
- `SessionManagementFilter` は、非対話型の認証処理でログインされた場合のときの処理を担当している。
- 非対話型の認証処理で典型的なのは、 Remember-Me 認証による自動ログイン。

#### Form ログインのときも SessionManagementFilter に処理を託せないのか？
<font color="red">これは個人的な推測で、正しいかどうかは分かっていません</font>

- Form ログインに成功すると、 Filter の連鎖はそこで中断し、ログイン後のページにリダイレクトする処理が行われる。
- つまり、 `UsernamePasswordAuthenticationFilter` より後ろにいる Filter は実行されない。
- `SessionManagementFilter` は `UsernamePasswordAuthenticationFilter` より後ろにいる Filter なので、 Form ログインのときのセッションチェックを `SessionManagementFilter` に託すことはできない。
- `SessionManagementFilter` を `UsernamePasswordAuthenticationFilter` より前に置いたらどう？　と思ったが、ログインが成功した後のチェックをするはずの `SessionManagementFilter` をログイン処理が行われる `UsernamePasswordAuthenticationFilter` の前に置くのは本末転倒なので、こうなっているのかもしれない。

### Remember-Me と組み合わせられない？
#### Remember-Me の自動ログインでセッション数オーバーになった場合
**実装**

```xml:applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       ...>
    
    <sec:http>
        <sec:intercept-url pattern="/login" access="permitAll" />
        <sec:intercept-url pattern="/**" access="isAuthenticated()" />
        <sec:form-login />
        <sec:remember-me />
        <sec:logout />
        
        <sec:session-management>
            <sec:concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
        </sec:session-management>
    </sec:http>
    
    ...
</beans>
```

**動作結果**

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/7881a04d-b327-ab49-0523-7fad3cdcbecd.jpeg)

Remember-Me を有効にしてログインする（ブラウザＡ）。

しばらく放置して、セッションがタイムアウトするのを待つ（Remember-Me トークンの期限は切れていない前提）。

別のブラウザ（ブラウザＢ）でログインし、タイムアウトしたブラウザ（ブラウザＡ）で画面を再描画する。

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/079c6eeb-d24b-c7d3-f26b-827998a377e8.jpeg)

401 エラー画面に飛ばされる（ブラウザＡ）。

#### エラー時のパスを指定した場合
デフォルトだと、 Remember-Me でセッション上限オーバーのエラーが発生すると、上記のように 401 エラーになってサーバーのエラーページに飛ばされてしまう。

任意のパスに飛ばすには、リファレンスでは `<session-management>` タグの `session-authentication-error-url` 属性を指定すればいいと書いてある。

> If the second authentication takes place through another non-interactive mechanism, such as "remember-me", an "unauthorized" (401) error will be sent to the client.
> If instead you want to use an error page, you can add the attribute session-authentication-error-url to the session-management element.
> （訳）
> もし２回目の認証が他（Formログイン以外）の非対話型メカニズム（例えば Remember-Me）によって行われる場合、 401 unauthorized エラーがクライアントに送信されます。
> もし代わりにエラーページを使いたい場合、 `session-management` 要素に `session-authentication-error-url` 属性を追加することができます。

https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#ns-concurrent-sessions


しかし、実際にそのパスを指定すると、リダイレクトループが発生してしまう。

**実装**

```jsp:my-login.jsp
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!doctype html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>My Login Page</title>
    </head>
    <body>
        <h1>My Login Page</h1>
        
        <c:url var="loginUrl" value="/login" />
        <form action="${loginUrl}" method="post">
            <div>
                <label>ユーザー名: <input type="text" name="username" /></label>
            </div>
            <div>
                <label>パスワード: <input type="password" name="password" /></label>
            </div>
            <div>
                <label>Remember-Me : <input type="checkbox" name="remember-me"></label>
            </div>
            <input type="submit" value="login" />
            <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
        </form>
    </body>
</html>
```

```xml:applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       ...>
    
    <sec:http>
        <sec:intercept-url pattern="/login" access="permitAll" />
        <sec:intercept-url pattern="/my-login.jsp" access="permitAll" />
        <sec:intercept-url pattern="/**" access="isAuthenticated()" />
        <sec:form-login login-page="/my-login.jsp" />
        <sec:remember-me />
        <sec:logout />
        
        <sec:session-management session-authentication-error-url="/my-login.jsp">
            <sec:concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
        </sec:session-management>
    </sec:http>
    
    ...
</beans>
```

**動作結果**

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/b628db36-29b3-a2e9-a4e8-6ddf275bc9ca.jpeg)

##### なぜこんな現象が発生するのか？
Remember-Me の場合は `SessionManagementFilter` でセッション数のチェックが行われる。
ここで `session-authentication-error-url` で指定した URL にリダイレクトが行われる。

すると、 URL にリダイレクトしたときに再び一連の Filter が実行され、 Remember-Me による自動ログインと `SessionManagementFilter` によるセッション数のチェックが行われる。
当然、再度エラーが発生し `session-authentication-error-url` で指定した URL にリダイレクトが行われる。

結果、リダイレクトのループが発生する。

検索したけど、同じような話で困っているというのは見当たらなかった。
回避方法も見つけられない。

なんか勘違いしているのだろうか・・・？

ちなみにこの現象は、 Spring Security がデフォルトで生成するログインページを使用している場合は発生しない。
これは、 `SessionManagementFilter` より前に `DefaultLoginPageGeneratingFilter` が存在しているため、セッション数のチェックが繰り返されるよりも前にデフォルトのログインページに遷移させられるため回避されている。

# セッション固定化攻撃対策
セッション固定化攻撃自体の説明は、 [IPA の解説ページとか](https://www.ipa.go.jp/security/awareness/vendor/programmingv2/contents/305.html)を参照。

対策としては、ログインの前後でセッションIDを変更するのが良いとされている。
Spring Security もデフォルトでこの対策が有効になっており、ログインの前後でセッションID(`JSESSIONID`)が変更されるようになっている。

## 動作確認
![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/73e24bab-f725-bbac-4320-5516918f7229.jpeg)

ログイン前の `JSESSIONID` は `376D515...`

ログインする

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/cd89241c-07cc-ede2-2a84-8f1fb4429447.jpeg)

ログイン後の `JSESSIONID` は `30194EA...` と変わっているのが分かる。

## セッションID の変更を制御する
一応、このログイン後にセッションID を変更するかどうかは、設定で変更できるようになっている。

**namespace**

```xml:applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:sec="http://www.springframework.org/schema/security"
       ...>

    <sec:http>
        ...
        <sec:session-management session-fixation-protection="none" />
    </sec:http>

    ...
</beans>
```

- `<session-management>` の `session-fixation-protection` 属性を指定
- ここでは `none` を指定して、セッションID の変更を無効にしている

**Java Configuration**

```java:MySpringSecurityConfig.java
...

@EnableWebSecurity
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                ...
                .and()
                .sessionManagement()
                    .sessionFixation().none();
    }

    ...
}
```

- `sessionFixation().none()` と設定

### 動作確認
![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/1192802a-8909-99fd-6ca9-807d62d5786a.jpeg)

ログイン前が `8CC8D2...`

![spring-security.jpg](https://qiita-image-store.s3.amazonaws.com/0/28302/78ba9d44-7b13-fcc4-a2bd-b0fe60028d18.jpeg)

ログイン後も `8CC8D2...`

変わらなくなった。
ただ、 `none` を指定しなければならない場面は想像できないが。。。

`session-fixation-protection` には次のような値を指定できる

|値|説明|
|---|---|
|`none`|セッションID を変更しない|
|`newSession`|新しいセッションを作る。Spring Security が管理する属性情報以外は破棄される|
|`migrateSession`|新しいセッションを作って、属性も移植する（Servlet 3.0 以下の環境ではデフォルト）|
|`changeSessionId`|`HttpServletRequest` に追加された `changeSessionId()` で ID を振りなおす（Servlet 3.1 以上の環境ではデフォルト）|




# 参考
- [Spring Security Reference](http://docs.spring.io/spring-security/site/docs/4.2.1.RELEASE/reference/htmlsingle/)
- [TERASOLUNA Server Framework for Java (5.x) Development Guideline — TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.3.0.RELEASE documentation](http://terasolunaorg.github.io/guideline/5.3.0.RELEASE/ja/)
- [セッション固定攻撃 | 鳩丸ぐろっさり (用語集)](http://bakera.jp/glossary/%E3%82%BB%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E5%9B%BA%E5%AE%9A%E6%94%BB%E6%92%83)
- [web-common_3_1.xsd](http://www.oracle.com/webfolder/technetwork/jsc/xml/ns/javaee/web-common_3_1.xsd)
- [JSR 340: Java Servlet 3.1 Specification](https://jcp.org/en/jsr/detail?id=340)