angular - JWT 身份验证令牌在 Angular 和 .Net Core 中具有无效的 Audience 和 Issuer 以及缺少范围
问题描述
我一直致力于将我的 Angular 和 .Net Core 应用程序发布到 OpenShift Container Platform。我已将 JWT 授权令牌配置为对用户进行身份验证并获取 User.Identity.Name 信息。
我使用 Angular 作为前端,使用 .Net Core 3.1 作为后端。在本地一切正常,但是当我尝试将我的应用程序发布到 OpenShift 时,它给了我错误的受众和发行人信息。它也没有给我令牌的任何范围。
我对其进行了故障排除,发现我从部署到 OpenShift 的应用程序中获得的令牌是 v2.0,而它在本地为我提供了 v1.0 令牌。
任何帮助将不胜感激。
这是我的 app.componenet.ts 文件。
import { Component, OnInit, OnDestroy } from '@angular/core';
import { BroadcastService, MsalService } from '@azure/msal-angular';
import { Subscription } from 'rxjs/Subscription';
@Component
({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy
{
loggedIn: boolean;
private subscription: Subscription;
public isIframe: boolean;
token: string;
// --------------------------------------------------------------------
constructor(private broadcastService: BroadcastService, private authService: MsalService) {
}
// --------------------------------------------------------------------
ngOnInit()
{
this.broadcastService.subscribe("msal:loginSuccess", (payload) => {
localStorage.setItem('token', JSON.stringify(payload));
this.token = JSON.parse(localStorage.getItem('token'))['_token'];
this.loggedIn = true;
this.checkoutAccount();
});
}
// --------------------------------------------------------------------
checkoutAccount() {
this.loggedIn = !!this.authService.getAccount();
if (this.m_router.url == "/")
{
if (!this.m_dbService.isAuthorizedUser())
{
this.setUserAndRoles();
}
}
}
// --------------------------------------------------------------------
login() {
const isIE = window.navigator.userAgent.indexOf('MSIE ') > -1 || window.navigator.userAgent.indexOf('Trident/') > -1;
if (isIE) {
this.authService.loginRedirect();
} else {
this.authService.loginPopup();
}
}
// --------------------------------------------------------------------
logout() {
this.authService.logout();
}
// --------------------------------------------------------------------
ngOnDestroy() {
this.broadcastService.getMSALSubject().next(1);
if (this.subscription) {
this.subscription.unsubscribe();
}
}
}
这是我的 app.module.ts 文件。
// Modules
import { NgModule, ErrorHandler, forwardRef } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; // new
import { HttpModule } from '@angular/http';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
// Routing
import { HashLocationStrategy, LocationStrategy, Location } from '@angular/common';
// Components
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
// Authentication
import { MsalModule, MsalInterceptor, MsalAngularConfiguration, MSAL_CONFIG, MSAL_CONFIG_ANGULAR, MsalService } from '@azure/msal-angular';
import { Logger, Configuration } from 'msal';
import { environment } from '../environments/environment';
export function loggerCallback(logLevel, message, piiEnabled) {
console.log('client logging' + message);
}
export const protectedResourceMap: Map<string, string[]> = new Map([
[environment.apiUrl, ['api://azure-app-client-id/api_consume']]
]);
const isIE = window.navigator.userAgent.indexOf('MSIE ') > -1 || window.navigator.userAgent.indexOf('Trident/') > -1;
@NgModule
({
imports:
[
BrowserModule
,BrowserAnimationsModule
,ToastrModule.forRoot()
,AppRoutingModule
,SharedModule
,CoreModule.forRoot()
,FwModule
,HttpModule
,HttpClientModule
,MsalModule.forRoot({
auth: {
clientId: environment.clientId,
authority: environment.authority,
redirectUri: environment.redirectUrl
},
framework: {
protectedResourceMap: protectedResourceMap
}
},
{
consentScopes: ['openid', 'profile', 'api://azure-app-client-id/api_consume'],
}
)
]
,declarations:
[
AppComponent
]
,exports:
[
]
,providers:
[
Globals
,CookieService
,AcErrorService
,{provide: ErrorHandler, useClass: GlobalErrorHandlerService}
,Location, {provide: LocationStrategy, useClass: HashLocationStrategy},
{
provide: HTTP_INTERCEPTORS,
useClass: MsalInterceptor,
multi: true
}
]
,bootstrap: [AppComponent]
})
export class AppModule { }
这是我的 environment.ts 文件。
export const environment = {
production: false,
clientId: 'my-app-registration-client-id',
authority: 'https://login.microsoftonline.com/my-app-registration-tenant-id',
redirectUrl: 'http://localhost:4200/',
apiUrl: 'http://localhost:5000/'
};
这是我的 environment.prod.ts 文件。
export const environment = {
production: true,
clientId: 'my-app-registration-client-id',
authority: 'https://login.microsoftonline.com/my-app-registration-tenant-id',
redirectUrl: 'my-openshift-app-url',
apiUrl: 'my-openshift-app-url'
};
这是我的 database.service.ts 文件。
export class DatabaseService
{
constructor
(
public m_httpClient: HttpClient
)
{}
public get(NetCoreApiPath): Observable<any>
{
var strUrl;
try
{
strUrl = this.m_globals.getBaseUrl() + p_strApiPath;
return this.m_httpClient.get<any>(strUrl, { withCredentials: true })
.pipe(map(data => data), catchError(this.handleError));
}
catch(err)
{
console.error(err);
this.sendErrorEmail(err.message, "1");
}
}
}
这是我的 .Net Core appsettings.json 文件。
{
"ConnectionStrings": {
"DefaultConnection": "Server=my-db-server,1433;Initial Catalog=my-db;Integrated Security=true;"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "https://MyOrg.onmicrosoft.com/",
"TenantId": "my-app-registration-tenant-id",
"Authority": "https://sts.windows.net/my-app-registration-tenant-id/",
"Audience": "api://my-app-registration-client-id",
"ClientId": "my-app-registration-client-id",
}
}
这是我的 .Net Core startup.cs 文件。
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore;
using API.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
using Newtonsoft.Json;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
namespace API
{
public class Startup
{
private readonly IWebHostEnvironment _environment;
public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
Configuration = configuration;
_environment = env;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
Configuration.Bind("AzureAd", options);
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true
};
});
services.AddCors(options =>
{
options.AddPolicy("AllowCors",
builder => builder
.WithOrigins("http://localhost:4200", "my-openshift-app-url")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
);
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
services
.AddMvc(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
options.MaxIAsyncEnumerableBufferLimit = int.MaxValue;
})
.AddNewtonsoftJson(options =>
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore
);
var connectionString = Configuration["ConnectionStrings:DefaultConnection"];
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Production")
services.AddDbContext<AppDB>(options =>
options.UseSqlServer(connectionString));
else
services.AddDbContext<AppDB>(options =>
options.UseSqlServer(connectionString));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors("AllowCors");
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
}
}
}
这是我的控制器类。
using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Web.Http;
using Microsoft.AspNetCore.Http.Extensions;
using System.Net;
using Microsoft.AspNetCore.Authorization;
using API.Helpers;
namespace API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class EmployeeStatusController : ControllerBase
{
private string m_strUserName;
private readonly AppDB db;
public EmployeeStatusController(AppDB context)
{
db = context;
}
[Authorize]
[HttpGet]
public IQueryable<EmployeeStatus> GetEmployeeStatus()
{
try
{
this.m_strUserName = UserHelper.getUserNameOrAlias(User);
IQueryable<EmployeeStatus> data =
from tbl in db.EmployeeStatus
select tbl;
// ----- /get -----
return data;
}
catch (Exception ex)
{
if (m_strError.Length == 0)
{
if (this.showException())
{
m_strError = ex.ToString();
}
}
sendEmail(this.m_strError + ": " + ex.ToString());
throw new System.Web.Http.HttpResponseException(HttpStatusCode.Forbidden);
}
}
}
}
这是我的 UserHelper.cs 类。
namespace API.Helpers
{
public static class UserHelper
{
public static string getUserName(IPrincipal user)
{
return user.Identity.IsAuthenticated ? user.Identity.Name.ToLower() : "";
}
}
}
这是我在本地得到的令牌结果。
{
"aud": "api://client-id",
"iss": "https://sts.windows.net/tenant-id/",
"iat": ,
"nbf": ,
"exp": ,
"acr": "1",
"aio": "",
"amr": [
""
],
"appid": "client-id",
"appidacr": "0",
"family_name": "",
"given_name": "",
"in_corp": "true",
"ipaddr": "",
"name": "",
"oid": "",
"onprem_sid": "",
"rh": "",
"scp": "api_consume",
"sub": "",
"tid": "tenant-id",
"unique_name": "",
"upn": "",
"uti": "",
"ver": "1.0"
}
这是我将应用程序发布到 OpenShift 后得到的令牌。
{
"aud": "client-id",
"iss": "https://login.microsoftonline.com/tenant-id/v2.0",
"iat": ,
"nbf": ,
"exp": ,
"acct": 0,
"aio": "",
"auth_time":
"ctry": "",
"email": "",
"family_name": "",
"given_name": "",
"in_corp": "true",
"ipaddr": "",
"name": "",
"nonce": "",
"oid": "",
"onprem_sid": "",
"preferred_username": "",
"rh": "",
"sid": "",
"sub": "",
"tenant_ctry": "",
"tenant_region_scope": "",
"tid": "tenant-id",
"upn": "",
"uti": "",
"ver": "2.0",
"verified_primary_email": [
""
],
"verified_secondary_email": [
"",
""
],
"xms_tpl": "en"
}
你可以看到我得到的两个令牌有不同的受众、发行者和版本。可能是我缺少一些小配置,但在此阶段的任何帮助将不胜感激。
解决方案
对于本地和生产,您是否有不同的租户 ID?如果是,那么检查应用程序的清单(portal.azure.com -> Azure Active Directory -> 应用程序注册 -> 搜索您的应用程序 -> 清单)并在第 4 行查看属性可能是值得的accessTokenAcceptedVersion
。如果值为null
或1
,则令牌将由 sts.windows.net 颁发。如果值为2
,则将由 login.microsoftonline.com 发出。
改变可能需要时间,虽然......
推荐阅读
- windows - 如何使用 Ansible 管理 Windows 服务器?
- svg - 以动态内容作为背景图像的 SVG
- elixir - “获取或插入”的更惯用方式是什么?
- scintilla - 如何突出显示闪烁/ scite中所有出现的选定单词?
- bash - 如何通过下划线拆分字符串并将元素提取为bash中的变量?
- regex - 如果存在正则表达式匹配,则忽略前缀,然后继续匹配
- html - 如何让我的图像出现在我的 HTML/CSS 中?
- python - Django 规则设置问题:自定义用户模型的 has_perm 始终为 False
- hbase - hbase 命令将区域拆分并移动到不同的区域服务器
- mysql - MYSQL 加载数据