Spring-session: Issue with deserialization CustomUser

Created on 16 May 2017  路  7Comments  路  Source: spring-projects/spring-session

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

Most helpful comment

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

All 7 comments

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

Was this page helpful?
0 / 5 - 0 ratings