>服务器端(.NET Core OpenIdDict),包含构建您自己的详细说明:https://github.com/Myrmex/repro-oidang
>客户端(Angular2):https://github.com/Myrmex/repro-angoid
至于服务器端,我按照OpenIdDict提供的关于此流程的示例(https://github.com/openiddict/openiddict-samples/blob/master/samples/PasswordFlow).以下是Startup中最相关的位:
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddCors();
- services.AddEntityFrameworksqlServer()
- .AddDbContext<CatalogContext>(options =>
- options.UsesqlServer(Configuration.GetConnectionString("Catalog")))
- .AddDbContext<ApplicationDbContext>(options =>
- options.UsesqlServer(Configuration.GetConnectionString("Catalog")));
- services.AddIdentity<ApplicationUser,ApplicationRole>()
- .AddEntityFrameworkStores<ApplicationDbContext>()
- .AddDefaultTokenProviders();
- services.AddOpenIddict<ApplicationDbContext>()
- .DisableHttpsRequirement()
- .EnableTokenEndpoint("/connect/token")
- .EnablelogoutEndpoint("/connect/logout")
- .EnableUserinfoEndpoint("/connect/userinfo")
- .AllowPasswordFlow()
- .AllowRefreshTokenFlow()
- .AddEphemeralSigningKey();
- services.AddMvc()
- .AddJsonOptions(options =>
- {
- options.SerializerSettings.ContractResolver =
- new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();
- });
- // add my services
- // ...
- services.AddTransient<IDatabaseInitializer,DatabaseInitializer>();
- services.AddSwaggerGen();
- }
- public void Configure(IApplicationBuilder app,IHostingEnvironment env,ILoggerFactory loggerFactory,IDatabaseInitializer databaseInitializer)
- {
- loggerFactory.AddConsole(Configuration.GetSection("Logging"));
- loggerFactory.AddDebug();
- loggerFactory.AddNLog();
- app.UseDefaultFiles();
- app.UseStaticFiles();
- app.UseCors(builder =>
- builder.WithOrigins("http://localhost:4200")
- .AllowAnyHeader()
- .AllowAnyMethod());
- app.USEOAuthValidation();
- app.USEOpenIddict();
- app.UseMvc();
- databaseInitializer.Seed().GetAwaiter().GetResult();
- env.ConfigureNLog("nlog.config");
- app.UseSwagger();
- app.UseSwaggerUi();
- }
如果我用Fiddler测试它,它工作正常:令牌请求获取令牌,然后我可以将它包含在Authorization标头中以访问任何受保护的API,它按预期返回JSON数据.
示例令牌请求:
- POST http://localhost:51346/connect/token
- Content-Type: application/x-www-form-urlencoded
- grant_type=password&scope=offline_access profile email roles&resource=http://localhost:4200&username=...&password=...
样本资源请求:
- GET http://localhost:51346/api/values
- Content-Type: application/json
- Authorization: Bearer ...received token here...
然而,在客户端,每当我尝试相同的请求时,我都会收到401错误;看着日志,似乎Angular2 Http服务根本没有发送所需的头,因为我收到错误身份验证被跳过,因为没有收到持有者令牌(请参阅下面的更多日志条目).
检索一些资源的服务是这样的:
- import { Injectable } from '@angular/core';
- import { Http,Response } from '@angular/http';
- import { Observable } from 'rxjs/Observable';
- import { SettingsService } from './settings.service';
- import { AuthenticationService } from './authentication.service';
- export interface ICategory {
- id: string;
- name: string;
- }
- @Injectable()
- export class CategoryService {
- constructor(
- private _http: Http,private _settings: SettingsService,private _authService: AuthenticationService) { }
- public getCategories(): Observable<ICategory[]> {
- let url = this._settings.apiBaseUrl + 'categories';
- let options = {
- headers: this._authService.createAuthHeaders({
- 'Content-Type': 'application/json'
- })
- };
- return this._http.get(url,options).map((res: Response) => res.json())
- .catch((error: any) => Observable.throw(error.json().error || 'server error'));
- }
- }
帮助程序createAuthHeaders只获取表示Header(https://angular.io/docs/ts/latest/api/http/index/Headers-class.html)条目的某些属性,检索存储的标记,将Authentication条目附加到标题,然后返回:
- public createAuthHeaders(headers?: { [name: string]: any }): Headers {
- let auth = new Headers();
- if (headers) {
- for (let key in headers) {
- if (headers.hasOwnProperty(key)) {
- auth.append(key,headers[key]);
- }
- }
- }
- let tokenResult = this._localStorage.retrieve(this._settings.tokenStorageKey,true);
- if (tokenResult) {
- auth.append('Authentication','Bearer ' + tokenResult.access_token);
- }
- return auth;
- }
然而,当尝试将响应映射到JSON对象(JSON输入的意外结束)时,此请求获得401响应然后Angular抛出.
我必须补充说,一旦客户端获得令牌,它就会发出另一个请求,以检索用户信息,这样可以正常工作;这是它(获取用户信息后的代码):
- public login(name: string,password: string) {
- let body = 'grant_type=password&scope=offline_access profile email roles' +
- `&resource=${this._settings.appBaseUrl}&username=${name}&password=${password}`;
- this._http.post(
- this._settings.authBaseUrl + `token`,body,{
- headers: new Headers({
- 'Content-Type': 'application/x-www-form-urlencoded'
- })
- }).map(res => res.json())
- .subscribe(
- (token: ITokenResult) => {
- if (token.expires_in) {
- token.expires_on = this.calculateExpirationDate(+token.expires_in);
- }
- this._localStorage.store(this._settings.tokenStorageKey,token,true);
- // get user info
- this._http.get(this._settings.authBaseUrl + 'userinfo',{
- headers: new Headers({
- 'Content-Type': 'application/json','Authorization': 'Bearer ' + token.access_token
- })
- }).map(res => res.json())
- .subscribe((info: IUserInfoResult) => {
- let user: IUser = {
- id: info.name,email: info.email,name: info.name,firstName: info.given_name,lastName: info.family_name,role: info.role,verified: info.email_verified
- };
- this._localStorage.store(this._settings.userStorageKey,user,true);
- this.userChanged.emit(user);
- },error => {
- console.log(error);
- });
- },error => {
- console.log(error);
- });
- }
但是,使用上述服务构建的任何其他请求都会失败.使用引用函数构建的标题有什么问题?
以下是服务器端的一些日志条目:
- 2016-11-18 20:41:31.9815|0|AspNet.Security.OAuth.Validation.OAuthValidationMiddleware|DEBUG| Authentication was skipped because no bearer token was received.
- 2016-11-18 20:41:31.9815|0|OpenIddict.Infrastructure.OpenIddictProvider|INFO| The token request validation process was skipped because the client_id parameter was missing or empty.
- 2016-11-18 20:41:32.0715|0|AspNet.Security.OpenIdConnect.Server.OpenIdConnectServerMiddleware|INFO| No explicit audience was associated with the access token.
- 2016-11-18 20:41:32.1165|10|AspNet.Security.OpenIdConnect.Server.OpenIdConnectServerMiddleware|INFO| AuthenticationScheme: ASOS signed in.
- 2016-11-18 20:41:32.1635|3|AspNet.Security.OAuth.Validation.OAuthValidationMiddleware|INFO| HttpContext.User merged via AutomaticAuthentication from authenticationScheme: Bearer.
- 2016-11-18 20:41:57.7430|0|AspNet.Security.OAuth.Validation.OAuthValidationMiddleware|DEBUG| Authentication was skipped because no bearer token was received.
- 2016-11-18 20:41:57.7430|0|AspNet.Security.OAuth.Validation.OAuthValidationMiddleware|DEBUG| Authentication was skipped because no bearer token was received.
- 2016-11-18 20:41:57.8820|12|AspNet.Security.OAuth.Validation.OAuthValidationMiddleware|INFO| AuthenticationScheme: Bearer was challenged.
- 2016-11-18 20:41:57.9305|12|AspNet.Security.OAuth.Validation.OAuthValidationMiddleware|INFO| AuthenticationScheme: Bearer was challenged.
- 2016-11-18 20:41:57.9465|0|AspNet.Security.OAuth.Validation.OAuthValidationMiddleware|DEBUG| Authentication was skipped because no bearer token was received.
- 2016-11-18 20:41:57.9925|12|AspNet.Security.OAuth.Validation.OAuthValidationMiddleware|INFO| AuthenticationScheme: Bearer was challenged.
解决方法
- auth.append('Authentication','Bearer ' + tokenResult.access_token) // wrong
- auth.append('Authorization','Bearer ' + tokenResult.access_token) // right