当前位置: 首页 > 知识库问答 >
问题:

如何使用KeyClope保护Angular 8前端,使用gateway eureka保护Java Spring云微服务后端

杜浩壤
2023-03-14

提前,我为这个looong问题道歉!事实上,问题并没有那么长,但是我发布了很多我的代码片段,因为我真的不知道什么与解决我的问题相关或不相关......

我一直在尝试使用以下内容制作一个简单的poc:-Angular 8前端-用于身份验证的Keycloak服务器-Spring云后端架构:-使用Spring Cloud Security保护的Spring Cloud Gateway-Spring CloudNetflixEureka服务器-Spring Cloud配置服务器-使用Spring Security OAuth2保护的一些Springboot微服务不起作用:我无法设法让我的Angular应用程序访问并从我受保护的后端uris获取任何数据。我收到了401 Un授权响应。如果我断点到MS Spring secu过滤器,我只是在HttpServletRequest请求中没有任何令牌

正在工作: - 通过 Angular 使用前端到 Angular 进行身份验证 - Angular 可以从后端未受保护的 uris 获取数据 - Postman 在受保护的后端 uris 上进行测试,OAuth2 授权类型设置为资源所有者密码凭据

我遵循了许多教程,但是我在这个教程中获得了更好的结果:https://blog.jdriven.com/2019/11/spring-cloud-gateway-with-openid-connect-and-token-relay/

以下是我认为相关的代码段:

棱角的

我使用了这个OAuth库:https://www.npmjs.com/package/angular-oauth2-oidc

    <李> < ul > < li >应用模块*
@NgModule({
  declarations: [
    AppComponent,
    BooksComponent,
    HeaderComponent,
    SideNavComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    HttpClientModule,
    AppRoutingModule,
    ReactiveFormsModule,
    OAuthModule.forRoot({
      resourceServer: {
        allowedUrls: ['http://localhost:4200'],
        sendAccessToken: true
      }
    }),
    AuthConfigModule,
    MatFormFieldModule,
    MatInputModule,
    MatButtonModule
  ],
  providers: [
    TheLibraryGuard,
    { provide: HTTP_INTERCEPTORS,
      useClass: DefaultOAuthInterceptor,
      multi: true
    }
  ],
  entryComponents: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {
}
    <李> < ul > < li>CustomAuthGuard*
@Injectable()
export class CustomAuthGuard implements CanActivate {

  constructor(private oauthService: OAuthService, protected router: Router) {
  }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): any {
    const hasIdToken = this.oauthService.hasValidIdToken();
    const hasAccessToken = this.oauthService.hasValidAccessToken();

    if (this.oauthService.hasValidAccessToken()) {
      return (hasIdToken && hasAccessToken);
    }

    this.router.navigate([this.router.url]);
    return this.oauthService.loadDiscoveryDocumentAndLogin();
  }
}
    • 默认OAuth拦截器*
    @Injectable()
    export class DefaultOAuthInterceptor implements HttpInterceptor {
    
      constructor(
        private authStorage: OAuthStorage,
        private oauthService: OAuthService,
        private errorHandler: OAuthResourceServerErrorHandler,
        @Optional() private moduleConfig: OAuthModuleConfig
      ) {
      }
    
      private checkUrl(url: string): boolean {
        const found = this.moduleConfig.resourceServer.allowedUrls.find(u => url.startsWith(u));
        return !!found;
      }
    
      public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    
        console.log('INTERCEPTOR');
    
        const url = req.url.toLowerCase();
    
        if (!this.moduleConfig) { return next.handle(req); }
        if (!this.moduleConfig.resourceServer) { return next.handle(req); }
        if (!this.moduleConfig.resourceServer.allowedUrls) { return next.handle(req); }
        if (!this.checkUrl(url)) { return next.handle(req); }
    
        const sendAccessToken = this.moduleConfig.resourceServer.sendAccessToken;
    
        if (sendAccessToken) {
    
          // const token = this.authStorage.getItem('access_token');
          const token = this.oauthService.getIdToken();
          const header = 'Bearer ' + token;
    
          console.log('TOKEN in INTERCEPTOR : ' + token);
    
          const headers = req.headers
            .set('Authorization', header);
    
          req = req.clone({ headers });
        }
    
        return next.handle(req)/*.catch(err => this.errorHandler.handleError(err))*/;
    
      }
    }
    
    • 这是一个很好的例子
      • AuthConfig*
      export const authConfig: AuthConfig = {
      
        issuer: environment.keycloak.issuer,
        redirectUri: environment.keycloak.redirectUri,
        clientId: environment.keycloak.clientId,
        dummyClientSecret: environment.keycloak.dummyClientSecret,
        responseType: environment.keycloak.responseType,
        scope: environment.keycloak.scope,
        requireHttps: environment.keycloak.requireHttps,
        // at_hash is not present in JWT token
        showDebugInformation: environment.keycloak.showDebugInformation,
        disableAtHashCheck: environment.keycloak.disableAtHashCheck
      };
      
      
      export class OAuthModuleConfig {
        resourceServer: OAuthResourceServerConfig = {sendAccessToken: false};
      }
      
      export class OAuthResourceServerConfig {
        /**
         * Urls for which calls should be intercepted.
         * If there is an ResourceServerErrorHandler registered, it is used for them.
         * If sendAccessToken is set to true, the access_token is send to them too.
         */
        allowedUrls?: Array<string>;
        sendAccessToken = true;
        customUrlValidation?: (url: string) => boolean;
      }
      
      • 这是一个很好的例子
        • AuthConfigService*
        @Injectable()
        export class AuthConfigService {
        
          private decodedAccessToken: any;
          private decodedIDToken: any;
        
          constructor(
            private readonly oauthService: OAuthService,
            private readonly authConfig: AuthConfig
          ) {
          }
        
          async initAuth(): Promise<any> {
            return new Promise((resolveFn, rejectFn) => {
              // setup oauthService
              this.oauthService.configure(this.authConfig);
              this.oauthService.setStorage(localStorage);
              this.oauthService.tokenValidationHandler = new NullValidationHandler();
        
              // subscribe to token events
              this.oauthService.events
                .pipe(filter((e: any) => {
                  return e.type === 'token_received';
                }))
                .subscribe(() => this.handleNewToken());
        
              // continue initializing app or redirect to login-page
        
              this.oauthService.loadDiscoveryDocumentAndLogin().then(isLoggedIn => {
                if (isLoggedIn) {
                  this.oauthService.setupAutomaticSilentRefresh();
                  resolveFn();
                } else {
                  this.oauthService.initLoginFlow();
                  rejectFn();
                }
              });
        
            });
          }
        
          private handleNewToken() {
            this.decodedAccessToken = this.oauthService.getAccessToken();
            this.decodedIDToken = this.oauthService.getIdToken();
          }
        }
        
          • 配置模块*
          @NgModule({
            imports: [ HttpClientModule, OAuthModule.forRoot() ],
            providers: [
              AuthConfigService,
              { provide: AuthConfig, useValue: authConfig },
              OAuthModuleConfig,
              {
                provide: APP_INITIALIZER,
                useFactory: init_app,
                deps: [ AuthConfigService ],
                multi: true
              }
            ]
          })
          export class AuthConfigModule { }
          
          • 环境技术服务
          export const environment = {
            production: false,
            envName: 'local',
            baseUrl: 'http://localhost:8081/',
            keycloak: {
              issuer: 'http://localhost:8080/auth/realms/TheLibrary',
              redirectUri: 'http://localhost:4200/',
              clientId: 'XXXXXXXXXXX',
              dummyClientSecret: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
              responseType: 'code',
              scope: 'openid profile email',
              requireHttps: false,
              // at_hash is not present in JWT token
              showDebugInformation: true,
              disableAtHashCheck: true
            }
          };
          

          网关

          • 这是一个很好的例子
            • application.yml*
            spring:
                application:
                    name: gateway-service
                cloud:
                    config:
                        uri: http://localhost:8888
                    discovery:
                        enabled: true
                    gateway:
            #            default-filters:
            #                - TokenRelay
                        routes:
                            -   id: THELIBRARY-MS-BOOK
                                uri: lb://thelibrary-ms-book
                                predicates:
                                    - Path=/api/**
                                filters:
                                    - TokenRelay=
                        globalcors:
                            corsConfigurations:
                                '[/**]':
                                    allowedOrigins: "*"
                                    allowedMethods:
                                        - GET
                                        - POST
                                        - DELETE
                                        - PUT
                                    add-to-simple-url-handler-mapping: true
                security:
                    oauth2:
                        client:
                            provider:
                                keycloak:
                                    issuer-uri: http://localhost:8080/auth/realms/TheLibrary
                                    user-name-attribute: preferred_username
                            registration:
                                keycloak:
                                    client-id: xxxxxxxxxxxxxxxxxx
                                    client-secret: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXxx
            
            eureka:
                client:
                    serviceUrl:
                        defaultZone: http://localhost:8761/eureka/
            
            management:
                endpoints:
                    web:
                        exposure:
                            include: "*"
            
            server:
                port: 8081
            
            logging:
                level:
                    org:
                        springframework:
                            cloud.gateway: DEBUG
                            http.server.reactive: DEBUG
                            web.reactive: DEBUG
            
              <李> < ul > < li>SpringBootApplication*
            @SpringBootApplication
            @CrossOrigin("*")
            public class GatewayApplication {
            
            //  @Autowired
            //  private TokenRelayGatewayFilterFactory filterFactory;
            //
            //  @Bean
            //  public RouteLocator myRoutes(RouteLocatorBuilder builder) {
            //      return builder.routes()
            //                     .route(route -> route
            //                                         .path("/api/**")
            ////                                           .filters(f -> f.hystrix(config -> config.setName("d").setFallbackUri( "forward:/defaultBook"  )))
            //                                         .filters(f -> f.filter( filterFactory.apply() ))
            //                                         .uri("lb://thelibrary-ms-book")
            //                                         .id( "ms-books" ))
            //              .build();
            //  }
            
                @Bean
                DiscoveryClientRouteDefinitionLocator discoveryClientRouteDefinitionLocator(
                        ReactiveDiscoveryClient reactiveDiscoveryClient,
                        DiscoveryLocatorProperties discoveryLocatorProperties ){
                    return new DiscoveryClientRouteDefinitionLocator(reactiveDiscoveryClient, discoveryLocatorProperties);
                }
            
                public static void main( String[] args ) {
                    SpringApplication.run( GatewayApplication.class, args );
                }
            
                @Bean
                public SecurityWebFilterChain springSecurityFilterChain( ServerHttpSecurity http,
                                                                         ReactiveClientRegistrationRepository clientRegistrationRepository) {
            
                    // Require authentication for all requests
                    http.cors().and().authorizeExchange().anyExchange().permitAll();
            
                    // Allow showing /home within a frame
            //      http.headers().frameOptions().mode( XFrameOptionsServerHttpHeadersWriter.Mode.SAMEORIGIN);
            
                    // Disable CSRF in the gateway to prevent conflicts with proxied service CSRF
                    http.csrf().disable();
                    return http.build();
                }
            }
            

            微服务

            • 这是一个很好的例子
              • application.yml*
              spring:
                  application:
                      name: thelibrary-ms-book
                  cloud:
                      config:
                          uri: http://localhost:8888
                          profile: local, prod
                      discovery:
                          enabled: true
                  data:
                      rest:
                          return-body-on-create: true
                          return-body-on-update: true
                  rabbitmq:
                      host: localhost
                      username: user
                      password: user
              
                  security:
                      oauth2:
                          resourceserver:
                              jwt:
                                  issuer-uri: http://localhost:8080/auth/realms/TheLibrary
                                  jwk-set-uri: http://localhost:8080/auth/realms/TheLibrary/.well-known/openid-configuration
              
              eureka:
                  client:
                      serviceUrl:
                          defaultZone: http://localhost:8761/eureka/
              
              management:
                  endpoints:
                      web:
                          exposure:
                              include: "*"
              
              server:
                  port: 8090
                  servlet:
                      context-path: /api/
              
              logging:
                  level:
                      org:
                          hibernate:
                              SQL: DEBUG
                              type:
                                  descriptor:
                                      sql:
                                          BasicBinder: TRACE
              
                <李> < ul > < li >安全性配置*
              @Configuration
              @EnableWebSecurity
              @EnableGlobalMethodSecurity(prePostEnabled = true)
              @AllArgsConstructor
              public class SecurityConfig extends WebSecurityConfigurerAdapter {
              
                  @Override
                  protected void configure(HttpSecurity http) throws Exception {
                      // Validate tokens through configured OpenID Provider
                      http.cors().and().oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());
                      http.cors().and().authorizeRequests().mvcMatchers("/books").hasRole("admin");
                      // Allow showing pages within a frame
                      http.headers().frameOptions().sameOrigin();
                  }
              
                  private JwtAuthenticationConverter jwtAuthenticationConverter() {
                      JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
                      // Convert realm_access.roles claims to granted authorities, for use in access decisions
                      jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRealmRoleConverter());
                      return jwtAuthenticationConverter;
                  }
              
                  @Bean
                  public JwtDecoder jwtDecoderByIssuerUri( OAuth2ResourceServerProperties properties) {
                      String issuerUri = properties.getJwt().getIssuerUri();
                      NimbusJwtDecoder jwtDecoder = ( NimbusJwtDecoder ) JwtDecoders.fromIssuerLocation(issuerUri);
                      // Use preferred_username from claims as authentication name, instead of UUID subject
                      jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
                      return jwtDecoder;
                  }
              }
              
                • 键斗RealmRoleConverter*
                class KeycloakRealmRoleConverter implements Converter< Jwt, Collection< GrantedAuthority > > {
                
                    @Override
                    @SuppressWarnings("unchecked")
                    public Collection<GrantedAuthority> convert(final Jwt jwt) {
                        final Map<String, Object> realmAccess = (Map<String, Object>) jwt.getClaims().get("realm_access");
                        return (( List<String> ) realmAccess.get("roles")).stream()
                                .map(roleName -> "ROLE_" + roleName)
                                .map( SimpleGrantedAuthority::new)
                                .collect( Collectors.toList());
                    }
                }
                
                  • 用户名子声明适配器*
                  class UsernameSubClaimAdapter implements Converter< Map<String, Object>, Map<String, Object>> {
                  
                      private final MappedJwtClaimSetConverter delegate = MappedJwtClaimSetConverter.withDefaults( Collections.emptyMap());
                  
                      @Override
                      public Map<String, Object> convert(Map<String, Object> claims) {
                          Map<String, Object> convertedClaims = this.delegate.convert(claims);
                          String username = (String) convertedClaims.get("preferred_username");
                          convertedClaims.put("sub", username);
                          return convertedClaims;
                      }
                  }
                  
                  • 这是一个很好的例子
                    • 相关依赖关系*
                            <springboot-version>2.2.5.RELEASE</springboot-version>
                    
                            <dependency>
                                <groupId>org.springframework.cloud</groupId>
                                <artifactId>spring-cloud-starter-config</artifactId>
                            </dependency>
                    
                            <dependency>
                                <groupId>org.springframework.cloud</groupId>
                                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
                            </dependency>
                    
                            <dependency>
                                <groupId>org.springframework.boot</groupId>
                                <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
                            </dependency>
                    
                            <dependency>
                                <groupId>org.springframework.cloud</groupId>
                                <artifactId>spring-cloud-dependencies</artifactId>
                                <version>Hoxton.SR3</version>
                                <type>pom</type>
                                <scope>import</scope>
                            </dependency>
                    

                    我有一个非常标准的Cleint钥匙斗篷配置,相关的是: - 访问类型:机密 - 启用标准流:开 - 启用隐式流:关闭 - 启用直接访问授权:开 - 启用服务帐户:开 - 启用授权:开

                    我真的尝试了很多东西,但我再也不知道了。。。

                    有人可以看一下,告诉我我做错了什么吗?我将不胜感激!:)

                    非常感谢你的时间!:)

共有1个答案

夏侯楷
2023-03-14

这就是解决我问题的方法!

1-角度:纠正默认OAuthiInterceptor

删除此部分:

    if (!this.moduleConfig) { return next.handle(req); }
    if (!this.moduleConfig.resourceServer) { return next.handle(req); }
    if (!this.moduleConfig.resourceServer.allowedUrls) { return next.handle(req); }
    if (!this.checkUrl(url)) { return next.handle(req); }

无论出于什么原因,这些条件中的一个总是以true结束,然后方法的其余部分永远不会执行。(警告:我真的不知道跳过此代码的后果)

所以最后的拦截是:

js prettyprint-override">@Injectable()
export class DefaultOAuthInterceptor implements HttpInterceptor {
  constructor(
    private authStorage: OAuthStorage,
    private oAuthService: OAuthService,
    private errorHandler: OAuthResourceServerErrorHandler,
    @Optional() private moduleConfig: OAuthModuleConfig
  ) {
  }

  private checkUrl(url: string): boolean {
    if (this.moduleConfig.resourceServer.customUrlValidation) {
      return this.moduleConfig.resourceServer.customUrlValidation(url);
    }

    if (this.moduleConfig.resourceServer.allowedUrls) {
      return !!this.moduleConfig.resourceServer.allowedUrls.find(u =>
        url.startsWith(u)
      );
    }

    return true;
  }

  public intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    const url = req.url.toLowerCase();

    // if (
    //   !this.moduleConfig ||
    //   !this.moduleConfig.resourceServer ||
    //   !this.checkUrl(url)
    // ) {
    //   return next.handle(req);
    // }

    const sendAccessToken = this.moduleConfig.resourceServer.sendAccessToken;

    if (!sendAccessToken) {
      return next
        .handle(req)
        .pipe(catchError(err => this.errorHandler.handleError(err)));
    }

    return merge(
      of(this.oAuthService.getAccessToken()).pipe(
        filter(token => (token ? true : false))
      ),
      this.oAuthService.events.pipe(
        filter(e => e.type === 'token_received'),
        timeout(this.oAuthService.waitForTokenInMsec || 0),
        catchError(_ => of(null)), // timeout is not an error
        map(_ => this.oAuthService.getAccessToken())
      )
    ).pipe(
      take(1),
      mergeMap(token => {
        if (token) {
          const header = 'Bearer ' + token;
          const headers = req.headers.set('Authorization', header);
          req = req.clone({headers});
        }

        return next
          .handle(req)
          .pipe(catchError(err => this.errorHandler.handleError(err)));
      })
    );
  }
}

2-在网关中,添加一个CorsWebFilter在Angular拦截器正常工作的情况下,我仍然有一个CORS问题,不管来自Spring Cloud网关留档的yaml配置如何。

我不得不添加一个简单的CorsWebFilter,因为这个链接说 https://github.com/spring-cloud/spring-cloud-gateway/issues/840:

@Configuration
public class PreFlightCorsConfiguration {

    @Bean
    public CorsWebFilter corsFilter() {
        return new CorsWebFilter(corsConfigurationSource());
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration().applyPermitDefaultValues();
        config.addAllowedMethod( HttpMethod.GET);
        config.addAllowedMethod( HttpMethod.PUT);
        config.addAllowedMethod( HttpMethod.POST);
        config.addAllowedMethod(HttpMethod.DELETE);
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

就是这样!它现在像一个魅力:)希望这有助于:)

 类似资料:
  • 我正在寻找一个最佳实践和高效的解决方案,以确保通过REST与Web客户端应用程序通信的多个微服务的安全。 当前设置: 这些微服务是用Java制作的,带有Spring框架,并运行在Docker容器中。 客户端是一个Angular 2应用程序。 我创建了一个新的µ服务,它将充当“网关”,是我的web客户端和其他服务之间的唯一通信点。 我从远程身份验证API检索JWT加密令牌(让我们称之为LOCK) 我

  • 我有一个前端,我通过让我们的登录页面向Keycloak服务器发出POST请求来保护它,这样我就可以获取访问令牌(JWT)。我们不使用kecloak提供的登录页面。此前端对后端(spel-boot)servlet进行REST调用,该servlet使用这些调用来查询数据库并将数据发送回前端。我看到的大多数关于保护spel-boot应用程序的教程都使用spel-boot来服务网页,并使用默认的keycl

  • 我有两个微服务,它们应该相互通信。我的例子是:微服务A向微服务B发出请求。用户不应该有访问权限,只能访问其他微服务。我想,那个解决方案可能是“Http基本身份验证”,所以microservice A还应该向microservice B发送用户名和密码。这很好,但microservice A应该如何找到用户名和密码呢?我正在使用Eureka作为发现服务,但在这种情况下,我没有找到任何可以帮助我的。我

  • 我正按照文章中的说明,尝试使用keydepeat保护Spring BootREST服务https://medium.com/devops-dudes/securing-spring-boot-rest-apis-with-keycloak-1d760b2004e.我以docker服务的形式启动KeyClope和我的服务(docker compose.yml见下文) 然后,我首先 然后在将环境变量$

  • 问题内容: 通常我跑步时总是得到这个输出,我确信每个人在跑步时都会得到。这不是全部输出,而是有关特定语句的。 正如标题所说的,我总是得到输出。如何确保端口ChromeDriver仅使用受保护的端口? 问题答案: 此信息消息… …是 ChromeDriver v2.46* 引发的错误的结果 * 分析 根据讨论,如果启用了详细日志记录,则2.46会生成意外的debug.log文件,在logging.c

  • 问题内容: 我的应用程序使用Express和AngularJS。我正在使用express通过静态处理角度代码的基本网络设置。角度代码使用的服务会影响express托管的API端点。我只希望用户经过身份验证后才能访问API端点。如何通过PassportJS完成此操作? 问题答案: 我已经在github上上传了一个Angular-Express 项目。 仍在进行中。希望对您有所帮助。 它使用Passp