1

我有一个使用 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")]

我可能正在做一些非常愚蠢的事情,但我已经阅读了我能找到的所有内容,但我无法让它发挥作用。非常感谢任何帮助。

4

3 回答 3

1

@Pinpoint 给出的解决方案是必要的,但还不够。此外,我必须改变我在控制器中装饰方法的方式。代替

[Authorize]

它必须是

[Authorize(ActiveAuthenticationSchemes = OAuthValidationDefaults.AuthenticationScheme)]

有没有办法让它成为默认值,而不必在每个方法上重复它?

于 2017-08-02T23:14:24.967 回答
0

When opting for JWT as the access token format, you can't use the aspnet-contrib validation middleware. 相反,您必须使用 JWT 中间件。

具体来说,您必须替换app.UseOAuthValidation();为:

app.UseJwtBearerAuthentication(new JwtBearerOptions
{
    Authority = "[url of your OpenIddict-based app]",
    Audience = "resource-server",
    RequireHttpsMetadata = false
});
于 2017-08-02T11:43:04.493 回答
0

要使用 JWT 令牌而不是默认加密格式,需要以下行。

options.UseJsonWebTokens();
options.AddEphemeralSigningKey();

我看到您的代码中已将 AddEphemeralSigningKey 注释掉。不确定您是否已经对此进行了测试。

希望这可以帮助 !

于 2017-08-02T05:12:56.943 回答