[英]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 措辭:
@ResponseBody
@RestController
@Controller
在這里,您可能正在嘗試將資源服務器配置為客戶端,但這是行不通的。
您可能會參考我編寫的本教程,以獲得最少的 OAuth2 背景和資源服務器的各種配置選項。
如果您想添加比 Google 更多的身份提供者,有兩種選擇
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.