我有一个使用 Asp.Net Core 后端的 Angular 4 SPA。我将 OpenIddict 与 JWT 令牌一起使用,身份验证工作正常并返回一个令牌。只要我不使用 [Authorize] 注释和控制器操作,应用程序就可以正常运行。当我这样做时,它总是返回 401。
老实说,我什至不确定授权应该如何工作。我假设当 Bearer 令牌被提供给一个用 [Authorize] 修饰的请求时,中间件会自动处理它。
我确实看到有一个 EnableAuthorizationEndpoint 可用,所以我使用了它,但该方法永远不会被调用。所以我不确定我应该做什么,所以我将在这里展示我所做的事情,也许有人会足够亲切地为我指明正确的方向。
所以这就是我目前正在做的事情。首先,这是我的 Angular 登录代码。
login(username: string, password: string): Observable<boolean> {
var url = this.apiUrl + '/connect/token';
var body =
`username=${username}&password=${password}&grant_type=password&scope=role`;
let headers: Headers = new Headers();
headers.append('Content-Type', 'application/x-www-form-urlencoded');
return this.http.post(url, body , { headers: headers })
.map((response: Response) => {
// login successful if there's a jwt token in the response
let token = response.json() && response.json().access_token;
if (token) {
// set token property
this.token = token;
this.username = username;
// store username and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('token', token);
localStorage.setItem('username', username);
// return true to indicate successful login
return true;
} else {
// return false to indicate failed login
return false;
}
});
}
这会产生一个我可以解析并且看起来是正确的令牌。
这是生成的令牌:
eyJhbGciOiJSUzI1NiIsImtpZCI6IkJEODE3RjE4NUVCRDM0MkQ0Q0NGNTgzNThFMUY3MThFMDkwRjk5MzYiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJGQjRFOUQ3Mi01QkQ4LTREM0ItOTc3QS0zMEIyRTU0NjI0MTkiLCJuYW1lIjoibWFydGluaG9ydG9uIiwicm9sZSI6WyJGYW4iLCJTUEZDQ2hpZWZzIiwiQ01TIEFkbWluIl0sInRva2VuX3VzYWdlIjoiYWNjZXNzX3Rva2VuIiwianRpIjoiYzhkODAzOWMtMzExNy00MGFjLWJmMjAtYTZlZTNlM2NlNzI5IiwiYXVkIjoicmVzb3VyY2Utc2VydmVyIiwibmJmIjoxNTAxNjQ3OTg1LCJleHAiOjE1MDE2NTE1ODUsImlhdCI6MTUwMTY0Nzk4NSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MzI0NC8ifQ.QLXt_IVEvat27Ut1OjBBMOPCTTULxXjmlg1skgI8gP6teE3BZLm3yzAzY9dyMeNKXli7dBMVh-PLwk_D0BRXrSTsm_Ufdc5f5z2hEnjhRA3rRM_nn8MxNLQ9RMAVLxBXyg_oyI9h2i_JX0LkqmNdn1ZiJ90_FCJ38vGXiCr9SAc7F47S3QqrI_gHqS-4lnurozj3zH0dzsxE2hCAiSMfHtu9WsFV7lCPONT9WsqX6muEtuJQaxmfcrRzhwFXutyso1v-iTtVnHukNkja9FnjVAt-arNSSAqS4GBmZjC9KOdrZ7fPE83yQXJLEeh7Wn1tIY-nebETu106fg5Zn5vdyAfR6wGAESbWg9FVt8QIlO06Cbq6Yubark-m3TlyXXBOv8-SLgv8I99nhra2bVsHAi2GeDKpmfdLPYmqiGsogztVJY-mte9WqQb25fYS-MfErQqzzxHnFxd8cy_lW_YFNyLVAfX1BTbQpuWRi_hvXqvX1vXHn-372s8JBUdii49udi081DXIUZAX2E0cRFt_5CreR_TR4fRDkzks4jyP3Qho2CEzM691s_V9n-orVxgOjDYd8U18h6Uswb8Xz2FU8knSCHjrjp8Vwc8s0A_b8KvkNFhODJ_f8mIS7glsjTGW3uts6J_gcoUbXy0MnizqKpMk0hTN4-3eOXemMny3Vyk
我将 angular2-jwt 用于所有 API 调用。配置如下:
export function authHttpServiceFactory(http: Http, options: RequestOptions) {
return new AuthHttp(new AuthConfig({
noJwtError: true,
tokenName: 'token',
tokenGetter: () => localStorage.getItem('token'),
globalHeaders : [{'Content-Type': 'application/json'}]
}), http, options);
}
浏览器中的检查显示所有 XHR 请求都按预期形成。接下来是启动代码。
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddDbContext<IdentityContext>(options => {
//options.UseSqlite(Configuration.GetConnectionString("FileConnection"));
options.UseSqlServer(Configuration.GetConnectionString("SqlConnection"));
options.UseOpenIddict();
});
services.AddCors();
services.AddOptions();
services.Configure<SIOptions>(Configuration);
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.Cookies.ApplicationCookie.AutomaticChallenge = false;
})
/*services.AddIdentity<ApplicationUser, IdentityRole>()*/
.AddEntityFrameworkStores<IdentityContext>()
.AddDefaultTokenProviders();
// Configure Identity to use the same JWT claims as OpenIddict instead
// of the legacy WS-Federation claims it uses by default (ClaimTypes),
// which saves you from doing the mapping in your authorization controller.
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
options.Cookies.ApplicationCookie.LoginPath = "";
options.Cookies.ApplicationCookie.Events = new CookieAuthenticationEvents
{
OnRedirectToLogin = ctx =>
{
if (ctx.Request.Path.StartsWithSegments("/api"))
{
//ctx.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
else
{
ctx.Response.Redirect(ctx.RedirectUri);
}
return Task.FromResult(0);
}
};
});
services.AddOpenIddict()
// Register the Entity Framework stores.
.AddEntityFrameworkCoreStores<IdentityContext>()
// Register the ASP.NET Core MVC binder used by OpenIddict.
// Note: if you don't call this method, you won't be able to
// bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
.AddMvcBinders()
// Enable the token endpoint.
.EnableTokenEndpoint("/connect/token")
.UseJsonWebTokens()
.AddSigningCertificate(new System.Security.Cryptography.X509Certificates.X509Certificate2(@"C:\Program Files (x86)\Windows Kits\10\bin\10.0.15063.0\x64\SIWWW.pfx", "Test123"))
//options.AddEphemeralSigningKey();
// Enable the password flow.
.AllowPasswordFlow()
// During development, you can disable the HTTPS requirement.
.DisableHttpsRequirement()
.AllowAuthorizationCodeFlow()
.EnableAuthorizationEndpoint("/connect/authorize");
;
services.AddMvc();
services.AddSingleton<IConfiguration>(Configuration);
// Add application services.
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
services.AddScoped<IPasswordHasher<ApplicationUser>, SqlPasswordHasher>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions {
HotModuleReplacement = true
});
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseCors(builder =>
builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()
);
app.UseStaticFiles();
app.UseIdentity();
app.UseOAuthValidation();
app.UseOpenIddict();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
app.MapWhen(x => !x.Request.Path.Value.StartsWith("/api"), builder =>
{
builder.UseMvc(routes =>
{
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Home", action = "Index" });
});
});
}
}
```
Here is my authentication method.
```csharp
[HttpPost("~/connect/token"), Produces("application/json")]
public async Task<IActionResult> Exchange(OpenIdConnectRequest request)
{
Debug.Assert(request.IsTokenRequest(),
"The OpenIddict binder for ASP.NET Core MVC is not registered. " +
"Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null)
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the user is allowed to sign in.
if (!await _signInManager.CanSignInAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Reject the token request if two-factor authentication has been enabled by the user.
if (_userManager.SupportsUserTwoFactor && await _userManager.GetTwoFactorEnabledAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Ensure the user is not already locked out.
if (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user))
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the password is valid.
if (!await _userManager.CheckPasswordAsync(user, request.Password))
{
if (_userManager.SupportsUserLockout)
{
await _userManager.AccessFailedAsync(user);
}
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
if (_userManager.SupportsUserLockout)
{
await _userManager.ResetAccessFailedCountAsync(user);
}
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(request, user);
var result = SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
return result;
}
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
ErrorDescription = "The specified grant type is not supported."
});
}
private async Task<AuthenticationTicket> CreateTicketAsync(OpenIdConnectRequest request, ApplicationUser user)
{
// Create a new ClaimsPrincipal containing the claims that
// will be used to create an id_token, a token or a code.
var principal = await _signInManager.CreateUserPrincipalAsync(user);
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(principal,
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
// Set the list of scopes granted to the client application.
ticket.SetScopes(new[]
{
OpenIdConnectConstants.Scopes.OpenId,
OpenIdConnectConstants.Scopes.Email,
OpenIdConnectConstants.Scopes.Profile,
OpenIddictConstants.Scopes.Roles
}.Intersect(request.GetScopes()));
ticket.SetResources("resource-server");
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in ticket.Principal.Claims)
{
// Never include the security stamp in the access and identity tokens, as it's a secret value.
if (claim.Type == _identityOptions.Value.ClaimsIdentity.SecurityStampClaimType)
{
continue;
}
var destinations = new List<string>
{
OpenIdConnectConstants.Destinations.AccessToken
};
// Only add the iterated claim to the id_token if the corresponding scope was granted to the client application.
// The other claims will only be added to the access_token, which is encrypted when using the default format.
if ((claim.Type == OpenIdConnectConstants.Claims.Name && ticket.HasScope(OpenIdConnectConstants.Scopes.Profile)) ||
(claim.Type == OpenIdConnectConstants.Claims.Email && ticket.HasScope(OpenIdConnectConstants.Scopes.Email)) ||
(claim.Type == OpenIdConnectConstants.Claims.Role && ticket.HasScope(OpenIddictConstants.Claims.Roles)))
{
destinations.Add(OpenIdConnectConstants.Destinations.IdentityToken);
}
claim.SetDestinations(destinations);
}
return ticket;
}
最后,这是一个控制器方法,在没有 [Authorize] 的情况下可以正常工作,但否则返回 401。
[Authorize]
[HttpGet("{id}"), Produces("application/json")]
public IActionResult Get(int id)
{
using (SIDB db = new SIDB())
{
Exercises exer = db.Exercises.Include("Video").Where(ex => ex.Mode == 0 && ex.nExerciseId == id).Select(ex => ex).FirstOrDefault();
if (exer == null)
return NotFound();
return Json(new ExerciseReturnModel(exer, false));
}
}
控制器本身装饰有:
[Route("api/activities")]
我可能正在做一些非常愚蠢的事情,但我已经阅读了我能找到的所有内容,但我无法让它发挥作用。非常感谢任何帮助。