StackoverflowError Spring Security Oauth clientDetailsService

    科技2022-07-12  116

    StackoverflowError Spring Security Oauth clientDetailsService

    背景

    在使用 Spring security oauth 的认证服务器时,出现了 StackoverflowError 错误

    错误信息

    如下

    java.lang.StackOverflowError: null at java.lang.ReflectiveOperationException.<init>(Unknown Source) ~[na:1.8.0_45] at java.lang.reflect.InvocationTargetException.<init>(Unknown Source) ~[na:1.8.0_45] at sun.reflect.GeneratedMethodAccessor34.invoke(Unknown Source) ~[na:na] at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) ~[na:1.8.0_45] at java.lang.reflect.Method.invoke(Unknown Source) ~[na:1.8.0_45] at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:302) ~[spring-aop-4.2.2.RELEASE.jar:4.2.2.RELEASE] at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:201) ~[spring-aop-4.2.2.RELEASE.jar:4.2.2.RELEASE] at com.sun.proxy.$Proxy67.authenticate(Unknown Source) ~[na:na] at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:192) ~[spring-security-core-4.0.3.RELEASE.jar:4.0.3.RELEASE] at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$AuthenticationManagerDelegator.authenticate(WebSecurityConfigurerAdapter.java:436) ~[spring-security-config-3.2.8.RELEASE.jar:3.2.8.RELEASE] at sun.reflect.GeneratedMethodAccessor34.invoke(Unknown Source) ~[na:na] at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) ~[na:1.8.0_45] at java.lang.reflect.Method.invoke(Unknown Source) ~[na:1.8.0_45] at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:302) ~[spring-aop-4.2.2.RELEASE.jar:4.2.2.RELEASE] at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:201) ~[spring-aop-4.2.2.RELEASE.jar:4.2.2.RELEASE] at com.sun.proxy.$Proxy67.authenticate(Unknown Source) ~[na:na] at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:192) ~[spring-security-core-4.0.3.RELEASE.jar:4.0.3.RELEASE]

    在网上找了一些回答没有指到点子上的,撸其源码,发现其根本原因。

    根本原因

    是 代理对象代理了自身 导致的。故在调用目标对象的方法时,经过代理对象,代理对象增强后调用目标对象方法,但此时目标对象是自身,故进行递归调用,无限调用最终导致堆内存溢出。

    Bean 注入时注入的是代理对象,为这个代理对象设置真实对象时,设置成了从 spring 容器中获取的代理对象,即其本身,导致代理对象代理自身,产生无限循环引用bug,从而导致StackoverflowError 。

    Spring Security Oauth 对 clientDetailsService 做了什么手脚?

    答案在 org.springframework.security.oauth2.config.annotation.configuration.ClientDetailsServiceConfiguration 类中,附这个类的源码:

    @Configuration public class ClientDetailsServiceConfiguration { @SuppressWarnings("rawtypes") private ClientDetailsServiceConfigurer configurer = new ClientDetailsServiceConfigurer(new ClientDetailsServiceBuilder()); @Bean public ClientDetailsServiceConfigurer clientDetailsServiceConfigurer() { return configurer; } @Bean @Lazy @Scope(proxyMode=ScopedProxyMode.INTERFACES) // !!!!注意这里 !!!!! public ClientDetailsService clientDetailsService() throws Exception { return configurer.and().build(); } }

    发现在注入Bean时,特殊之处就在于多了一个 @Scope(proxyMode=ScopedProxyMode.INTERFACES) 注解。

    @Scope 注解什么用?

    简单来说,该注解主要是加在非单例的 Bean 上,因为该Bean可能注入到单例的bean中,spring 是通过为该bean创建代理对象来实现的,每次访问这个接口时,访问代理对象,由对应的请求/session等上下文决定这个bean用哪个。

    错误是如何产生的

    在配置 AuthorizationServerConfigurerAdapter 时,使用

    @Configuration public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { // 根据刚才看的源码,这里获取的是Spring 创建的代理对象,必须为 ClientDetailsService 接口而非实现类才会出错 @Autowired private ClientDetailsService clientDetailsService; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // 此时把 clientDetailsService 注册到了Spring的代理对象上,由于 clientDetailsService 其实就是代理对象本身,故将会导致死循环 clients.withClientDetails(clientDetailsService); } }

    解决方法

    代码可以在这看 https://github.com/ChinaLym/shoulder-framework

    1. 直接注入实现类,而非接口,获取真实对象

    改造举例:

    @Configuration public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { // 设置为 JdbcClientDetailsService 将不会走代理机制 @Autowired private JdbcClientDetailsService clientDetailsService; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(clientDetailsService); } }

    2. 使用自己的 ClientDetailsService 而不是 Spring 提供的代理对象

    改造举例1:

    @Configuration public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { // 通过 @Querlify 来强制使用自己创建的 clientDetailsService @Autowired @Querlify("myClientDetailsService") private ClientDetailsService clientDetailsService; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(clientDetailsService); } @Bean @Querlify("myClientDetailsService") public ClientDetailsService myClientDetailsService() throws Exception { return xxxx; } }

    改造举例2:

    @Configuration public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { @Autowired private ClientDetailsService clientDetailsService; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(clientDetailsService); } @Bean @Primary // 通过该注解,指定 ClientDetailsService 接口对应的 bean 就是真实的 bean public ClientDetailsService myClientDetailsService() throws Exception { return xxxx; } }

    相关回答:

    https://stackoverflow.com/questions/31798631/stackoverflowerror-in-spring-oauth2-with-custom-clientdetailsservicehttps://www.javaroad.cn/questions/310306
    Processed: 0.012, SQL: 8