[JAVA] Spring Security usage memo session management

Basic and mechanical story Authentication / Authorization Story Remember-Me story CSRF story The story of the response header Method security story CORS story The story of Run-As The story of ACL Test story Talk about cooperation with MVC and Boot

Extra edition What Spring Security can and cannot do

Controlling the number of simultaneous sessions

By default, the same user can create multiple sessions. In other words, the same user can log in as many times as they want from different terminals.

By changing the settings, you can limit the number of simultaneous sessions or prevent multiple logins at all.

Limit the number of simultaneous sessions

Implementation

namespace

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>

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

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;
    }
}

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());
    }
}

Operation check

spring-security.jpg

Log in with your first browser.

spring-security.jpg

Log in as the same user in another browser.

spring-security.jpg

When I redisplay the first browser, an error message is displayed.

Description

web.xml


    <listener>
        <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
    </listener>

MySpringSecurityInitializer.java


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

--You need to register HttpSessionEventPublisher as a listener so that Spring Security can detect that a new session has been created. --If you are using web.xml, declare it normally with<listener>. --If you are using WebApplicationInitializer, you can override the ʻenableHttpSessionEventPublisher ()method to returntrue and it will register HttpSessionEventPublisher` as a listener.

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>

--Add the <session-management> tag, add the <concurrency-control> tag, and specify the maximum number of sessions with the max-sessions attribute.

MySpringSecurityConfig.java


...

@EnableWebSecurity
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {

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

--Specify with sessionManagement (). MaximumSessions (int).

Specify the error screen

By default, the screen will display only an error message, so skip to the specified page (for example, login page).

namespace

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>

--Specify the URL and path in the ʻexpired-url` attribute.

Java Configuration

MySpringSecurityConfig.java


...

@EnableWebSecurity
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {

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

--Specified by ʻexpiredUrl (String) `method.

Specify the processing at the time of error in the implementation

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");
    }
}

--Create a class that implements SessionInformationExpiredStrategy. --ʻOnExpiredSessionDetected ()method implements error processing. --Here, we are redirecting to/ login`.

namespace

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>

--Specify the bean of SessionInformationExpiredStrategy with ʻexpired-session-strategy-ref`.

Java Configuration

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());
    }
    
    ...
}

―― Register with the ʻexpiredSessionStrategy (SessionInformationExpiredStrategy) `method.

Prevent you from logging in beyond the limit

If you just specify max-sessions (maximumSessions), old sessions will be discarded if you log in beyond the upper limit.

Conversely, if you try to log in beyond the limit, you can make an error if you try to create a new session. However, be aware that if you enable this setting, you may not be able to log in again until your old session expires or you explicitly log out.

namespace

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>

--Specify true for ʻerror-if-maximum-exceeded`.

Java Configuration

MySpringSecurityConfig.java


...

@EnableWebSecurity
public class MySpringSecurityConfig extends WebSecurityConfigurerAdapter {

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

--Pass true in themaxSessionsPreventsLogin (boolean)method.

** Operation check **

spring-security.jpg

Log in with the first browser.

spring-security.jpg

If you try to log in as the same user on the second browser, an error will occur.

Specify the transition destination at the time of error

Implementation

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>The number of sessions has reached the limit</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>

The page to be the transition destination. An error message is being displayed.

namespace

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

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);
    }
    
    ...
}

Operation check

spring-security.jpg

Log in with the first browser.

spring-security.jpg

When you log in with the second browser, you will be taken to the specified error page (max-session-error.jsp).

Description

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" />

--If the number of sessions exceeds the upper limit and you try to log in to Form, the error page will be done by the ʻauthentication-failure-urlattribute of the tag which is the setting of Form login. ――Of course, the error page that is normally skipped by mistaken password etc. will be the page specified here. ――So, in reality, it seems necessary to specify the URL of the login screen, identify the error on the login screen side (or the controller of the login screen), and switch the message. --Alternatively, you can choose to handle authentication errors by specifying ʻAuthenticationFailureHandler in ʻauthentication-failure-handler-ref (for ʻ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)) --In case of session limit error, SessionAuthenticationException is thrown.

Why is Form login set?

Checked with UsernamePasswordAuthenticationFilter

Why is the specification of the transition destination when an error occurs in the session upper limit check is the setting of the <form-login> tag?

applicationContext.xml


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

--When you add the <session-management> tag, a Filter called SessionManagementFilter is added. --The number of simultaneous sessions is checked by the class SessionAuthenticationStrategy that this Filter has. --Specifically, ConcurrentSessionControlAuthenticationStrategy. --However, this SessionAuthenticationStrategy also has a ʻUsernamePasswordAuthenticationFilter that executes the Form login process. --Strictly speaking, it is possessed by the parent class ʻAbstractAuthenticationProcessingFilter. --ʻIf authentication with UsernamePasswordAuthenticationFilteris successful, the authentication process related to the session is delegated toSessionAuthenticationStrategy. --And at that time, the number of simultaneous sessions is checked. --If an error exceeds the upper limit, it is natural to handle the error with ʻUsernamePasswordAuthenticationFilter. --SessionManagementFilter is waiting after ʻUsernamePasswordAuthenticationFilter`. --Therefore, the Form login setting will be adopted for the error page specification that exceeds the maximum number of sessions.

So when does SessionManagementFilter work?

--SessionManagementFilter is in charge of processing when logged in by non-interactive authentication processing. --A typical non-interactive authentication process is automatic login with Remember-Me authentication.

Is it possible to entrust the processing to SessionManagementFilter even when logging in to Form?

This is a personal guess and I don't know if it's correct </ font>

--If the Form login is successful, the Filter chain will be interrupted and you will be redirected to the page after login. --That is, Filters after ʻUsernamePasswordAuthenticationFilterare not executed. --SinceSessionManagementFilter is a Filter after ʻUsernamePasswordAuthenticationFilter, it is not possible to entrust the session check at the time of Form login to SessionManagementFilter. --What if you put SessionManagementFilter before ʻUsernamePasswordAuthenticationFilter? I thought, but it may be because it is overwhelming to put SessionManagementFilter, which should be checked after successful login, before ʻUsernamePasswordAuthenticationFilter where login processing is performed.

Can't you combine it with Remember-Me?

When the number of sessions is exceeded by automatic login of Remember-Me

Implementation

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>

** Operation result **

spring-security.jpg

Log in with Remember-Me enabled (browser A).

Leave it for a while and wait for the session to time out (assuming the Remember-Me token has not expired).

Log in with another browser (browser B) and redraw the screen with the time-out browser (browser A).

spring-security.jpg

You are skipped to the 401 error screen (browser A).

When the path at the time of error is specified

By default, if Remember-Me fails to exceed the session limit, it will result in a 401 error as described above and will be skipped to the server error page.

To skip to any path, the reference says that you should specify the session-authentication-error-url attribute of the<session-management>tag.

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. (Translation) If the second authentication is done by another non-interactive mechanism (eg Remember-Me) (other than Form login), a 401 unauthorized error will be sent to the client. If you want to use the error page instead, you can add the session-authentication-error-url attribute to the session-management element.

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

However, if you actually specify that path, a redirect loop will occur.

Implementation

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>username: <input type="text" name="username" /></label>
            </div>
            <div>
                <label>password: <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>

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>

** Operation result **

spring-security.jpg

Why does this happen?

In case of Remember-Me, the number of sessions is checked by SessionManagementFilter. Here, the redirect is performed to the URL specified by session-authentication-error-url.

Then, when redirected to the URL, a series of Filters will be executed again, and Remember-Me will automatically log in and SessionManagementFilter will check the number of sessions. Naturally, the error occurs again and the redirect is made to the URL specified by session-authentication-error-url.

As a result, a redirect loop occurs.

I searched, but I couldn't find any trouble with the same story. I can't find a workaround.

Am I misunderstanding something ...?

By the way, this phenomenon does not occur when using the login page generated by Spring Security by default. This is avoided because the DefaultLoginPageGeneratingFilter exists before the SessionManagementFilter, so the default login page is transitioned before the session count check is repeated.

Session fixation attack countermeasures

For the explanation of the session fixation attack itself, refer to IPA explanation page.

As a countermeasure, it is recommended to change the session ID before and after login. Spring Security also has this countermeasure enabled by default, and the session ID (JSESSIONID) is changed before and after login.

Operation check

spring-security.jpg

JSESSIONID before login is376D515 ...

log in

spring-security.jpg

You can see that the JSESSIONID after login has changed to30194EA ....

Control session ID changes

For the time being, whether or not to change the session ID after this login can be changed in the settings.

namespace

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>

--Specify the session-fixation-protection attribute of<session-management> --Here, none is specified to disable the session ID change.

Java Configuration

MySpringSecurityConfig.java


...

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

    ...
}

--Set as sessionFixation (). none ()

Operation check

spring-security.jpg

Before login 8CC8D2 ...

spring-security.jpg

Even after logging in 8CC8D2 ...

It hasn't changed. However, I can't imagine a situation where you have to specify none. .. ..

You can specify the following values for session-fixation-protection:

value Description
none Do not change session ID
newSession Create a new session. Information other than the attribute information managed by Spring Security is discarded.
migrateSession Create a new session and port attributes as well (Servlet 3).Default in 0 or less environment)
changeSessionId HttpServletRequestAdded tochangeSessionId()Reassign the ID with (Servlet 3).Default in 1 and above environments)

reference

Recommended Posts

Spring Security usage memo session management
Spring Security usage memo CSRF
Spring Security usage memo Run-As
Spring Security Usage memo Method security
Spring Security usage memo CORS
Spring Security usage memo test
Spring Security usage memo Authentication / authorization
Spring Security usage memo response header
Spring Security usage memo Basic / mechanism
Spring Security Usage Memo Domain Object Security (ACL)
Spring Shell usage memo
JavaParser usage memo
WatchService usage memo
PlantUML usage memo
JUnit5 usage memo
JJUG CCC Spring 2018 memo
About Spring Security authentication
Spring boot memo writing (1)
Spring Security causes 403 forbidden
Spring boot memo writing (2)
[Personal memo] About Spring framework
JJUG CCC 2018 Spring participation memo
Spring Framework self-study memo series_1
Login function with Spring Security
[Spring Security] Spring Security on GAE (SE)
Try using Spring Boot Security
Spring boot controller method memo