谷歌在 Angular 7 中使用 .NET Core API 登录

Posted

技术标签:

【中文标题】谷歌在 Angular 7 中使用 .NET Core API 登录【英文标题】:Google login in Angular 7 with .NET Core API 【发布时间】:2019-07-08 17:16:24 【问题描述】:

我正在尝试在我的 Angular 应用程序中实现 Google 登录。如果我尝试调用外部登录服务器的 api 端点返回 405 错误代码,如下所示:

从源“null”访问位于 'https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=...'(从 'http://localhost:5000/api/authentication/externalLogin?provider=Google' 重定向)的 XMLHttpRequest 已被 CORS 策略阻止:对预检请求的响应未通过访问控制检查:否 'Access-Control-Allow-Origin ' 请求的资源上存在标头。

如果我在新的浏览器选项卡中调用 api/authentication/externalLogin?provider=Google 一切正常。我认为问题出在角度代码中。

我的 api 适用于 localhost:5000。 Angular 应用程序适用于 localhost:4200。我使用 .net core 2.1 和 Angular 7

C#代码

Startup.cs

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(x =>

    x.RequireHttpsMetadata = false;
    x.SaveToken = true;
    x.TokenValidationParameters = new TokenValidationParameters
    
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(key),
        ValidateIssuer = false,
        ValidateAudience = false
    ;
)
.AddCookie()
.AddGoogle(options => 
    options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.ClientId = "xxx";
    options.ClientSecret = "xxx";
    options.Scope.Add("profile");
    options.Events.OnCreatingTicket = (context) =>
    
        context.Identity.AddClaim(new Claim("image", context.User.GetValue("image").SelectToken("url").ToString()));

        return Task.CompletedTask;
    ;
);

AuthenticationController.cs

[HttpGet]
public IActionResult ExternalLogin(string provider)

    var callbackUrl = Url.Action("ExternalLoginCallback");
    var authenticationProperties = new AuthenticationProperties  RedirectUri = callbackUrl ;
    return this.Challenge(authenticationProperties, provider);


[HttpGet]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)

    var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);

    return this.Ok(new
    
        NameIdentifier = result.Principal.FindFirstValue(ClaimTypes.NameIdentifier),
        Email = result.Principal.FindFirstValue(ClaimTypes.Email),
        Picture = result.Principal.FindFirstValue("image")
    );

角度代码

login.component.html

<button (click)="googleLogIn()">Log in with Google</button>

login.component.ts

googleLogIn() 
  this.authenticationService.loginWithGoogle()
  .pipe(first())
  .subscribe(
    data => console.log(data)
  );

authentication.service.ts

public loginWithGoogle() 
  return this.http.get<any>(`$environment.api.apiUrl$environment.api.authenticationexternalLogin`,
  
    params: new HttpParams().set('provider', 'Google'),
    headers: new HttpHeaders()
      .set('Access-Control-Allow-Headers', 'Content-Type')
      .set('Access-Control-Allow-Methods', 'GET')
      .set('Access-Control-Allow-Origin', '*')
  )
  .pipe(map(data => 
    return data;
  ));

我想象以下方案: Angular -> 我的 API -> 重定向到 Google -> google 将用户数据返回到我的 api -> 我的 API 返回 JWT 令牌 -> Angular 使用令牌

你能帮我解决这个问题吗?

【问题讨论】:

问题出在您的 google 身份验证设置上,您没有提供 localhost:5000 作为回调 url。访问控制标头是响应而不是请求标头。 您的应用程序与您的 API 在同一主机上吗? 感谢先生的回答。我在谷歌控制台中添加了 localhost:5000 。 Api 端点和谷歌登录配置工作正常,因为如果我从浏览器调用 localhost:5000/api/authentication/externalLogin?provider=Google 我会得到成功的响应。我认为这种行为的原因是角码。这个请求发送正确吗? 不,不是。您应该从 google 1st 获取令牌,然后在授权标头中传递令牌 或者使用你网站发送的cookie认证后 【参考方案1】:

问题似乎是,虽然服务器正在发送 302 响应(url 重定向),但 Angular 正在发出 XMLHttpRequest,但它没有重定向。有更多人有这个问题...

对于我试图在前端拦截响应以进行手动重定向或更改服务器上的响应代码(这是一个“挑战”响应..)不起作用。

所以我所做的就是在 Angular 中将 window.location 更改为后端服务,以便浏览器可以管理响应并正确进行重定向。

注意:在文章的最后,我解释了一个更直接的 SPA 应用程序解决方案,无需使用 cookie 或 AspNetCore 身份验证。

完整的流程是这样的:

(1) Angular 将浏览器位置设置为 API -> (2) API 发送 302 响应 -> (3) 浏览器重定向到 Google -> (4) Google 将用户数据作为 cookie 返回给 API -> (5) API 返回 JWT 令牌 -> (6) Angular 使用令牌

1.- Angular 将浏览器位置设置为 API。我们将提供程序和 returnURL 传递给我们希望 API 在流程结束时返回 JWT 令牌的位置。

import  DOCUMENT  from '@angular/common';
...
 constructor(@Inject(DOCUMENT) private document: Document, ...)  
...
  signInExternalLocation() 
    let provider = 'provider=Google';
    let returnUrl = 'returnUrl=' + this.document.location.origin + '/register/external';

    this.document.location.href = APISecurityRoutes.authRoutes.signinexternal() + '?' + provider + '&' + returnUrl;
  

2.- API 发送 302 挑战响应。我们使用提供者和我们希望 Google 回电给我们的 URL 创建重定向。

// GET: api/auth/signinexternal
[HttpGet("signinexternal")]
public IActionResult SigninExternal(string provider, string returnUrl)

    // Request a redirect to the external login provider.
    string redirectUrl = Url.Action(nameof(SigninExternalCallback), "Auth", new  returnUrl );
    AuthenticationProperties properties = _signInMgr.ConfigureExternalAuthenticationProperties(provider, redirectUrl);

    return Challenge(properties, provider);

5.- API 接收 google 用户数据并返回 JWT 令牌。 在查询字符串中,我们将拥有 Angular 返回 URL。在我的情况下,如果用户未注册,我正在做一个额外的步骤来请求许可。

// GET: api/auth/signinexternalcallback
[HttpGet("signinexternalcallback")]
public async Task<IActionResult> SigninExternalCallback(string returnUrl = null, string remoteError = null)

    //string identityExternalCookie = Request.Cookies["Identity.External"];//do we have the cookie??

    ExternalLoginInfo info = await _signInMgr.GetExternalLoginInfoAsync();

    if (info == null)  return new RedirectResult($"returnUrl?error=externalsigninerror");

    // Sign in the user with this external login provider if the user already has a login.
    Microsoft.AspNetCore.Identity.SignInResult result = 
        await _signInMgr.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true);

    if (result.Succeeded)
    
        CredentialsDTO credentials = _authService.ExternalSignIn(info);
        return new RedirectResult($"returnUrl?token=credentials.JWTToken");
    

    if (result.IsLockedOut)
    
        return new RedirectResult($"returnUrl?error=lockout");
    
    else
    
        // If the user does not have an account, then ask the user to create an account.

        string loginprovider = info.LoginProvider;
        string email = info.Principal.FindFirstValue(ClaimTypes.Email);
        string name = info.Principal.FindFirstValue(ClaimTypes.GivenName);
        string surname = info.Principal.FindFirstValue(ClaimTypes.Surname);

        return new RedirectResult($"returnUrl?error=notregistered&provider=loginprovider" +
            $"&email=email&name=name&surname=surname");
    

用于注册额外步骤的 API(对于此调用,Angular 必须使用“WithCredentials”发出请求才能接收 cookie):

[HttpPost("registerexternaluser")]
public async Task<IActionResult> ExternalUserRegistration([FromBody] RegistrationUserDTO registrationUser)

    //string identityExternalCookie = Request.Cookies["Identity.External"];//do we have the cookie??

    if (ModelState.IsValid)
    
        // Get the information about the user from the external login provider
        ExternalLoginInfo info = await _signInMgr.GetExternalLoginInfoAsync();

        if (info == null) return BadRequest("Error registering external user.");

        CredentialsDTO credentials = await _authService.RegisterExternalUser(registrationUser, info);
        return Ok(credentials);
    

    return BadRequest();


SPA 应用程序的不同方法:

当我完成它的工作时,我发现对于 SPA 应用程序有更好的方法(https://developers.google.com/identity/sign-in/web/server-side-flow、Google JWT Authentication with AspNet Core 2.0、https://medium.com/mickeysden/react-and-google-oauth-with-net-core-backend-4faaba25ead0)

对于这种方法,流程是:

(1) Angular 打开 google 身份验证 -> (2) 用户身份验证 -> (3) Google 将 googleToken 发送到 angular -> (4) Angular 将其发送到 API -> (5) API 验证它针对谷歌并返回 JWT 令牌 -> (6) Angular 使用令牌

为此,我们需要在 Angular 中安装“angularx-social-login”npm 包,并在 netcore 后端安装“Google.Apis.Auth”NuGet 包

1.和 4. - Angular 打开 google 身份验证。我们将使用 angularx-social-login 库。用户在 Angular 中唱歌后将 googletoken 发送到 API

login.module.ts 我们添加:

let config = new AuthServiceConfig([
  
    id: GoogleLoginProvider.PROVIDER_ID,
    provider: new GoogleLoginProvider('Google ClientId here!!')
  
]);

export function provideConfig() 
  return config;


@NgModule(
  declarations: [
...
  ],
  imports: [
...
  ],
  exports: [
...
  ],
  providers: [
    
      provide: AuthServiceConfig,
      useFactory: provideConfig
    
  ]
)

在我们的 login.component.ts 上:

import  AuthService, GoogleLoginProvider  from 'angularx-social-login';
...
  constructor(...,  private socialAuthService: AuthService)
...

  signinWithGoogle() 
    let socialPlatformProvider = GoogleLoginProvider.PROVIDER_ID;
    this.isLoading = true;

    this.socialAuthService.signIn(socialPlatformProvider)
      .then((userData) => 
        //on success
        //this will return user data from google. What you need is a user token which you will send it to the server
        this.authenticationService.googleSignInExternal(userData.idToken)
          .pipe(finalize(() => this.isLoading = false)).subscribe(result => 

            console.log('externallogin: ' + JSON.stringify(result));
            if (!(result instanceof SimpleError) && this.credentialsService.isAuthenticated()) 
              this.router.navigate(['/index']);
            
        );
      );
  

在我们的 authentication.service.ts 上:

  googleSignInExternal(googleTokenId: string): Observable<SimpleError | ICredentials> 

    return this.httpClient.get(APISecurityRoutes.authRoutes.googlesigninexternal(), 
      params: new HttpParams().set('googleTokenId', googleTokenId)
    )
      .pipe(
        map((result: ICredentials | SimpleError) => 
          if (!(result instanceof SimpleError)) 
            this.credentialsService.setCredentials(result, true);
          
          return result;

        ),
        catchError(() => of(new SimpleError('error_signin')))
      );

  

5.- API 对 google 进行验证并返回 JWT 令牌。我们将使用“Google.Apis.Auth”NuGet 包。我不会为此提供完整代码,但请确保在验证 de 令牌时将受众添加到安全登录设置中:

 private async Task<GoogleJsonWebSignature.Payload> ValidateGoogleToken(string googleTokenId)
    
        GoogleJsonWebSignature.ValidationSettings settings = new GoogleJsonWebSignature.ValidationSettings();
        settings.Audience = new List<string>()  "Google ClientId here!!" ;
        GoogleJsonWebSignature.Payload payload = await GoogleJsonWebSignature.ValidateAsync(googleTokenId, settings);
        return payload;
    

【讨论】:

我真的很喜欢这个替代想法,我试图让它工作,但在通过 Authentication.SignIn 等登录用户后,我在服务器端卡住了。我无法得到它像使用默认 GrantResourceOwnerCredentials 时一样返回令牌信息 @Javi 我所做的一切都与您在第一种方法中描述的相同,但我不断收到Correlation failed,从这里Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler&lt;TOptions&gt;.HandleRequestAsync()。你可能知道出了什么问题吗? @Javi 我正在尝试实现第二种方法,但我也为我自己的用户 ID、密码身份验证目的实现了 jwt 承载,一旦我在 Angular 应用程序中从谷歌获得 idToken 然后我就没有得到那个如何在后续请求中使用 google 凭据对用户进行身份验证。在我的应用程序中,用户可以选择创建自己的用户 ID、密码或使用 google 注册。所以在我的启动中,我提到了 jwtauthentication。如果我在标头中传递 jwt 创建的令牌,它会进行身份验证,但是如何告诉 api 也验证 google 令牌。 嗨@Mr.Jay,使用这种方法,您只需验证一次google idToken,如果有效,您将返回您自己的应用程序JWT,因此在后续请求中您只使用您自己的应用程序令牌。如果您的令牌存在足够长的时间,则用户可能已经更改了他的 Google 凭据,但仍然登录到您的应用程序。无论如何请记住,Google 令牌仅存在 1 小时,如果您的应用需要在此之后访问 Google api,您应该使用刷新令牌。 @Mr.Jay,关于谷歌的刷新令牌,我还没有实现它,但流程发生了变化。现在在 Angular 应用程序中,您应该获得一个身份验证代码(您必须在提供程序配置中将 offline_access 配置参数设置为 true --> 'provider: new GoogleLoginProvider(environment.googleClientId, offline_access: true)'),然后在后端使用 Dmitry Komar 发布的 GoogleAuthorizationCodeFlow,它将为您提供访问令牌和刷新令牌,让您可以访问更多令牌。【参考方案2】:

只想从 Jevi 的回答中澄清第 5 部分,因为我花了一些时间才弄清楚如何使用 access_code 获取 google access_token。这是一个完整的服务器方法。 redirectUrl 应该等于来自 Google Console API 的“Authorized javascript origins”中的一。 “授权重定向 URI”可以为空。

[HttpPost("ValidateGoogleToken")]
    public async Task<GoogleJsonWebSignature.Payload> ValidateGoogleToken(string code)
    
        IConfigurationSection googleAuthSection = _configuration.GetSection("Authentication:Google");

        var flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
        
            ClientSecrets = new ClientSecrets
            
                ClientId = googleAuthSection["ClientId"],
                ClientSecret = googleAuthSection["ClientSecret"]
            
        );

        var redirectUrl = "http://localhost:6700";
        var response = await flow.ExchangeCodeForTokenAsync(string.Empty, code, redirectUrl, CancellationToken.None);

        GoogleJsonWebSignature.ValidationSettings settings = new GoogleJsonWebSignature.ValidationSettings
        
            Audience = new List<string>() googleAuthSection["ClientId"]
        ;

        var payload = await GoogleJsonWebSignature.ValidateAsync(response.IdToken, settings);
        return payload;
    

【讨论】:

【参考方案3】:

我想这对你们会有帮助。

import  Injectable, Inject  from '@angular/core';
import  DOCUMENT  from '@angular/common';
@Injectable()
export class LoginService 

constructor(@Inject(DOCUMENT) private document: Document,...)
    login() 
         this.document.location.href = 'https://www.mywebsite.com/account/signInWithGoogle';
    

https://www.blinkingcaret.com/2018/10/10/sign-in-with-an-external-login-provider-in-an-angular-application-served-by-asp-net-core/

【讨论】:

正如目前所写,您的答案尚不清楚。请edit 添加其他详细信息,以帮助其他人了解这如何解决所提出的问题。你可以找到更多关于如何写好答案的信息in the help center。【参考方案4】:

我遇到了类似的问题,既然你说你已经在后端设置了 CORS,Angular 没有在 API 请求中添加凭据可能是问题所在。当您在 url 栏中键入 api 端点时浏览器会执行的操作。您可以使用角度拦截器在每个请求中添加凭据。检查这个:https://angular.io/guide/http#intercepting-requests-and-responses

对于您的特定情况,这可能有效:

export class CookieInterceptor implements HttpInterceptor 

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> 
    request = request.clone(
      withCredentials: true
    );
    return next.handle(request);
  

【讨论】:

【参考方案5】:

我有几点要补充:

    我已经检查了@Nehuen Antiman 的答案,它对我部分有效。

    按照他的建议创建这样的 intereptor 是一种很好的做法,但如果您只是将“withCredentials”标志添加到您的 service.ts 中也可以:

    public loginWithGoogle() 
      return this.http.get<any>(`$environment.api.apiUrl$environment.api.authenticationexternalLogin`,
      
        params: new HttpParams().set('provider', 'Google'),
        headers: new HttpHeaders()
          .set('Access-Control-Allow-Headers', 'Content-Type')
          .set('Access-Control-Allow-Methods', 'GET')
          .set('Access-Control-Allow-Origin', '*'),
        withCredentials: true
      )
      .pipe(map(data => 
        return data;
      ));
    
    

    还请记住将AllowCredentials() 方法添加到您的CorsOptions。这是我的代码中的示例:

    services.AddCors(options =>
    
        options.AddPolicy(AllowedOriginsPolicy,
        builder =>
        
            builder.WithOrigins("http://localhost:4200")
                .AllowAnyHeader()
                .AllowAnyMethod()
                .AllowCredentials();
        );
    );
    

【讨论】:

以上是关于谷歌在 Angular 7 中使用 .NET Core API 登录的主要内容,如果未能解决你的问题,请参考以下文章

谷歌在谷歌浏览器中阻止了网站/域

谷歌在 Android 上使用集群映射自定义标记图标

PJzhang:谷歌在中国大陆可以使用的部分服务

为啥谷歌在我得到的美元上花了 0.3 [关闭]

Android Drawable Mipmap Vector使用及Vector兼容

谷歌在 NestJS 后端使用 PassportStrategy 登录无法由 React WebApp 调用