Welcome toVigges Developer Community-Open, Learning,Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
499 views
in Technique[技术] by (71.8m points)

java spring security SSO 登录, 客户端启动报错

折腾了一两周了, 问题始终没有解决, 只能来这里求助了, 希望知道的朋友帮忙下!
下面开始说需求:
公司有两个系统, 我们就称之为聊天系统和业务系统吧, 聊天系统是因为业务系统需要沟通才存在, 聊天系统本身是没有用户, 用户都是来自业务系统.

这两个系统都需要保存数据到数据库, 因为聊天系统需要保存聊天记录, 业务系统也有业务数据需要保存, 两个系统用的框架都是spring boot,而且都是使用 mybatis, 当然了, 出于安全的需要, 就用到了 spring security, 本来是想成立一个用户中心, 但是公司除了这两个系统外,还有其他系统, 这样牵连比较大, 所以没有成立用户中心, 但聊天系统和业务系统本身是需要数据交互的.

所以, 引入 spring security 了以后, 我就想到 SSO 单点登录, 网上了解了下, spring security 实现单点登录的方式还有好多种方式的, 比如用到 CAS, JWT 等等, 我就选择使用 @EnableOAuth2Sso 注解这个方式来实现单点登录, 但是我折腾了好久, 问题始终没有解决.

出现的问题是:
聊天系统(客户端)启动时, 会自动向业务系统(授权端)发起一个 get 请求: oauth/token_key,
业务系统也能收到这个 get 请求, 但它却重定向去获取 static/index.html, 这是业务系统的首页, 但由于前后端分离, 前端采用 vue 来开发, 所以不存在这个页面, 报 404 错误!

请注意: 这个 get 请求的地址在聊天系统(客户端)的 application.properties 文件中配置, key 为: security.oauth2.resource.jwt.key-uri

我产生的疑问是:
1) 聊天系统发起这个 get 请求的时候, 不应该去获取 index.html, 我也不知道发起这个 get 请求的目的是什么, 但他不应该去登录或者请求首页.
2) 网上看到文章, 聊天系统作为客户端, 使用 @EnableOAuth2Sso 来实现单点登录的时候, 聊天系统是不需要配置 security.oauth2.resource.jwt.key-uri 的, 但是我不配置就报错.报错如下:

Description:

Method springSecurityFilterChain in 
org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration 
required a bean of type 'org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoRestTemplateFactory' that could not be found.

The following candidates were found but could not be injected:
    - Bean method 'userInfoRestTemplateFactory' in 'ResourceServerTokenServicesConfiguration' not loaded because @ConditionalOnMissingBean (types: org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration; SearchStrategy: all) found beans of type 'org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration' org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration


Action:

Consider revisiting the entries above or defining a bean of type 'org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoRestTemplateFactory' in your configuration.

Disconnected from the target VM, address: '127.0.0.1:9125', transport: 'socket'

Process finished with exit code 1

下面给出聊天系统(客户端) spring security 的安全配置:

@Configuration
//@EnableWebSecurity()
@EnableOAuth2Sso
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Resource
 private DataSource dataSource;
 @Bean
 @Override protected UserDetailsService userDetailsService() {
        JdbcUserDetailsManager manager = getUserDetailsService();
 manager.setDataSource(dataSource);
 return manager;
 }
    public static JdbcUserDetailsManager getUserDetailsService() {
        JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
 manager.setUsersByUsernameQuery("select login_name AS username, password,'1' AS enabled  from im_user where login_name=?");
 manager.setAuthoritiesByUsernameQuery("select login_name AS username, 'user' AS authority   from im_user where login_name=?");
 return manager;
 }
    @Bean
 PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
 }
    /**
 * 这一步的配置是必不可少的,否则SpringBoot会自动配置一个 AuthenticationManager,覆盖掉内存中的用户
 */
 @Bean
 @Override public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
 }
    @Override
 protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated().and().csrf().disable();
 }
}

业务系统(资源/授权)服务器的 spring security 安全配置如下:

@Order(1)
@Configuration
@EnableWebSecurity(debug=true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
 SysUserDetailService userDetailService;
 // 上传图片所在的路径
 @Value("${UPLOAD_STATIC_PATH}")
    private String UPLOAD_STATIC_PATH;
 @Override
 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailService)
            .passwordEncoder(passwordEncoder());
 }
    @Bean
 PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
 }
    /**
 * 这一步的配置是必不可少的,否则SpringBoot会自动配置一个 AuthenticationManager,覆盖掉内存中的用户
 */
 @Bean
 @Override public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
 }
    @Override
 public void configure(WebSecurity web) throws Exception {
        String[] ignoreList = {
            UPLOAD_STATIC_PATH, "static/index.html", "favicon.ico", "/static/**"
 };
 web.ignoring().antMatchers(ignoreList);
 }
    @Override
 protected void configure(HttpSecurity http) throws Exception {
        String[] permits = {"/oauth/**", "/oauths/**", "/lgn"};
 http.authorizeRequests()
                .antMatchers(permits).permitAll()
            .and()
                .formLogin()
                .loginPage("/lgn")
                .loginProcessingUrl("/user/lgn")
                .successHandler((request, response, authentication) -> {
                    RequestDispatcher dispatcher = request.getRequestDispatcher("/user/loginSuccess");
 dispatcher.forward(request, response);
 })
                .failureHandler((request, response, authentication) -> {
                    RequestDispatcher dispatcher = request.getRequestDispatcher("/user/loginError");
 dispatcher.forward(request, response);
 })
                .permitAll()
            .and()
                .authorizeRequests().anyRequest().authenticated()
            .and()
                .csrf()
                .disable();
 http.logout()
            .logoutUrl("/user/exit")
            .logoutSuccessHandler(
                new LogoutSuccessHandler() {
                    @Override
 public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        RequestDispatcher dispatcher = request.getRequestDispatcher("/user/exitSuccess");
 dispatcher.forward(request, response);
 }
            });
 }
}

下面给出业务系统的授权及资源配置:

@Configuration
 @EnableAuthorizationServer()
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
   @Resource
 AuthenticationManager authenticationManager;
 @Resource
 private UserDetailsService userDetailsService;
 public final static String clientId = "aaa";
 public final  static String scopes = "select";
 public final  static String rawPassword = "bbb";
 @Override
 public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
      String finalSecret = "{bcrypt}" + new BCryptPasswordEncoder().encode(rawPassword);
 clients.inMemory().withClient(clientId)
            .authorizedGrantTypes("password", "refresh_token", "authorization_code")
            .autoApprove(true)
            .scopes(scopes, "user")
            .authorities("oauth2")
            .secret(finalSecret)
            .accessTokenValiditySeconds(7200)
      ;
 }
   @Bean
 public TokenStore getTokenStore() {
      return new InMemoryTokenStore();
 }
   @Override
 public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
      endpoints.authenticationManager(authenticationManager)
            .tokenStore(getTokenStore())
                .userDetailsService(userDetailsService)
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
              ;
 endpoints.tokenServices(tokenServices());
 }
   @Override
 public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
//    oauthServer.allowFormAuthenticationForClients();
 oauthServer.allowFormAuthenticationForClients()
            .tokenKeyAccess("permitAll()")
            .checkTokenAccess("isAuthenticated()");
 }
   @Bean
 public DefaultTokenServices tokenServices() {
      final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
 defaultTokenServices.setTokenStore(getTokenStore());
 return defaultTokenServices;
 }
   @Configuration
 @EnableResourceServer 
 protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
      @Override
 public void configure(ResourceServerSecurityConfigurer resources) {}
      @Override
 public void configure(HttpSecurity http) throws Exception {
          String[] permits = {"/oauth/**", "/oauths/**", "/user/lgn"};
 http
            .authorizeRequests()
               .antMatchers("/api/**")
               .authenticated().and()
               .authorizeRequests().antMatchers(permits).permitAll().and()
            .cors().and()
            .rememberMe().and()
            .csrf().disable();
 }
   }
}

另外还有一点 @EnableResourceServer 注解已经加在业务系统(授权/资源端)的 spring boot 启动类.

聊天系统(客户端)启动报错如下:

java.lang.Object.wait(Native Method)
 java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
 com.mysql.jdbc.AbandonedConnectionCleanupThread.run(AbandonedConnectionCleanupThread.java:40)
2020-09-19 11:37:21.673 ERROR 10832 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'UserController': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'startTioRunner': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'tioWsMsgHandler': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'jwtTokenServices' defined in class path resource [org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration$JwtTokenServicesConfiguration.class]: Unsatisfied dependency expressed through method 'jwtTokenServices' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jwtTokenStore' defined in class path resource [org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration$JwtTokenServicesConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.oauth2.provider.token.TokenStore]: Factory method 'jwtTokenStore' threw exception; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jwtTokenEnhancer' defined in class path resource [org/springframework/boot/autoconfigure/security/oauth2/resource/ResourceServerTokenServicesConfiguration$JwtTokenServicesConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter]: Factory method 'jwtTokenEnhancer' threw exception; nested exception is org.springframework.web.client.HttpClientErrorException$NotFound: 404 null
    at org.springframework.context.annotation.CommonAnnotationBeanPostProcessor.postProcessProperties(CommonAnnotationBeanPostProcessor.java:324)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1378)
    at org.springframework.beans.factory.support.A

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

我是把 授权服务器 单独出来了。你这种情况来讲,也可以试试 单独起个授权服务器,聊天系统和业务系统 都通过这个认证服务器去认证。这样来实现SSO的流程大概如下:

  1. 访问业务系统
  2. 未登陆
  3. 重定向跳转至认证服务器
  4. 输入账号密码
  5. 输入正确,认证服务器登录成功
  6. 附带授权码重定向回最初的url也就是业务系统
  7. 业务系统拿着授权码去认证服务器获取token
  8. 业务系统有了token就可以访问后端API了
  9. 此时登陆聊天系统
  10. 未登陆
  11. 跳转至认证服务器
  12. 认证服务器已登陆,且有cookie存的session信息在认证服务器的域名下
  13. 此时直接带着授权码跳转回聊天系统
  14. 聊天系统拿着授权码去获取token
  15. 有了token聊天系统也可访问自己的后端了。

前后端不分离实现比较简单;
前后端分离,需要前端保存认证服务器返回的token,根据有无token或token是否有效来判断是否需要跳转认证服务器,以及跳转回来去获取token也需要前端去实现。
以上都是基于授权码模式的SSO实现。
我自己测试过,完成登录还是没问题的。题主可以往这个方向试试。我开始 也是 我有个主业务系统,我想把登录授权放这个祝业务系统上,但是 就有各种问题。后来 拆出来,认证服务器只管认证授权就好了。


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to Vigges Developer Community for programmer and developer-Open, Learning and Share
...