提前,我为这个冗长的问题道歉!事实上,问题并没有那么长,但是我发布了很多我的代码片段,因为我真的不知道什么是相关的或不解决我的问题......
我一直在尝试使用以下方法制作一个简单的 poc: - Angular 8 前端 - 用于身份验证的 Keycloak 服务器 - Spring Cloud 后端架构: - 使用 Spring Cloud Security 保护的 Spring Cloud Gateway - Spring Cloud Netflix Eureka 服务器 - Spring云配置服务器 - 一些使用 Spring Security OAuth2 保护的 Springboot 微服务
不工作:我无法让我的 Angular 应用程序访问并从受保护的后端 uris 中获取任何数据。我得到401 Unauthorized
回应。如果我对 MS Spring secu 过滤器设置断点,我只是在HttpServletRequest request
工作: - 通过 Angular 进行前端身份验证 - Angular 可以从后端未受保护的 uri 中获取数据 - 邮递员对受保护的后端 uri 进行测试,并将 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
- 应用模块*
@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 {
}
- 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();
}
}
- DefaultOAuthInterceptor*
@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))*/;
}
}
- 授权配置*
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;
}
- 验证配置服务*
@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 { }
- 环境.ts
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
}
};
网关
- 应用程序.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
- SpringBoot应用程序*
@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();
}
}
微服务
- 应用程序.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
- 安全配置*
@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;
}
}
- KeycloakRealmRoleConverter*
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());
}
}
- 用户名SubClaimAdapter*
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 Keycloak 配置,相关的是: - 访问类型:机密 - 启用标准流:开 - 启用隐式流:关 - 启用直接访问授权:开 - 启用服务帐户:开 - 启用授权:开
我真的尝试了很多东西,但我已经没有任何想法了......
有人可以看看并告诉我我做错了什么吗?我将不胜感激!:)
非常感谢您的时间!:)