SpringCloud整合SpringSecurity OAuth泪目之作

前言

在看着篇文章前,首先需要一定的知识储备,SpringCloud这一块我就不多说了,这里着重讲解SpringSecurity OAuth相关的,那么必须具备SpringSecurity和SpringSecurity OAuth相关知识储备,不然一路上会走很多坑!这篇文章是以SpringBoot2.X为基本版本整合的,所以SpringSecurity Oauth的版本也必须与SpringBoot2.X相适配,因为这里就光版本适配就有很多坑,因为SpringSecurity在1.X和2.X的一些默认配置是不一样的,导致我们写起来可能有点麻烦,但是我相信看完我这边文章应该能解决不少潜在的问题!如版本适配,如版本变化的相关配置变化解决方案!

环境准备

这里关于微服务环境搭建不会过涉及微服务组件,主要是围绕SpringSecurity OAuth相关的
1.SpringCloud父工程依赖搭建

	<!--服版本设定为2.3.0-->
	<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

 	<!--子模块-->
    <modules>
        <module>common</module><!--公共模块-->
        <module>gateway</module><!--网关-->
        <module>auth-server</module><!--授权模块-->
        <module>demo1</module>
        <module>demo2</module>
    </modules>

	
	<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.1.0.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${
     spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

这里需要注意,好多朋友在整合的时候会遇到如下报错解决方案

Exception in thread "main" java.lang.AbstractMethodError: org.springframework.boot.context.config.ConfigFileApplicationListener.supportsSourceType(Ljava/lang/Class;)Z

2.准备认证模块的环境
这里提前说明一下,在单体项目或者是分布式情况下可以直接使用下面依赖

  		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
        </dependency>

但是我这里整合的是微服务,那么就不能这么使用依赖,而是要使用微服务相关的

 		<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        </dependency>

3.开启认证服务器注解
在认证模块中创建认证服务配置类

@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfig  {
     
 
}

4.启动服务器
然后按照常规流程使用授权码模式或者密码模式是能直接得到授权的,但是你会碰到下面的问题

http://192.168.0.99:3332/oauth/authorize?response_type=code&client_id=0e74df97-7083-4324-97a2-8a8791912de5&redirect_uri=http://live.xxx.com/live/pay/getCode.html&scope=all

界面报错
SpringCloud整合SpringSecurity OAuth泪目之作_第1张图片
控制台输出
SpringCloud整合SpringSecurity OAuth泪目之作_第2张图片

org.springframework.security.authentication.InsufficientAuthenticationException: User must be authenticated with Spring Security before authorization can be completed.
	at org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.authorize(AuthorizationEndpoint.java:143) ~[spring-security-oauth2-2.3.4.RELEASE.jar:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_45]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_45]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_45]
	at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_45]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:879) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:634) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:741) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.35.jar:9.0.35]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:209) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
	at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:178) ~[spring-security-web-5.3.2.RELEASE.jar:5.3.2.RELEASE]
	at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:358) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:271) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:93) ~[spring-boot-actuator-2.3.0.RELEASE.jar:2.3.0.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.6.RELEASE.jar:5.2.6.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) [tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139) [tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) [tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) [tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373) [tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) [tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590) [tomcat-embed-core-9.0.35.jar:9.0.35]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.35.jar:9.0.35]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_45]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_45]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.35.jar:9.0.35]
	at java.lang.Thread.run(Thread.java:745) [na:1.8.0_45]

不要慌!这个意思是然我们走完整了认证流程,这里可能有朋友会懵逼了,感觉之前使用SpringSecurity OAuth整合的好好的,这里就是一个版本改变的坑,低版本的确实访问能到登录界面,可能是默认登录也,或者Basic登录,输入账号密码后进入授权界面,但是这里SpringBoot2.X适配的SpringSecurity 版本就不行了,高版本默认不在开启基本登录了,也就是高版本不在默认提供表单和Basic登录,注意这里是默认不在提供,而不是没有表单登录和Basic登录,这里我们自己开启即可!

5.开启基本登录
在认证模块中编写MySecurityConfig实现SpringSecurity核心配置

@Configuration
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
     
   
}

6.重启访问

http://192.168.0.99:3332/oauth/authorize?response_type=code&client_id=0e74df97-7083-4324-97a2-8a8791912de5&redirect_uri=http://live.xxx.com/live/pay/getCode.html&scope=all

诶,发现真的进来了,但是,但是,但是你懂的,胃口拉满
SpringCloud整合SpringSecurity OAuth泪目之作_第3张图片
SpringCloud整合SpringSecurity OAuth泪目之作_第4张图片
用户名为user密码为19e54fab-afa5-4da0-afff-44d5f187d2c8
点击登录报错,哈哈
SpringCloud整合SpringSecurity OAuth泪目之作_第5张图片

Encoded password does not look like BCrypt

不要慌,这里是告诉我们编码又问题!但是我们在SpringBoot1.X的版本压根不用管这些,一个注解搞定全部,害,年轻人慢慢来,万事尽头,终将美好!

7.解决编码问题
在解决编码问题前,我们还做一件不可描述的事情~~,固定clientid和secret,每次启动都会改变太麻烦了!

固定clientid和secret

@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
     
  
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
     

        clients.inMemory()
                .withClient("tao")
                .secret("secret")
                .redirectUris("http://live.lingdangji.com/live/pay/getCode.html")
                .scopes("all")
                .authorizedGrantTypes("authorization_code","password");
    }
}

配置编码器

@Configuration
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
     
    @Bean
    public PasswordEncoder passwordEncoder(){
     //密码加密
        return new BCryptPasswordEncoder();
    }

    @Autowired
    public PasswordEncoder passwordEncoders() {
     
        return new BCryptPasswordEncoder();
    }

    @Autowired
    public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
     
        auth.inMemoryAuthentication().withUser("admin").password(passwordEncoders().encode("123456")).roles("USER");
    }
}

这里先跑通,等下在编写数据库互通用户的UserDetailsService
重启服务访问
登录成功后跳转授权界面
SpringCloud整合SpringSecurity OAuth泪目之作_第6张图片
得到code授权码
SpringCloud整合SpringSecurity OAuth泪目之作_第7张图片
走到这里你以为就行了么,想多了,所谓成长就是翻过一个坑掉进另一个坑!按照OAuth2.0协议的流程下一步应该就是通过授权码code获取token了,咋!不信?那就访问看看!这里我们需要发送post去请求,我们可以使用postMan或者谷歌的REST插件,这里我分别做一个演示吧,不用谢!
1.PostMan通过授权码获得token
添加Authorization
SpringCloud整合SpringSecurity OAuth泪目之作_第8张图片
这里username和password不是我们用户登录的,而是我们在MyAuthorizationServerConfig配置的应用的clienth和secret配置这里还没完
SpringCloud整合SpringSecurity OAuth泪目之作_第9张图片
参数我就不过多解释了,因为能发现这些问题的都应该是有SpringSecurity OAuth的知识储备额,没有的话可以评论,收到我会回复!
请求
SpringCloud整合SpringSecurity OAuth泪目之作_第10张图片
2.谷歌REST
SpringCloud整合SpringSecurity OAuth泪目之作_第11张图片
谷歌浏览器安装Restlet Client
TMMD这里怎么还是这个问题,刚才在第6不不是已经解决了么,其实算是决绝了一半吧,第六步是解决用户登录的密码编码器,这里则是应用的secret解码问题所以我们的道路还很长呀!

8.解决secret编码问题
secret编码问题解决的思路和用户登录的密码编码是一样的!不信?你看下面配置
在认证服务核心配置注入编码器,secret使用编码器

@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
     
    @Autowired
    public PasswordEncoder passwordEncoder() {
     //密码加密
        return new BCryptPasswordEncoder();
    }
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
     
        clients.inMemory()
                .withClient("tao")
                .secret(passwordEncoder().encode("secret"))
                //.secret("secret")
                .redirectUris("http://live.lingdangji.com/live/pay/getCode.html")
                .scopes("all")
                .authorizedGrantTypes("authorization_code","password");
    }
}

重启访问,从新登录获取新的授权code
SpringCloud整合SpringSecurity OAuth泪目之作_第12张图片
那么走到这里才算是开始搞定了授权码模式,不要天真的以为这么容易搞定!访问测试密码模式
SpringCloud整合SpringSecurity OAuth泪目之作_第13张图片
这里授权码类型和密码类型使用同一个url只是grant_type需要设置一下!
这里报错,说是没有这种密码授权类型,我TM。。。上面明明配置了,害,还是太年轻,太年轻!
不要慌!

9.解决不支持密码模式
MySecurityConfig

	@Override
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
     
        return super.authenticationManager();
    }

MyAuthorizationServerConfig

	@Autowired
    public AuthenticationManager authenticationManager;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
     
        endpoints.authenticationManager(authenticationManager);
    }

重启访问!ok
SpringCloud整合SpringSecurity OAuth泪目之作_第14张图片
这里还有几点需要注意的报错:java.lang.StackOverflowError: null如果碰到这个异常请不要慌,这就是没有实现globalUserDetails导致登录的用户无法找到,然后一直递归查找,导致应用程序的堆栈已耗尽。其实在第7不的是后我是故意使用哪种方式的,就是故意引出这个报错!那么刚才这个报错就是在我把第7步中的如下代码删除导致的!

@Autowired
    public PasswordEncoder passwordEncoders() {
     //密码加密
        return new BCryptPasswordEncoder();
    }

    @Autowired
    public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
     
        auth.inMemoryAuthentication().withUser("admin").password(passwordEncoders().encode("123456")).roles("USER");
    }

不要删就没关系!但是还是要删掉,因为在第7步的末尾还留了以步,就是连接数据库真实的用户身份权限数据获取!

整合UserDetailsService
删除

@Autowired
    public PasswordEncoder passwordEncoders() {
     //密码加密
        return new BCryptPasswordEncoder();
    }

    @Autowired
    public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
     
        auth.inMemoryAuthentication().withUser("admin").password(passwordEncoders().encode("123456")).roles("USER");
    }

实现MyUserDetailsService

@Component
public class MyUserDetailsService implements UserDetailsService {
     

    @Autowired
    public PasswordEncoder passwordEncoder() {
     //密码加密
        return new BCryptPasswordEncoder();
    }

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
     
        String pas = passwordEncoder().encode("123456");
        //如下通过用户名访问DB查找用户
        return new User(userName, pas, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_root"));
    }
}

其实上面的代码不删除也行,只是这里分了模块,写在不同的项目里,而且认证模块基本不会再改变,MyUserDetailsService 也不是写在认证模块中的,为了灵活性这里写在了demo2,也就是对用我们真实的业务服,让我们自己的业务服务能够更加灵活!

在认证服务核心配置类中添加MyUserDetailsService实现

@Configuration
//@Order(99)
@EnableAuthorizationServer
public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
     
    @Autowired
    public PasswordEncoder passwordEncoder() {
     //密码加密
        return new BCryptPasswordEncoder();
    }
 
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
     

        clients.inMemory()
                .withClient("tao")
                .secret(passwordEncoder().encode("secret"))
                //.secret("secret")
                .redirectUris("http://live.lingdangji.com/live/pay/getCode.html")
                .scopes("all")
                .authorizedGrantTypes("authorization_code","password");
    }

    @Autowired
    public AuthenticationManager authenticationManager;

    @Autowired
    public UserDetailsService userDetailsService;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
     
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }
}

重启服务
SpringCloud整合SpringSecurity OAuth泪目之作_第15张图片
那么基本的整合就到这里!后续还会在其他文章中整合Token生成策略,Token持久化策略,社交登录授权、自定义登录授权,SSO单点登录等!

写在最后

这里整合起来是坑不少,这是基于两点问题来的,首先就是版本问题,再就是版本问题带来的一些配置变动,在老版本的时候确实不是这样的,比较简单!没办法,谁让我们是与时俱进的程序员,永远走在时代了最浪潮,翻滚吧!后浪!!!

你可能感兴趣的:(#,SpringSecurity)