Spring Security Oauth2:RedisTokenStore(二)之JSON序列化

RedisTokenStore:Json序列化

一. 前言

Spring Security Oauth2 存儲Token的方式有多種, 比如JWT、Jdbc(數據庫)、Redis等,但是對於一個大型的分佈式服務應用,Redis存儲方式應該是最佳選擇。

二. 問題

我們使用默認的Redis存儲方式,序列化到到Redis的數據是採用JDK序列化策略寫入到redis的。這樣對於程序的功能毫無影響,但是對於開發者卻很不直觀,出現問題,也不容易排查,我們能不能把它們序列化成JSON格式呢?image.png

默認序列化方式

Spring Security Oauth2 Redis序列化Token相關的數據是採用JdkSerializationStrategy,用這個序列化策略序列化出的結果正如上圖所示那樣,具體的代碼如下:

//org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore

public class RedisTokenStore implements TokenStore {

	private static final String ACCESS = "access:";
	private static final String AUTH_TO_ACCESS = "auth_to_access:";
	private static final String AUTH = "auth:";
	private static final String REFRESH_AUTH = "refresh_auth:";
	private static final String ACCESS_TO_REFRESH = "access_to_refresh:";
	private static final String REFRESH = "refresh:";
	private static final String REFRESH_TO_ACCESS = "refresh_to_access:";
	private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:";
	private static final String UNAME_TO_ACCESS = "uname_to_access:";

	private static final boolean springDataRedis_2_0 = ClassUtils.isPresent(
			"org.springframework.data.redis.connection.RedisStandaloneConfiguration",
			RedisTokenStore.class.getClassLoader());

	private final RedisConnectionFactory connectionFactory;
	private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();
	//Jdk序列方式
    private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy();
    ......省略無關代碼

Fastjson序列化策略

筆者首先想到的是自定義一個RedisTokenStoreSerializationStrategy**,**接口的實現改成Object和JSONString之間的序列化、反序列應該就好了,於是筆者使用Fastjson實現了一個序列化策略,並注入到Spring Bean容器中,如下:

public class FastjsonRedisTokenStoreSerializationStrategy implements RedisTokenStoreSerializationStrategy {

    private final static ParserConfig defaultRedisConfig = new ParserConfig();

    static {
        defaultRedisConfig.setAutoTypeSupport(true);
    }

    static {
        //設置Fastjson Json自動轉換爲Java對象
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    @Override
    public <T> T deserialize(byte[] bytes, Class<T> clazz) {
        Preconditions.checkArgument(clazz != null,
            "clazz can't be null");
        if (bytes == null || bytes.length == 0) {
            return null;
        }

        try {
            return JSON.parseObject(new String(bytes, IOUtils.UTF8), clazz, defaultRedisConfig);
        } catch (Exception ex) {
            throw new SerializationException("Could not serialize: " + ex.getMessage(), ex);
        }
    }

    @Override
    public String deserializeString(byte[] bytes) {
        if (bytes == null || bytes.length == 0) {
            return null;
        }
        return new String(bytes, IOUtils.UTF8);
    }

    @Override
    public byte[] serialize(Object object) {
        if (object == null) {
            return new byte[0];
        }

        try {
            return JSON.toJSONBytes(object, SerializerFeature.WriteClassName,
                SerializerFeature.DisableCircularReferenceDetect);
        } catch (Exception ex) {
            throw new SerializationException("Could not serialize: " + ex.getMessage(), ex);
        }
    }

    @Override
    public byte[] serialize(String data) {
        if (data == null || data.length() == 0) {
            return new byte[0];
        }

        return data.getBytes(Charset.forName("utf-8"));
    }
}
@Bean
public RedisTokenStore redisTokenStore(){
    RedisTokenStore store = new RedisTokenStore(redisConnectionFactory);
    store.setSerializationStrategy(new FastjsonRedisTokenStoreSerializationStrategy());
    return store;
}

筆者剛開始以爲這樣處理就可以了,OAuth2Authentication 這個類並沒有默認構造方法,會導致Fastjson反序列化失敗,因此針對Oauth2等無法序列化或者反序列化的類需要特殊化處理。針對DefaultOAuth2RefreshToken、OAuth2Authentication等特殊的類,我們需要定製化序列化以及反序列化策略。下面是筆者項目的配置以及序列化和反序列策略,有些類是滿足需求定製的,不在Spring Security包中。提供只是參考思路,不可照抄。配置和自定義策略如下:

public class FastjsonRedisTokenStoreSerializationStrategy implements RedisTokenStoreSerializationStrategy {

    private static ParserConfig config = new ParserConfig();

    static {
        init();
    }

    protected static void init() {
        //自定義oauth2序列化:DefaultOAuth2RefreshToken 沒有setValue方法,會導致JSON序列化爲null
        config.setAutoTypeSupport(true);
        config.putDeserializer(DefaultOAuth2RefreshToken.class, new DefaultOauth2RefreshTokenSerializer());
        config.putDeserializer(OAuth2Authentication.class, new OAuth2AuthenticationSerializer());
        config.addAccept("org.springframework.security.oauth2.provider.");
        config.addAccept("org.springframework.security.oauth2.provider.client");

        TypeUtils.addMapping("org.springframework.security.oauth2.provider.OAuth2Authentication",
            OAuth2Authentication.class);
        TypeUtils.addMapping("org.springframework.security.oauth2.provider.client.BaseClientDetails",
            BaseClientDetails.class);
    }
    ......
 }
public class OAuth2AuthenticationSerializer implements ObjectDeserializer {

    @Override
    public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
        if (type == OAuth2Authentication.class) {
            try {
                Object o = parse(parser);
                if (o == null) {
                    return null;
                } else if (o instanceof OAuth2Authentication) {
                    return (T) o;
                }

                JSONObject jsonObject = (JSONObject) o;
                OAuth2Request request = parseOAuth2Request(jsonObject);
                UsernamePasswordAuthenticationToken authentication = jsonObject
                    .getObject("userAuthentication", UsernamePasswordAuthenticationToken.class);
                return (T) new OAuth2Authentication(request, authentication);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
        return null;
    }

    private OAuth2Request parseOAuth2Request(JSONObject jsonObject) {
        JSONObject json = jsonObject.getObject("oAuth2Request", JSONObject.class);
        Map<String, String> requestParameters = json.getObject("requestParameters", Map.class);
        String clientId = json.getString("clientId");
        String grantType = json.getString("grantType");
        String redirectUri = json.getString("redirectUri");
        Boolean approved = json.getBoolean("approved");
        Set<String> responseTypes = json
            .getObject("responseTypes", new TypeReference<HashSet<String>>() {
            });
        Set<String> scope = json.getObject("scope", new TypeReference<HashSet<String>>() {
        });
        Set<String> authorities = json.getObject("authorities", new TypeReference<HashSet<String>>() {
        });
        Set<GrantedAuthority> grantedAuthorities = new HashSet<>(0);
        if (authorities != null && !authorities.isEmpty()) {
            authorities.forEach(s -> grantedAuthorities.add(new SimpleGrantedAuthority(s)));
        }
        Set<String> resourceIds = json
            .getObject("resourceIds", new TypeReference<HashSet<String>>() {
            });
        Map<String, Serializable> extensions = json
            .getObject("extensions", new TypeReference<HashMap<String, Serializable>>() {
            });

        OAuth2Request request = new OAuth2Request(requestParameters, clientId,
            grantedAuthorities, approved, scope, resourceIds, redirectUri, responseTypes, extensions);
        TokenRequest tokenRequest = new TokenRequest(requestParameters, clientId, scope, grantType);
        request.refresh(tokenRequest);
        return request;
    }


    @Override
    public int getFastMatchToken() {
        return 0;
    }

    private Object parse(DefaultJSONParser parse) {
        JSONObject object = new JSONObject(parse.lexer.isEnabled(Feature.OrderedField));
        Object parsedObject = parse.parseObject((Map) object);
        if (parsedObject instanceof JSONObject) {
            return (JSONObject) parsedObject;
        } else if (parsedObject instanceof OAuth2Authentication) {
            return parsedObject;
        } else {
            return parsedObject == null ? null : new JSONObject((Map) parsedObject);
        }
    }
}

public class DefaultOauth2RefreshTokenSerializer implements ObjectDeserializer {

    @Override
    public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
        if (type == DefaultOAuth2RefreshToken.class) {
            JSONObject jsonObject = parser.parseObject();
            String tokenId = jsonObject.getString("value");
            DefaultOAuth2RefreshToken refreshToken = new DefaultOAuth2RefreshToken(tokenId);
            return (T) refreshToken;
        }
        return null;
    }

    @Override
    public int getFastMatchToken() {
        return 0;
    }
}

Jackson序列化策略

能用Fastjson當然也可以使用Jackson,而且Spring Security也默認加了一下Jackson的相關注解,只不過Jackson與Fastjson本就功能類似,但實現完全不同的序列化工具。配置、序列化策略、以及序列化結果如下:

public class JacksonRedisTokenStoreSerializationStrategy implements RedisTokenStoreSerializationStrategy {

    protected static ObjectMapper mapper = new ObjectMapper();

    {
        SimpleModule module = new SimpleModule();
        module.addDeserializer(OAuth2Authentication.class,
            new OAuth2AuthenticationJackson2Deserializer(OAuth2Authentication.class));
        mapper.registerModule(module);
    }

    @Override
    public <T> T deserialize(byte[] bytes, Class<T> clazz) {
        Preconditions.checkArgument(clazz != null,
            "clazz can't be null");
        if (bytes == null || bytes.length == 0) {
            return null;
        }

        try {
            return mapper.readValue(new String(bytes, IOUtils.UTF8), clazz);
        } catch (Exception ex) {
            throw new SerializationException("Could not serialize: " + ex.getMessage(), ex);
        }
    }

    @Override
    public String deserializeString(byte[] bytes) {
        if (bytes == null || bytes.length == 0) {
            return null;
        }
        return new String(bytes, IOUtils.UTF8);
    }

    @Override
    public byte[] serialize(Object object) {
        if (object == null) {
            return new byte[0];
        }

        try {
            return mapper.writeValueAsBytes(object);
        } catch (Exception ex) {
            throw new SerializationException("Could not serialize: " + ex.getMessage(), ex);
        }
    }

    @Override
    public byte[] serialize(String data) {
        if (data == null || data.length() == 0) {
            return new byte[0];
        }

        return data.getBytes(Charset.forName("utf-8"));
    }
}
public class OAuth2AuthenticationJackson2Deserializer extends StdDeserializer<OAuth2Authentication> {

    protected OAuth2AuthenticationJackson2Deserializer(Class<?> vc) {
        super(vc);
    }

    private static String readString(ObjectMapper mapper, JsonNode jsonNode) {
        return readValue(mapper, jsonNode, String.class);
    }

    private static <T> T readValue(ObjectMapper mapper, JsonNode jsonNode, Class<T> clazz) {
        if (mapper == null || jsonNode == null || clazz == null) {
            return null;
        }
        try {
            return mapper.readValue(jsonNode.traverse(), clazz);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static <T> T readValue(ObjectMapper mapper, JsonNode jsonNode, TypeReference<T> type) {
        if (mapper == null || jsonNode == null || type == null) {
            return null;
        }
        try {
            return mapper.readValue(jsonNode.traverse(), type);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public OAuth2Authentication deserialize(JsonParser jp, DeserializationContext ctxt)
        throws IOException, JsonProcessingException {
        ObjectMapper mapper = (ObjectMapper) jp.getCodec();
        JsonNode jsonNode = mapper.readTree(jp);
        JsonNode requestNode = readJsonNode(jsonNode, "oauth2Request");
        JsonNode userAuthenticationNode = readJsonNode(jsonNode, "userAuthentication");

        Authentication authentication = parseAuthentication(mapper, userAuthenticationNode);
        OAuth2Request request = parseOAuth2Request(mapper, requestNode);
        return new OAuth2Authentication(request, authentication);
    }

    private Authentication parseAuthentication(ObjectMapper mapper, JsonNode json) {
        if (mapper == null || json == null) {
            return null;
        }

        Oauth2User principal = parseOAuth2User(mapper, json.get("principal"));
        Object credentials = readValue(mapper, json.get("credentials"), Object.class);
        Set<SimpleGrantedAuthority> grantedAuthorities = parseSimpleGrantedAuthorities(mapper, json.get("authorities"));

        return new UsernamePasswordAuthenticationToken(principal, credentials, grantedAuthorities);
    }

    private Oauth2User parseOAuth2User(ObjectMapper mapper, JsonNode json) {
        if (mapper == null || json == null) {
            return null;
        }

        String username = readString(mapper, json.get("username"));
        String password = readString(mapper, json.get("password"));
        String nickname = readString(mapper, json.get("nickname"));
        String phone = readString(mapper, json.get("phone"));
        String email = readString(mapper, json.get("email"));
        String avatar = readString(mapper, json.get("avatar"));

        List<Map<String, Object>> roleList = readValue(mapper, json.get("roleList"),
            new TypeReference<List<Map<String, Object>>>() {
            });
        List<Map<String, Object>> permissionList = readValue(mapper, json.get("permissionList"),
            new TypeReference<List<Map<String, Object>>>() {
            });

        Boolean accountNonExpired = readValue(mapper, json.get("accountNonExpired"), Boolean.class);
        Boolean accountNonLocked = readValue(mapper, json.get("accountNonLocked"), Boolean.class);
        Boolean credentialsNonExpired = readValue(mapper, json.get("credentialsNonExpired"), Boolean.class);
        Boolean enabled = readValue(mapper, json.get("enabled"), Boolean.class);

        Set<SimpleGrantedAuthority> grantedAuthorities = parseSimpleGrantedAuthorities(mapper, json.get("authorities"));

        return Oauth2User.builder()
            .username(username)
            .password(password)
            .nickname(nickname)
            .phone(phone)
            .email(email)
            .avatar(avatar)
            .accountNonExpired(accountNonExpired)
            .accountNonLocked(accountNonLocked)
            .credentialsNonExpired(credentialsNonExpired)
            .enabled(enabled)
            .authorities(grantedAuthorities)
            .roleList(roleList)
            .permissionList(permissionList).build();
    }

    private OAuth2Request parseOAuth2Request(ObjectMapper mapper, JsonNode json) {
        if (mapper == null || json == null) {
            return null;
        }
        HashMap<String, String> requestParameters = readValue(mapper, json.get("requestParameters"), HashMap.class);
        String clientId = readString(mapper, json.get("clientId"));
        String grantType = readString(mapper, json.get("grantType"));
        String redirectUri = readString(mapper, json.get("redirectUri"));
        Boolean approved = readValue(mapper, json.get("approved"), Boolean.class);

        Set<String> responseTypes = readValue(mapper, json.get("responseTypes"), Set.class);
        Set<String> scope = readValue(mapper, json.get("scope"), Set.class);
        Set<String> resourceIds = readValue(mapper, json.get("resourceIds"), Set.class);
        Map<String, Serializable> extensions = readValue(mapper, json.get("extensions"),
            new TypeReference<Map<String, Serializable>>() {
            });

        Set<SimpleGrantedAuthority> grantedAuthorities = parseSimpleGrantedAuthorities(mapper, json.get("authorities"));

        OAuth2Request request = new OAuth2Request(requestParameters, clientId,
            grantedAuthorities, approved, scope, resourceIds, redirectUri, responseTypes, extensions);
        TokenRequest tokenRequest = new TokenRequest(requestParameters, clientId, scope, grantType);
        request.refresh(tokenRequest);
        return request;
    }

    private Set<SimpleGrantedAuthority> parseSimpleGrantedAuthorities(ObjectMapper mapper, JsonNode json) {
        Set<LinkedHashMap<String, String>> authorities = readValue(mapper, json, Set.class);
        Set<SimpleGrantedAuthority> grantedAuthorities = new HashSet<>(0);
        if (authorities != null && !authorities.isEmpty()) {
            authorities.forEach(s -> {
                if (s != null && !s.isEmpty()) {
                    grantedAuthorities.add(new SimpleGrantedAuthority(s.get("authority")));
                }
            });
        }
        return grantedAuthorities;
    }

    private JsonNode readJsonNode(JsonNode jsonNode, String field) {
        return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance();
    }

}

序列化結果:

image.png

相關文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章