Open RenaudDenis opened 6 years ago
By the way, the path toward this configuration is:
@EnableOAuth2Client
=> org.springframework.security.oauth2.config.annotation.web.configuration.OAuth2ClientConfiguration
=> org.springframework.security.oauth2.config.annotation.web.configuration.OAuth2ClientConfiguration.OAuth2ClientContextConfiguration
Did you ever find a solution to this? I'm also using spring-session-data-redis along with spring-security-oauth2, and I'm getting the same exception.
Yes, same problem here
SpringBoot 2.4.5:
First we had a serialization issue which got fixed with:
@Bean
public FilterRegistrationBean<RequestContextFilter> requestContextFilter() {
FilterRegistrationBean<RequestContextFilter> registration = new FilterRegistrationBean<>(new RequestContextFilter());
registration.setOrder(SessionRepositoryFilter.DEFAULT_ORDER - 1);
return registration;
}
Then we had a deserialization issue:
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Cannot construct instance of `jdk.proxy2.$Proxy399` (no Creators, like default constructor, exist): no default constructor found
at [Source: (byte[])"{"@class":"org.springframework.security.oauth2.client.DefaultOAuth2ClientContext","accessToken":null,"accessTokenRequest":{"@class":"jdk.proxy2.$Proxy399"}}"; line: 1, column: 155] (through reference chain: org.springframework.security.oauth2.client.DefaultOAuth2ClientContext["accessTokenRequest"]); nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `jdk.proxy2.$Proxy399` (no Creators, like default constructor, exist): no default constructor found
Workaround: custom typed Jackson Deserializer. e.g.:
public class DefaultOAuth2ClientContextDeserializer extends JsonDeserializer<DefaultOAuth2ClientContext> {
@Override
public DefaultOAuth2ClientContext deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
JsonNode jsonNode = mapper.readTree(jp);
OAuth2AccessToken accessToken = mapper.convertValue(readJsonNode(jsonNode, "accessToken"), new TypeReference<>() {
});
DefaultOAuth2ClientContext context = new DefaultOAuth2ClientContext(readTokenRequest(jsonNode, mapper));
if (accessToken != null) {
context.setAccessToken(accessToken);
}
return context;
}
private AccessTokenRequest readTokenRequest(JsonNode jsonNode, ObjectMapper mapper) {
ObjectNode accessTokenRequestNode = (ObjectNode) readJsonNode(jsonNode, "accessTokenRequest");
String typeField = JsonTypeInfo.Id.CLASS.getDefaultPropertyName();
if (accessTokenRequestNode.has(typeField)) {
String type = accessTokenRequestNode.asText(typeField);
try {
Class<?> clazz = Class.forName(type);
if (!AccessTokenRequest.class.isAssignableFrom(clazz)) {
throw new ClassNotFoundException();
}
} catch (ClassNotFoundException ignored) {
accessTokenRequestNode.put(typeField, DefaultAccessTokenRequest.class.getName());
}
return (AccessTokenRequest) mapper.convertValue(accessTokenRequestNode, Object.class);
} else {
return null;
}
}
private JsonNode readJsonNode(JsonNode jsonNode, String field) {
return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance();
}
}
^ It didn't work for us in the end - our logic relies on SessionRepositoryFilter
being before RequestContextFilter
.
SessionRepositoryFilter relies on RequestContextHolder attributes, but RequestContextFilter resets them right before SessionRepositoryFilter tries to save current session. So I have added two more filters just to keep it simpler. Workaround recreates in reverse chain RequestContextHolder attributes in RecreateRequestContextFilter between RequestContextFilter and SessionRepositoryFilter filters and resets them in EvictRequestContextFilter after all.
public class EvictRequestContextFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
doFilter(request, response, filterChain);
} finally {
resetContextHolders();
if (logger.isTraceEnabled()) {
logger.trace("Cleared recreated thread-bound request context: " + request);
}
}
}
private void resetContextHolders() {
LocaleContextHolder.resetLocaleContext();
RequestContextHolder.resetRequestAttributes();
}
}
public class RecreateRequestContextFilter extends OncePerRequestFilter {
private boolean threadContextInheritable = false;
public RecreateRequestContextFilter() {
}
public RecreateRequestContextFilter(boolean threadContextInheritable) {
this.threadContextInheritable = threadContextInheritable;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} finally {
ServletRequestAttributes attributes = new ServletRequestAttributes(request, response);
initContextHolders(request, attributes);
}
}
private void initContextHolders(HttpServletRequest request, ServletRequestAttributes requestAttributes) {
LocaleContextHolder.setLocale(request.getLocale(), this.threadContextInheritable);
RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
if (logger.isTraceEnabled()) {
logger.trace("Recreate and bound request context to thread: " + request);
}
}
}
Filters should be registered in following order EvictRequestContextFilter -> SessionRepositoryFilter -> RecreateRequestContextFilter -> RequestContextFilter
@Bean
public FilterRegistrationBean<EvictRequestContextFilter> evictRequestContextFilter() {
FilterRegistrationBean<EvictRequestContextFilter> registration = new FilterRegistrationBean<>(new EvictRequestContextFilter());
registration.setOrder(SessionRepositoryFilter.DEFAULT_ORDER - 1);
return registration;
}
@Bean
public FilterRegistrationBean<RecreateRequestContextFilter> recreateRequestContextFilter() {
FilterRegistrationBean<RecreateRequestContextFilter> registration = new FilterRegistrationBean<>(new RecreateRequestContextFilter());
registration.setOrder(SessionRepositoryFilter.DEFAULT_ORDER + 1);
return registration;
}
@Bean
public FilterRegistrationBean<RequestContextFilter> requestContextFilter() {
FilterRegistrationBean<RequestContextFilter> registration = new FilterRegistrationBean<>(new RequestContextFilter());
registration.setOrder(SessionRepositoryFilter.DEFAULT_ORDER + 2);
return registration;
}
Same problem here. I haven't tried the workaround yet, just wanted to lend my "ping" to this thread.
Hi,
I'm trying to combine OAuth2 authentication together with a global Redis session. I'm facing an issue and I'd like to know what could be done to avoid it.
Problem seems to come from the OAuth2ClientContext:
As you can see, the oauth2ClientContext bean is session-scoped, so it's serialized in Redis.
When the session is deserialized (in another microservice), there is an attempt at deserializing the
DefaultOAuth2ClientContext
. However, the spring-security-oauth dependency is not present in that microservice (and in my opinion, it should not, since that service doesn't care about authentication).See the stacktrace:
What's your advice in order to avoid that problem, while keeping the microservices dependencies clean?
Thx