簡體   English   中英

使用 Google 的 Angular/Spring OAuth2 身份驗證,Rest 調用始終返回 401 Unauthorized

[英]Angular/Spring OAuth2 authentication using Google, Rest calls always return 401 Unauthorized

我正在嘗試在 spring 引導項目中實現 Google 身份驗證,

首先,我使用以下依賴項:

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

然后,我使用以下屬性(application.properties):

spring.security.oauth2.client.registration.google.client-id=[clientId]
spring.security.oauth2.client.registration.google.client-secret=[clientSecret]

對於安全配置

    @Configuration
    @ConditionalOnProperty(value = "mode.is-dev", havingValue = "false")
    public class SecurityConfig {
        
        private UserService userService;
        private ClientRegistrationRepository clientRegistrationRepository;
        
        ProdSecurityConfig(UserService userService, ClientRegistrationRepository clientRegistrationRepository) {
            this.userService = userService;
            this.clientRegistrationRepository = clientRegistrationRepository;
        }
        
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            http.formLogin().disable();
            
            //1st match is slected:
            http.authorizeRequests()
                    //Config must be loaded before login => no sensible info must be returned
                    .antMatchers("/api/config").permitAll()
                    .anyRequest().authenticated();
            
            http.oauth2Login()
                    .clientRegistrationRepository(clientRegistrationRepository)
                    .defaultSuccessUrl("/#/")
                    .failureUrl("/#/")
                    .authorizedClientRepository(new HttpSessionOAuth2AuthorizedClientRepository())
                    .userInfoEndpoint()
                    .oidcUserService(userService);
            
            //return 401 on unauthenticated requests (instead of 302 redirect)
            http.exceptionHandling()
                    .defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(UNAUTHORIZED), new AntPathRequestMatcher("/api/**"));
            
            http.logout()
                    .deleteCookies("JSESSIONID")
                    .clearAuthentication(true)
                    .invalidateHttpSession(true)
                    .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
            
            http.csrf()
                    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
            
            return http.build();
        }
    
        /**
         * Overrides the default factory to customize it with an added {@link CustomJwtValidator}
         * 
         * @return an {@link OidcIdTokenDecoderFactory} with an extra {@link CustomJwtValidator}
         */
        @Bean
        @ConditionalOnProperty(value = "mode.is-dev", havingValue = "false")
        public JwtDecoderFactory<ClientRegistration> customJwtDecoderFactory() {
            OidcIdTokenDecoderFactory factory = new OidcIdTokenDecoderFactory();
            factory.setJwtValidatorFactory(
                    (ClientRegistration clientRegistration) -> new DelegatingOAuth2TokenValidator<Jwt>(
                            new JwtTimestampValidator(), 
                            new OidcIdTokenValidator(clientRegistration),
                            new CustomJwtValidator()
                    ));
            
            return factory;
        }
        
        /**
         * Custom JWT validator that checks the User's email Domain 
         * Otherwise, any Google account can be "logged-in"
         */
        private static class CustomJwtValidator implements OAuth2TokenValidator<Jwt> {
            private final List<String> validDomains = new ArrayList("@domain1.com","@domain2.com", "@domain3.com");
            
            @Override
            public OAuth2TokenValidatorResult validate(Jwt token) {
                String email = token.getClaimAsString("email");
                if(email != null && validDomains.stream().anyMatch(d -> email.endsWith(d))) {
                    return OAuth2TokenValidatorResult.success();
                }
                return OAuth2TokenValidatorResult.failure(new OAuth2Error(INVALID_TOKEN));
            }
        }
    }


/*UserService Class:*/


    @Service
    public class UserService implements OAuth2UserService<OidcUserRequest, OidcUser>{
    
        private UserRepository userRepo;
    
        public UserService(UserRepository userRepo) {
            this.userRepo = userRepo;
        }
        
        public List<String> retrieveUserAuthorizedApps(final String userName) {
            return userRepo.retrieveUserAuthorizedApps(userName);   
        }
        
        @Override
        public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
            String email = userRequest.getIdToken().getEmail();
            List<SimpleGrantedAuthority> authorities = retrieveUserAuthorizedApps(email).stream()
                    .map(appName -> new SimpleGrantedAuthority(appName))
                    .collect(toList());
            
            return new DefaultOidcUser(authorities, userRequest.getIdToken());
        }
    }

令牌首先從 angular 客戶端應用程序生成,使用點擊登錄按鈕后觸發的以下代碼:

    import {AfterViewInit, ChangeDetectorRef, Component, OnInit} from '@angular/core';
    import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
    import {AppModule} from './app.module';
    import {AuthService} from './auth-service/auth.service';
    import {ObjectUtils} from './common/util/object-utils.service';
    import {AppComponentService} from './app.component.service';
    
    declare const gapi: any;
    
    @Component({
      selector: 'app-login',
      templateUrl: './login.component.html'
    })
    
    export class LoginComponent implements OnInit, AfterViewInit {
    
      gapiInitialzed = false;
    
      public constructor(private service: AppComponentService, public changeDetectorRef: ChangeDetectorRef, public authService: AuthService) {
      }

      async ngOnInit() {}
    
      async ngAfterViewInit() {
        if (!(globalThis.projectEnvironment as any).isDev) {
          if (ObjectUtils.isNullOrUndefined(gapi.auth2)) {
            this.googleInit();
          }
        } else {
          this.authService.isAuthenticated = true;
          this.changeDetectorRef.detectChanges();
          await platformBrowserDynamic().bootstrapModule(AppModule);
        }
      }
    
    
      public googleInit() {
    
        /*Defining of callback function after loading auth2*/
        const initClient = () => {
    
          const onSuccess = async (googleUser) => {
            const profile = googleUser.getBasicProfile();

            this.authService.isAuthenticated = true;
            this.changeDetectorRef.detectChanges();
            await platformBrowserDynamic().bootstrapModule(AppModule);
          }
    
          const onError = (error) => {
            console.log(error)
          }
    
          gapi.auth2.init({
            client_id: (globalThis.projectEnvironment as any).openidConfig.client_id,   
            cookiepolicy: 'single_host_origin',
            scope: 'profile email'
          })
            /* Promise after initializing */
            .then(async auth2 => {
              if (gapi.auth2.getAuthInstance() && gapi.auth2.getAuthInstance().isSignedIn.get()) {
                this.authService.user = gapi.auth2.getAuthInstance().currentUser.get();
                this.authService.isAuthenticated = true;
                this.service.applications = await this.service.retrieveUserAuthorizedApp(this.authService.user.getBasicProfile().getEmail()).toPromise();
                this.changeDetectorRef.detectChanges();
                await platformBrowserDynamic().bootstrapModule(AppModule);
              } else {
                this.gapiInitialzed = true;
                this.authService.isAuthenticated = false;
                this.changeDetectorRef.detectChanges();
                /* binding click handler to DOM 'signin' button with ID 'googleBtn' */
                /* attachClickHandler():(container, options, onSuccess(), err()) */
                auth2.attachClickHandler(document.getElementById('googleBtn'), {},
                  onSuccess,
                  onError);
              }
            });
        }
        /*Loading auth2 library*/
        gapi.load('auth2', initClient);  // 2nd argument is reference to callback function
      }
    }

在客戶端登錄后並使用作為 Rest 調用的 header 返回的令牌時,通過在每個請求中添加以下 header :

Authorization: Bearer id_token

我總是回復狀態為 401 Unauthorized

2022-08-21 15:37:05.213 TRACE 9480 --- [nio-8080-exec-6] o.s.s.w.a.i.FilterSecurityInterceptor    : Authorizing filter invocation [GET /api/services/userpreferences/getuserauthorizedapps?userName=test@dom1.com] with attributes [authenticated]
2022-08-21 15:37:05.213 TRACE 9480 --- [nio-8080-exec-6] o.s.s.w.a.expression.WebExpressionVoter  : Voted to deny authorization
2022-08-21 15:37:05.213 TRACE 9480 --- [nio-8080-exec-6] o.s.s.w.a.i.FilterSecurityInterceptor    : Failed to authorize filter invocation [GET /api/services/userpreferences/getuserauthorizedapps?userName=test@dom1.com] with attributes [authenticated] using AffirmativeBased [DecisionVoters=[org.springframework.security.web.access.expression.WebExpressionVoter@3980b44f], AllowIfAllAbstainDecisions=false]
2022-08-21 15:37:05.214 TRACE 9480 --- [nio-8080-exec-6] o.s.s.w.a.ExceptionTranslationFilter     : Sending AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[ROLE_ANONYMOUS]] to authentication entry point since access is denied

org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:73) ~[spring-security-core-5.6.2.jar:5.6.2]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.attemptAuthorization(AbstractSecurityInterceptor.java:239) ~[spring-security-core-5.6.2.jar:5.6.2]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:208) ~[spring-security-core-5.6.2.jar:5.6.2]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:113) ~[spring-security-web-5.6.2.jar:5.6.2]
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:81) ~[spring-security-web-5.6.2.jar:5.6.2]

OAuth2 措辭:

  • 客戶端是您的 Angular 應用程序
  • 資源服務器是帶有@ResponseBody @RestController @Controller
  • Google 是授權服務器

在這里,您可能正在嘗試將資源服務器配置為客戶端,但這是行不通的。

您可能會參考我編寫的本教程,以獲得最少的 OAuth2 背景和資源服務器的各種配置選項。

附言

如果您想添加比 Google 更多的身份提供者,有兩種選擇

  • 如果所有其他授權服務器都是 OpenID 和服務器 JWT,那么您可以使用上面教程中使用的庫中的多租戶功能(快速簡單的解決方案)
  • 如果至少一個授權服務器不是 OpenID 或不提供 JWT(例如 Github),或者如果您需要統一的角色管理,或者連接到 LDAP 等,那么您應該考慮使用能夠像 Keycloak 這樣的身份聯合來代理其他身份源(您的堆棧中還有一項服務,但功能如此強大)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM