Pitfalls when Integrating Spring Sessions with Spring Security

Spring Sessions allows you to persist user sessions into a database. Combined with Spring Security, it is easy to create a setup where each logged in user is represented by a row in a SPRING_SESSION table. Externalising sessions into a database is also a prerequisite for a zero-downtime deployment strategy where users can stay logged in while a new application version is deployed.

On paper, the integration of those two Spring components is very easy. Assuming you have a working Spring Security setup, just add this dependency:

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-jdbc</artifactId>
</dependency>

and add this line to your application.properties:

spring.session.store-type=jdbc

and you should be good to go!

Value too long for column PRINCIPAL_NAME

In my case, the setup was unfortunately not working out of the box. After login, I noticed the following exception:

org.h2.jdbc.JdbcSQLDataException: Value too long for column """PRINCIPAL_NAME"" VARCHAR(100)": "STRINGDECODE('Customer{id=123, email=''[email protected]'', firstName=''Jane'', lastName=''Doe'', company='... (179)"; SQL statement:
UPDATE SPRING_SESSION SET SESSION_ID = ?, LAST_ACCESS_TIME = ?, MAX_INACTIVE_INTERVAL = ?, EXPIRY_TIME = ?, PRINCIPAL_NAME = ? WHERE PRIMARY_ID = ? [22001-199]

As stated in the exception, the data format of one of the newly created database tables did not expect a PRINCIPAL_NAME to exceed 100 characters. The principal in Spring Security is your user object (e.g., a class named “User” or something similar that is used in the login process). Therefore, this error means that there is some problem when Spring Security tries to create the user session in the database.

We can have a look at the schema definition of the SPRING_SESSION table to further understand the data schema – it is located at .m2/repository/org/springframework/session/spring-session-jdbc/<version>.RELEASE/spring-session-jdbc-<version>.RELEASE.jar!/org/springframework/session/jdbc/schema-h2.sql if you are using an h2 database (which is fine for testing):

CREATE TABLE SPRING_SESSION (
  PRIMARY_ID CHAR(36) NOT NULL,
  SESSION_ID CHAR(36) NOT NULL,
  CREATION_TIME BIGINT NOT NULL,
  LAST_ACCESS_TIME BIGINT NOT NULL,
  MAX_INACTIVE_INTERVAL INT NOT NULL,
  EXPIRY_TIME BIGINT NOT NULL,
  PRINCIPAL_NAME VARCHAR(100),
  CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
);

CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
CREATE INDEX SPRING_SESSION_IX2 ON SPRING_SESSION (EXPIRY_TIME);
CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);

CREATE TABLE SPRING_SESSION_ATTRIBUTES (
  SESSION_PRIMARY_ID CHAR(36) NOT NULL,
  ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
  ATTRIBUTE_BYTES LONGVARBINARY NOT NULL,
  CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
  CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
);

As you can see, the PRINCIPAL_NAME column must not exceed 100 characters.

So which attribute of your User class is ending up in this column? This was the tricky part – per default it is the result of the toString()-method. If you have something autogenerated from your IDE, it is very likely to exceed 100 characters.

The fix for this problem is to just return either the primary key field (e.g., the numeric ID) or a field with a unique constraint (the email-address is a very good candidate) from the toString() method.

NotSerializableException

There was another Exception once I took care of the toString()-method:

Failed to convert from type [java.lang.Object] to type [byte[]] for value 'org.springframework.security.core.context.SecurityContextImpl@5c84386: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@5c84386: Principal: [email protected]; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@ffff8868: RemoteIpAddress: 127.0.0.1; SessionId: 76cd5a48-3878-462e-a638-c8f7bb93e612; Not granted any authorities'; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.io.NotSerializableException: org.example.Customer

Apparently, Spring Security needs your Principal entity and all the entities that are referenced directly via relations to be classes implementing the Serializable interface:

@Entity
@Data
@Transactional
public class User implements Serializable {
  // ...
}

Implementing the interface is enough, there are no methods that need to be implemented as well.

Conclusion

Dealing with these two exceptions was all it took for me to adopt Spring Session in my Spring Security-protected application. Let me know if this was helpful for you or if you found even better workarounds for those issues.

As a reminder, please do not use h2 databases in production but switch to Postgres or MariaDB instead. While redis or Hazelcast are even better alternatives for session storage as far as performance is concerned, a relational database is often sufficient for a small to medium number of concurrent user connections and does not require you to setup another service.

Bernhard Knasmüller on Software Development