I replaced the default serializer to GenericJackson2JsonRedisSerializer. And I have CustomUser that extends org.springframework.security.core.userdetails.User.
Serialization is successful, but when I call Authentication.getPrincipal(), it just returns org.springframework.security.core.userdetails.User.
I suspect that it happens because I call mapper.registerModules(SecurityJackson2Modules.getModules(this.loader)) and under the hood it registers its own User and UserMixin, but it's the only method that works for me. Any suggestions ?
The "@class" field in redis shows that principal is saved as CustomUser, so serialization is proceeding well.
{
"@class": "org.springframework.security.core.context.SecurityContextImpl",
"authentication": {
"@class": "org.springframework.security.authentication.UsernamePasswordAuthenticationToken",
"authorities": [
"java.util.ArrayList",
[
{
"@class": "com.realtortask.security.GrantedAuthorityImpl",
"authority": "USER"
},
{
"@class": "org.springframework.security.core.authority.SimpleGrantedAuthority",
"authority": "ROLE_USER"
}
]
],
"details": {
"@class": "org.springframework.security.web.authentication.WebAuthenticationDetails",
"remoteAddress": "remoteAddress",
"sessionId": null
},
"authenticated": true,
"principal": {
"@class": "somepackage.security.CustomUser",
"username": "username",
"password": null,
"authorities": [
"java.util.Collections$UnmodifiableSet",
[
{
"@class": "somepackage.security.GrantedAuthorityImpl",
"authority": "USER"
}
]
],
"userId": 939,
"enabled": true,
"accountNonExpired": true,
"credentialsNonExpired": true,
"accountNonLocked": true
},
"credentials": null
}
}
Redis config:
@Configuration
@EnableRedisHttpSession
public class SessionStorageConfig implements BeanClassLoaderAware {
private ClassLoader loader;
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModules(SecurityJackson2Modules.getModules(this.loader));
mapper.addMixIn(GrantedAuthorityImpl.class, GrantedAuthorityMixin.class);
mapper.addMixIn(CustomUser.class, CustomUserMixin.class);
return new GenericJackson2JsonRedisSerializer(mapper);
}
@Bean
public JedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory();
}
public void setBeanClassLoader(ClassLoader classLoader) {
this.loader = classLoader;
}
}
CustomUser:
public class CustomUser extends User {
private Integer userId;
public CustomUser(String username, String password, Collection<? extends GrantedAuthority> authorities, Integer userId) {
super(username, password, authorities);
this.userId = userId;
}
public CustomUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired,
boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities, Integer userId) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
this.userId = userId;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
}
CustomUserMixin:
public abstract class CustomUserMixin {
public CustomUserMixin(@JsonProperty("username") String username, @JsonProperty("password") String password,
@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
@JsonProperty("userId") Integer userId) {}
public CustomUserMixin(@JsonProperty("username") String username, @JsonProperty("password") String password,
@JsonProperty("enabled") boolean enabled, @JsonProperty("accountNonExpired") boolean accountNonExpired,
@JsonProperty("credentialsNonExpired") boolean credentialsNonExpired,
@JsonProperty("accountNonLocked") boolean accountNonLocked,
@JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
@JsonProperty("userId") Integer userId) {}
@JsonProperty("userId")
public abstract String getUserId();
}
GrantedAuthorityImpl:
public class GrantedAuthorityImpl implements GrantedAuthority {
private final String authority;
public GrantedAuthorityImpl(String authority){
this.authority = authority;
}
public String getAuthority() {
return authority;
}
}
GrantedAuthorityMixin:
public abstract class GrantedAuthorityMixin {
public GrantedAuthorityMixin(@JsonProperty("authority") String authority) {}
@JsonProperty("authority")
public abstract String getAuthority();
}
The problem is relevant
Because I haven't found the solution, I replaced CustomUser to default Spring User and I've put userId into username field.
Thanks for the report and the details. I see you have all your configuration, but if you provide a complete minimal example project I (or someone else) will be able to look at this faster.
I created the minimal example, you can see here - https://github.com/Nike185/springSerialization
More interesting for you will be package "security" and "SecurityService" in my example
@Nike185 Thanks for the example. Can you provide details on how to reproduce the issue, What you expect to happen and What actually does happen? I can run the application, but without digging around and guessing for a while, I don't know the answers to these questions which means it will take longer for me to help.
To reproduce the issue:
Start the app;
Register a new user;
Add a breakpoint in SecurityService.getCurrentSecurityId(), line 23 and in the debugger you can see returned principal in Authentication object.
What I expect ?
Spring returns CustomUser which is stored in redis
What actually does happen ?
Spring returns its own org.springframework.security.core.userdetails.User and I can't get my own fields that placed in CustomUser
I don't know whether to consider it as an issue, because I solved it by removing CustomUser and placing userId into org.springframework.security.core.userdetails.User.username field.
It looks like your Jackson configuration is not correct. You need to leverage @JsonDeserialize. For example:
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
@JsonDeserialize(using = CustomUserDeserializer.class)
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonIgnoreProperties(ignoreUnknown = true)
public abstract class CustomUserMixin {
}
package com.example.security;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.MissingNode;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.io.IOException;
import java.util.Set;
/**
* @author Rob Winch
* @since 5.0
*/
public class CustomUserDeserializer extends JsonDeserializer {
@Override
public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
JsonNode jsonNode = mapper.readTree(jp);
Set<GrantedAuthority> authorities = mapper.convertValue(jsonNode.get("authorities"), new TypeReference<Set<SimpleGrantedAuthority>>() {
});
JsonNode password = readJsonNode(jsonNode, "password");
CustomUser result = new CustomUser(
readJsonNode(jsonNode, "username").asText(), password.asText(""),
readJsonNode(jsonNode, "enabled").asBoolean(), readJsonNode(jsonNode, "accountNonExpired").asBoolean(),
readJsonNode(jsonNode, "credentialsNonExpired").asBoolean(),
readJsonNode(jsonNode, "accountNonLocked").asBoolean(), authorities, readJsonNode(jsonNode, "id").asInt()
);
if(password.asText(null) == null) {
result.eraseCredentials();
}
return result;
}
private JsonNode readJsonNode(JsonNode jsonNode, String field) {
return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance();
}
}
Most helpful comment
It looks like your Jackson configuration is not correct. You need to leverage
@JsonDeserialize. For example: