[alibaba/fastjson]使用 fastjson 时, spring security oauth2 获取 token 格式变化

2025-11-12 306 views
3

基于 spring boot 1.5.x 的项目,使用 webmvc + security + oauth2。 集成 fastjson 前,使用缺省的 jackson,.../oauth/token 获取的响应中 token 名称是 access_token : xxxxxxx,和 spring 文档一致。 集成 fastjson 后,.../oauth/token 获取的响应中 token 名称变成 value : xxxxxxx,且其他字段名及格式均有变化,如果服务器发生异常,甚至会把 exception stack 全部返回,与 spring 文档不一致。

如何才能使spring oauth在使用 fastjson 和 jackson 时返回的 token 格式一致呢?

回答

9

fastjson配置发出来, 错误的json和正确的也发出来。

7

# 集成 fastjson 的配置如下: @Configuration public class WebMvcConfigurer extends WebMvcConfigurerAdapter { ... /**

使用 FastJson 替换 Jackson @param converters */ @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter(); FastJsonConfig config = new FastJsonConfig(); config.setSerializerFeatures( SerializerFeature.DisableCircularReferenceDetect ); converter.setFastJsonConfig(config); converters.add(converter); log.info("FastJSON enabled"); } ... }

# /oauth/token 获取到的报文如下(其中 value 即是 access token): { "expiration": 1513752144897, "expired": false, "expiresIn": 604799, "refreshToken": { "expiration": 1515739344897, "value": "dae50004-939c-40f2-98d8-4882374e08c7" }, "scope": [ "api" ], "tokenType": "bearer", "value": "1e2c4181-01be-4aa1-8042-c2d2f74008d0" }

# 而不集成 fastjson 时,获取到的报文如下: { "access_token": "d83156d4-cec3-4aa9-b9dc-78612ce3cd92", "token_type": "bearer", "refresh_token": "37d7638a-8ee2-4a74-8194-cf9673ab1689", "expires_in": 604799, "scope": "api" }

多谢!

9

这个bug我也遇到了,目前是把FastJsonHttpMessageConverter去掉了。

3

请问这个问题解决了吗?

4

You can Autowired ObjectMapper Just like this:

@Component
public class ***AuthenticationSuccessHandler implements AuthenticationSuccessHandler {

        private final ObjectMapper objectMapper;

    @Autowired
    public **AuthenticationSuccessHandler(
            ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }
        @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
        ***
                response.getWriter().write(objectMapper.writeValueAsString(oAuth2AccessToken));
        }
}
2
我跟你碰到的问题一样 请问你最后怎么解决的
0

我也遇到了,这个问题怎么解决呢? 现在我是在每个spring boot项目的启动文件中单独设置。

1

貌似还没解决,就因为这个,换回去用 jackson 了

2

我也遇到了,目前只能把FastJsonHttpMessageConverter先去掉

5

可以考虑手动生成token流程,并在最后序列化的时候使用Jackson的ObjectMapper进行序列化并返回给客户端。(之前我的回复过于模糊,实在抱歉)

import java.io.IOException;
import java.util.Base64;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.collections4.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.TokenRequest;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

/**
 * authentication success handler.
 * 
 * @author johnniang
 *
 */
@Component
public class ***AuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    private String credentialsCharset = "UTF-8";

    private final ClientDetailsService clientDetailsService;

    private final AuthorizationServerTokenServices authorizationServerTokenServices;

    private final PasswordEncoder passwordEncode;

    private final ObjectMapper objectMapper;

    @Autowired
    public ***AuthenticationSuccessHandler(ClientDetailsService clientDetailsService,
            AuthorizationServerTokenServices authorizationServerTokenServices, PasswordEncoder passwordEncode,
            ObjectMapper objectMapper) {
        this.clientDetailsService = clientDetailsService;
        this.authorizationServerTokenServices = authorizationServerTokenServices;
        this.passwordEncode = passwordEncode;
        this.objectMapper = objectMapper;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
        logger.info("login successfully.");
        String header = request.getHeader("Authorization");

        if (header == null || !header.startsWith("Basic ")) {
            throw new UnapprovedClientAuthenticationException("There is not client information.");
        }

        String[] tokens = extractAndDecodeHeader(header, request);
        assert tokens.length == 2;

        String clientId = tokens[0];
        String clientSecret = tokens[1];

        if (logger.isDebugEnabled()) {
            logger.debug("Client Id     : {}", clientId);
            logger.debug("Client Secret : {}", clientSecret);
        }

        ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);

        if (clientDetails == null) {
            throw new UnapprovedClientAuthenticationException("Client information was not found: " + clientId);
        }
        if (!passwordEncode.matches(clientSecret, clientDetails.getClientSecret())) {
            throw new UnapprovedClientAuthenticationException("Client secret was not matched: " + clientId);
        }

        @SuppressWarnings("unchecked")
        TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_SORTED_MAP, clientId, clientDetails.getScope(),
                "custom");

        // create a OAuth2Request
        OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);

        // create a OAuth2Authentication
        OAuth2Authentication oAuth2Authorization = new OAuth2Authentication(oAuth2Request, authentication);

        // produce a token
        OAuth2AccessToken oAuth2AccessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authorization);

        response.setStatus(HttpStatus.OK.value());
        response.setContentType("application/json;charset=UTF-8");

        // write date as timestamp
        objectMapper.enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        response.getWriter().write(objectMapper.writeValueAsString(oAuth2AccessToken));
    }

    /**
     * Decodes the header into a username and password.
     *
     * @throws BadCredentialsException
     *             if the Basic header is not present or is not valid Base64
     */
    private String[] extractAndDecodeHeader(String header, HttpServletRequest request) throws IOException {

        byte[] base64Token = header.substring(6).getBytes("UTF-8");
        byte[] decoded;
        try {
            decoded = Base64.getDecoder().decode(base64Token);
        } catch (IllegalArgumentException e) {
            throw new BadCredentialsException("Failed to decode basic authentication token");
        }

        String token = new String(decoded, credentialsCharset);

        int delim = token.indexOf(":");

        if (delim == -1) {
            throw new BadCredentialsException("Invalid basic authentication token");
        }
        return new String[] { token.substring(0, delim), token.substring(delim + 1) };
    }
}
/**
 * OAuth2 resource server config
 * 
 * @author johnniang
 *
 */
@Component
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {

        private final AuthenticationSuccessHandler authenticationSuccessHandler;
        public OAuth2ResourceServerConfig(
            AuthenticationSuccessHandler authenticationSuccessHandler)
        this.authenticationSuccessHandler = authenticationSuccessHandler;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http //
                .formLogin() //
                .loginProcessingUrl("/login") //
                .successHandler(authenticationSuccessHandler) //
                .permitAll() //
                .and() //
                .logout() //
                                ***
        }
}

希望能够解决你们的问题

1

我也碰到这类问题,最后通过fastjson 自定了一个SerializeFilter进行处理DefaultOAuth2AccessToken的序列化

public class CustomSerializeFilter extends BeforeFilter implements PropertyPreFilter {

    @Override
    public void writeBefore(Object object) {
        /**
         * DefaultOAuth2AccessToken 默认格式
         * {
         *     "access_token": "763d17ee-7847-4d8b-b3e8-207110d094c9",
         *     "token_type": "bearer",
         *     "refresh_token": "68d85c17-e817-4582-95dd-169dce4a3264",
         *     "expires_in": 7199,
         *     "scope": "read write"
         * }
         */
        if (object instanceof DefaultOAuth2AccessToken) {
            DefaultOAuth2AccessToken accessToken = (DefaultOAuth2AccessToken) object;
            writeKeyValue("access_token", accessToken.getValue());
            writeKeyValue("token_type", accessToken.getTokenType());
            writeKeyValue("refresh_token", accessToken.getRefreshToken().getValue());
            writeKeyValue("expires_in", accessToken.getExpiresIn());
            writeKeyValue("scope", String.join(" ", accessToken.getScope()));
        }
    }

    @Override
    public boolean apply(JSONSerializer serializer, Object object, String name) {
        // DefaultOAuth2AccessToken、DefaultExpiringOAuth2RefreshToken
        // 原先的所有属性都不进行序列化
        return !(object instanceof DefaultOAuth2AccessToken
                || object instanceof DefaultExpiringOAuth2RefreshToken);
    }
}

FastJsonHttpMessageConverter中加入自定义的SerializeFilter 后面又发现FormOAuth2AccessTokenMessageConverter的实现,可以把这个加进去测试一下

1

这个就太坑了 如果这样改 后续岂不是还有很多其他错误可能发生?有一套整体的解决方案不

4

这个就太坑了 如果这样改 后续岂不是还有很多其他错误可能发生?有一套整体的解决方案不

已经提供了解决方案,将FormOAuth2AccessTokenMessageConverter这个加入HttpMessageConverter中即可

7

这个就太坑了 如果这样改 后续岂不是还有很多其他错误可能发生?有一套整体的解决方案不

已经提供了解决方案,将FormOAuth2AccessTokenMessageConverter这个加入HttpMessageConverter中即可

你好,可以详细讲一下。如何把这个FormOAuth2AccessTokenMessageConverter 加到 HttpMessageConverter中的方案吗?非常感谢

2

这个就太坑了 如果这样改 后续岂不是还有很多其他错误可能发生?有一套整体的解决方案不

已经提供了解决方案,将FormOAuth2AccessTokenMessageConverter这个加入HttpMessageConverter中即可

你好,可以详细讲一下。如何把这个FormOAuth2AccessTokenMessageConverter 加到 HttpMessageConverter中的方案吗?非常感谢

不好意思,当时我看错了,这段时间比较忙没看这个问题,你需要自行写个HttpMessageConverter来转换OAuth2AccessToken,fastjson本身也意识到这个问题,但是FormOAuth2AccessTokenMessageConverter这个类的writeInternal方法没有进行实现,你需要自行实现转换

public class OAuth2AccessTokenMessageConverter extends AbstractHttpMessageConverter<OAuth2AccessToken> {

    private final FastJsonHttpMessageConverter delegateMessageConverter;

    public OAuth2AccessTokenMessageConverter() {
        super(MediaType.APPLICATION_JSON);
        this.delegateMessageConverter = new FastJsonHttpMessageConverter();
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return OAuth2AccessToken.class.isAssignableFrom(clazz);
    }

    @Override
    protected OAuth2AccessToken readInternal(Class<? extends OAuth2AccessToken> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        throw new UnsupportedOperationException(
                "This converter is only used for converting from externally aqcuired form data");
    }

    @Override
    protected void writeInternal(OAuth2AccessToken accessToken, HttpOutputMessage outputMessage) throws IOException,
            HttpMessageNotWritableException {
        Map<String, Object> data = new HashMap<>(8);
        data.put(OAuth2AccessToken.ACCESS_TOKEN, accessToken.getValue());
        data.put(OAuth2AccessToken.TOKEN_TYPE, accessToken.getTokenType());
        data.put(OAuth2AccessToken.EXPIRES_IN, accessToken.getExpiresIn());
        data.put(OAuth2AccessToken.SCOPE, String.join(" ", accessToken.getScope()));
        OAuth2RefreshToken refreshToken = accessToken.getRefreshToken();
        if (Objects.nonNull(refreshToken)) {
            data.put(OAuth2AccessToken.REFRESH_TOKEN, refreshToken.getValue());
        }
        delegateMessageConverter.write(data, MediaType.APPLICATION_JSON, outputMessage);
    }

}

加入MessageConverters

@Configuration
public class Oauth2WebMvcConfigurer implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
       converters.add(0, new FastJsonHttpMessageConverter());
       converters.add(0, new OAuth2AccessTokenMessageConverter());
    }
}

你可以参考OAuth2AccessToken这个序列化类OAuth2AccessTokenJackson2Serializer进行编写相应的代码

8

这个就太坑了 如果这样改 后续岂不是还有很多其他错误可能发生?有一套整体的解决方案不

已经提供了解决方案,将FormOAuth2AccessTokenMessageConverter这个加入HttpMessageConverter中即可

你好,可以详细讲一下。如何把这个FormOAuth2AccessTokenMessageConverter 加到 HttpMessageConverter中的方案吗?非常感谢

不好意思,当时我看错了,这段时间比较忙没看这个问题,你需要自行写个HttpMessageConverter来转换OAuth2AccessToken,fastjson本身也意识到这个问题,但是FormOAuth2AccessTokenMessageConverter这个类的writeInternal方法没有进行实现,你需要自行实现转换

public class OAuth2AccessTokenMessageConverter extends AbstractHttpMessageConverter<OAuth2AccessToken> {

    private final FastJsonHttpMessageConverter delegateMessageConverter;

    public OAuth2AccessTokenMessageConverter() {
        super(MediaType.APPLICATION_JSON);
        this.delegateMessageConverter = new FastJsonHttpMessageConverter();
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return OAuth2AccessToken.class.isAssignableFrom(clazz);
    }

    @Override
    protected OAuth2AccessToken readInternal(Class<? extends OAuth2AccessToken> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        throw new UnsupportedOperationException(
                "This converter is only used for converting from externally aqcuired form data");
    }

    @Override
    protected void writeInternal(OAuth2AccessToken accessToken, HttpOutputMessage outputMessage) throws IOException,
            HttpMessageNotWritableException {
        Map<String, Object> data = new HashMap<>(8);
        data.put(OAuth2AccessToken.ACCESS_TOKEN, accessToken.getValue());
        data.put(OAuth2AccessToken.TOKEN_TYPE, accessToken.getTokenType());
        data.put(OAuth2AccessToken.EXPIRES_IN, accessToken.getExpiresIn());
        data.put(OAuth2AccessToken.SCOPE, String.join(" ", accessToken.getScope()));
        OAuth2RefreshToken refreshToken = accessToken.getRefreshToken();
        if (Objects.nonNull(refreshToken)) {
            data.put(OAuth2AccessToken.REFRESH_TOKEN, refreshToken.getValue());
        }
        delegateMessageConverter.write(data, MediaType.APPLICATION_JSON, outputMessage);
    }

}

加入MessageConverters

@Configuration
public class Oauth2WebMvcConfigurer implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
       converters.add(0, new FastJsonHttpMessageConverter());
       converters.add(0, new OAuth2AccessTokenMessageConverter());
    }
}

你可以参考OAuth2AccessToken这个序列化类OAuth2AccessTokenJackson2Serializer进行编写相应的代码

扩展下,上面问题回答针对的只是OAuth2AccessToken,如果我们的项目还有其他的http请求需要处理,则需要修改上面的转换器,只处理token的回执保持兼容,其他请求的回执仍然保持原样

public class OAuth2AccessTokenMessageConverter extends AbstractHttpMessageConverter<Object> { //这里替换成Object

    @Override
    protected Object readInternal(Class<? extends OAuth2AccessToken> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        //需要写自己的转换规则
    }

    @Override
    protected void writeInternal(Object t, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        String jsonString = JSON.toJSONString(t);
        if (t instanceof DefaultOAuth2AccessToken) {
            DefaultOAuth2AccessToken oAuth2AccessToken = JSON.parseObject(jsonString, DefaultOAuth2AccessToken.class);
            Map<String, Object> data = new HashMap<>(8);
            data.put(OAuth2AccessToken.ACCESS_TOKEN, oAuth2AccessToken.getValue());
            data.put(OAuth2AccessToken.TOKEN_TYPE, oAuth2AccessToken.getTokenType());
            data.put(OAuth2AccessToken.EXPIRES_IN, oAuth2AccessToken.getExpiresIn());
            data.put(OAuth2AccessToken.SCOPE, String.join(" ", oAuth2AccessToken.getScope()));

            //获取refreshToken
            JSONObject jsonObject = JSON.parseObject(jsonString);
            String refreshTokenString = jsonObject.getString("refreshToken");
            if (Objects.nonNull(refreshTokenString)) {
                data.put(OAuth2AccessToken.REFRESH_TOKEN, JSONObject.parseObject(refreshTokenString).getString("value"));
            }
            jsonString = JSON.toJSONString(data);
        }
        StreamUtils.copy(jsonString, DEFAULT_CHARSET, outputMessage.getBody());
    }

}
1

这个就太坑了 如果这样改 后续岂不是还有很多其他错误可能发生?有一套整体的解决方案不

已经提供了解决方案,将FormOAuth2AccessTokenMessageConverter这个加入HttpMessageConverter中即可

你好,可以详细讲一下。如何把这个FormOAuth2AccessTokenMessageConverter 加到 HttpMessageConverter中的方案吗?非常感谢

不好意思,当时我看错了,这段时间比较忙没看这个问题,你需要自行写个HttpMessageConverter来转换OAuth2AccessToken,fastjson本身也意识到这个问题,但是FormOAuth2AccessTokenMessageConverter这个类的writeInternal方法没有进行实现,你需要自行实现转换

public class OAuth2AccessTokenMessageConverter extends AbstractHttpMessageConverter<OAuth2AccessToken> {

    private final FastJsonHttpMessageConverter delegateMessageConverter;

    public OAuth2AccessTokenMessageConverter() {
        super(MediaType.APPLICATION_JSON);
        this.delegateMessageConverter = new FastJsonHttpMessageConverter();
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return OAuth2AccessToken.class.isAssignableFrom(clazz);
    }

    @Override
    protected OAuth2AccessToken readInternal(Class<? extends OAuth2AccessToken> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        throw new UnsupportedOperationException(
                "This converter is only used for converting from externally aqcuired form data");
    }

    @Override
    protected void writeInternal(OAuth2AccessToken accessToken, HttpOutputMessage outputMessage) throws IOException,
            HttpMessageNotWritableException {
        Map<String, Object> data = new HashMap<>(8);
        data.put(OAuth2AccessToken.ACCESS_TOKEN, accessToken.getValue());
        data.put(OAuth2AccessToken.TOKEN_TYPE, accessToken.getTokenType());
        data.put(OAuth2AccessToken.EXPIRES_IN, accessToken.getExpiresIn());
        data.put(OAuth2AccessToken.SCOPE, String.join(" ", accessToken.getScope()));
        OAuth2RefreshToken refreshToken = accessToken.getRefreshToken();
        if (Objects.nonNull(refreshToken)) {
            data.put(OAuth2AccessToken.REFRESH_TOKEN, refreshToken.getValue());
        }
        delegateMessageConverter.write(data, MediaType.APPLICATION_JSON, outputMessage);
    }

}

加入MessageConverters

@Configuration
public class Oauth2WebMvcConfigurer implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
       converters.add(0, new FastJsonHttpMessageConverter());
       converters.add(0, new OAuth2AccessTokenMessageConverter());
    }
}

你可以参考OAuth2AccessToken这个序列化类OAuth2AccessTokenJackson2Serializer进行编写相应的代码

扩展下,上面问题回答针对的只是OAuth2AccessToken,如果我们的项目还有其他的http请求需要处理,则需要修改上面的转换器,只处理token的回执保持兼容,其他请求的回执仍然保持原样

public class OAuth2AccessTokenMessageConverter extends AbstractHttpMessageConverter<Object> { //这里替换成Object

    @Override
    protected Object readInternal(Class<? extends OAuth2AccessToken> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        //需要写自己的转换规则
    }

    @Override
    protected void writeInternal(Object t, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        String jsonString = JSON.toJSONString(t);
        if (t instanceof DefaultOAuth2AccessToken) {
            DefaultOAuth2AccessToken oAuth2AccessToken = JSON.parseObject(jsonString, DefaultOAuth2AccessToken.class);
            Map<String, Object> data = new HashMap<>(8);
            data.put(OAuth2AccessToken.ACCESS_TOKEN, oAuth2AccessToken.getValue());
            data.put(OAuth2AccessToken.TOKEN_TYPE, oAuth2AccessToken.getTokenType());
            data.put(OAuth2AccessToken.EXPIRES_IN, oAuth2AccessToken.getExpiresIn());
            data.put(OAuth2AccessToken.SCOPE, String.join(" ", oAuth2AccessToken.getScope()));

            //获取refreshToken
            JSONObject jsonObject = JSON.parseObject(jsonString);
            String refreshTokenString = jsonObject.getString("refreshToken");
            if (Objects.nonNull(refreshTokenString)) {
                data.put(OAuth2AccessToken.REFRESH_TOKEN, JSONObject.parseObject(refreshTokenString).getString("value"));
            }
            jsonString = JSON.toJSONString(data);
        }
        StreamUtils.copy(jsonString, DEFAULT_CHARSET, outputMessage.getBody());
    }

}

为啥要修改转换器,再写一个转换器就行了,你这个才要修改加上判断逻辑