spring-projects / spring-security

Spring Security
http://spring.io/projects/spring-security
Apache License 2.0
8.78k stars 5.89k forks source link

Spring Session integration do not supports primitive type as the principal #6571

Open coldcutter opened 5 years ago

coldcutter commented 5 years ago

Summary

In a Spring Boot application using both Spring Security and Spring Session (with Redis and GenericJackson2JsonRedisSerializer), a UsernamePasswordAuthenticationToken with java.lang.Long(such as userId) as the principal can be serialized, but deserialized value is empty

Actual Behavior

deserialize value is empty string

Expected Behavior

the Long value as i stored

Configuration

@Configuration
public class SessionConfig implements BeanClassLoaderAware {

    private ClassLoader classLoader;

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer(objectMapper());
    }

    /**
     * Customized {@link ObjectMapper} to add mix-in for class that doesn't have default constructors
     *
     * {@link org.springframework.security.web.savedrequest.DefaultSavedRequest}
     *
     * @return
     */
    private ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModules(SecurityJackson2Modules.getModules(classLoader));
        return mapper;
    }

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }
}

Version

Spring Boot: 2.1.0.RELEASE Spring Security: 5.1.1.RELEASE Spring Session: 2.1.1.RELEASE

Sample

private void test() {
    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(1L, null, null);
    RedisOperationsSessionRepository sessionRepository = beanFactory.getBean(RedisOperationsSessionRepository.class);
    BoundHashOperations<Object, Object, Object> operations = sessionRepository.getSessionRedisOperations().boundHashOps("test");
    operations.put("sessionAttr:token", token);

    UsernamePasswordAuthenticationToken stored = (UsernamePasswordAuthenticationToken) operations.get("sessionAttr:token");
    System.out.println(stored);
}

The content stored in redis:

sessionAttr:token
{"@class":"org.springframework.security.authentication.UsernamePasswordAuthenticationToken","authorities":["java.util.Collections$EmptyList",[]],"details":null,"authenticated":true,"principal":["java.lang.Long",1],"credentials":null}

the deserialize object printed is:

org.springframework.security.authentication.UsernamePasswordAuthenticationToken@ffffffc4: Principal: ; Credentials: [PROTECTED]; Authenticated: true; Details: null; Not granted any authorities

the magic is in UsernamePasswordAuthenticationTokenDeserializer's deserialize method:

@Override
public UsernamePasswordAuthenticationToken deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
    UsernamePasswordAuthenticationToken token = null;
    ObjectMapper mapper = (ObjectMapper) jp.getCodec();
    JsonNode jsonNode = mapper.readTree(jp);
    Boolean authenticated = readJsonNode(jsonNode, "authenticated").asBoolean();
    JsonNode principalNode = readJsonNode(jsonNode, "principal");
    Object principal = null;
    if (principalNode.isObject()) {
        principal = mapper.readValue(principalNode.traverse(mapper), Object.class);
    } else {
        principal = principalNode.asText();
    }
    ...
}

because the principal serialized is an array, the principalNode is a Jackson's ArrayNode, the asText() method return "".

coldcutter commented 5 years ago

Now i am using GenericFastJsonRedisSerializer, it seems work well..

but i wonder whether this is a bug of GenericJackson2JsonRedisSerializer, if the field type is Object and i put primitive type in it, the jackson will serialize it to an array???

buzzerrookie commented 5 years ago

I think you can replace spring security's default UsernamePasswordAuthenticationTokenDeserializer with your own UsernamePasswordAuthenticationTokenDeserializer. No matter what type principal is, just deserialize it as a Object as follows:

Object principal = mapper.readValue(principalNode.traverse(mapper), Object.class);

but i wonder whether this is a bug of GenericJackson2JsonRedisSerializer, if the field type is Object and i put primitive type in it, the jackson will serialize it to an array???

The javadoc of AsPropertyTypeSerializer says:

Type serializer that preferably embeds type information as an additional JSON Object property, if possible (when resulting serialization would use JSON Object). If this is not possible (for JSON Arrays, scalars), uses a JSON Array wrapper (similar to how JsonTypeInfo.As.WRAPPER_ARRAY always works) as a fallback.

When debugging, I find jackson treats Long as a scalar, so a Long field is serialized as the following form:

"principal": [
    "java.lang.Long",
    1
],