在ASP.NET Core中實現一個Token base的身份認證

注:本文提到的代碼示例下載地址> How to achieve a bearer token authentication and authorization in ASP.NET Corehtml

在ASP.NET Core中實現一個Token base的身份認證

之前在web端的身份認證都是基於Cookie | Session的身份認證, 在沒有更多的終端出現以前,這樣作也沒有什麼問題,
但在Web API時代,你所須要面對的就不止是瀏覽器了,還有各類客戶端,這樣就有了一個問題,這些客戶端是不知道cookie是什麼鬼的。 (cookie實際上是瀏覽器搞出來的小貓膩,用來保持會話的,但HTTP自己是無狀態的, 各類客戶端能提供的無非也就是HTTP操做的API)jquery

而基於Token的身份認證就是應對這種變化而生的,它更開放,安全性也更高。web

基於Token的身份認證有不少種實現方式,但咱們這裏只使用微軟提供的API。ajax

接下來的例子將帶領你們完成一個使用微軟JwtSecurityTokenHandler完成一個基於beare token的身份認證。json

注意:這種文章屬於Step by step教程,跟着作纔不至於看暈,下載完整代碼分析代碼結構纔有意義。api

前期準備

建立項目

在VS中新建項目,項目類型選擇ASP.NET Core Web Application(.NET Core), 輸入項目名稱爲CSTokenBaseAuth瀏覽器

Coding

注:添加下面的代碼時IDE會報代碼錯誤,這是由於尚未引用對用的包,進入報錯的這一行,點擊燈泡,加載對應的包就能夠了。安全

(圖文無關)cookie

  • 建立一些輔助類app

    在項目根目錄下建立一個文件夾Auth,並添加RSAKeyHelper.cs以及TokenAuthOption.cs兩個文件

    • 在RSAKeyHelper.cs中

      using System.Security.Cryptography;
      
      namespace CSTokenBaseAuth.Auth
      {
          public class RSAKeyHelper
          {
              public static RSAParameters GenerateKey()
              {
                  using (var key = new RSACryptoServiceProvider(2048))
                  {
                      return key.ExportParameters(true);
                  }
              }
          }
      }
    • 在TokenAuthOption.cs中

      using System;
      using Microsoft.IdentityModel.Tokens;
      
      namespace CSTokenBaseAuth.Auth
      {
          public class TokenAuthOption
          {
              public static string Audience { get; } = "ExampleAudience";
              public static string Issuer { get; } = "ExampleIssuer";
              public static RsaSecurityKey Key { get; } = new RsaSecurityKey(RSAKeyHelper.GenerateKey());
              public static SigningCredentials SigningCredentials { get; } = new SigningCredentials(Key, SecurityAlgorithms.RsaSha256Signature);
      
              public static TimeSpan ExpiresSpan { get; } = TimeSpan.FromMinutes(20);
          }
      }
  • Startup.cs

    在ConfigureServices中添加以下代碼:

    services.AddAuthorization(auth =>
    {
        auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
            .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme‌​)
            .RequireAuthenticatedUser().Build());
    });

    完整的代碼應該是這樣

    public void ConfigureServices(IServiceCollection services)
    {
        // Add framework services.
        services.AddApplicationInsightsTelemetry(Configuration);
        // Enable the use of an [Authorize("Bearer")] attribute on methods and classes to protect.
        services.AddAuthorization(auth =>
        {
            auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
                .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme‌​)
                .RequireAuthenticatedUser().Build());
        });
        services.AddMvc();
    }

    在Configure方法中添加以下代碼

    app.UseExceptionHandler(appBuilder => {
        appBuilder.Use(async (context, next) => {
            var error = context.Features[typeof(IExceptionHandlerFeature)] as IExceptionHandlerFeature;
            //when authorization has failed, should retrun a json message to client
            if (error != null && error.Error is SecurityTokenExpiredException)
            {
                context.Response.StatusCode = 401;
                context.Response.ContentType = "application/json";
                await context.Response.WriteAsync(JsonConvert.SerializeObject(
                    new { authenticated = false, tokenExpired = true }
                ));
            }
            //when orther error, retrun a error message json to client
            else if (error != null && error.Error != null)
            {
                context.Response.StatusCode = 500;
                context.Response.ContentType = "application/json";
                await context.Response.WriteAsync(JsonConvert.SerializeObject(
                    new { success = false, error = error.Error.Message }
                ));
            }
            //when no error, do next.
            else await next();
        });
    });

    這段代碼主要是Handle Error用的,好比當身份認證失敗的時候會拋出異常,而這裏就是處理這個異常的。

    接下來在相同的方法中添加以下代碼,

    app.UseExceptionHandler(appBuilder => {
        appBuilder.Use(async (context, next) => {
            var error = context.Features[typeof(IExceptionHandlerFeature)] as IExceptionHandlerFeature;
    
            //when authorization has failed, should retrun a json message to client
            if (error != null && error.Error is SecurityTokenExpiredException)
            {
                context.Response.StatusCode = 401;
                context.Response.ContentType = "application/json";
    
                await context.Response.WriteAsync(JsonConvert.SerializeObject(
                    new { authenticated = false, tokenExpired = true }
                ));
            }
            //when orther error, retrun a error message json to client
            else if (error != null && error.Error != null)
            {
                context.Response.StatusCode = 500;
                context.Response.ContentType = "application/json";
                await context.Response.WriteAsync(JsonConvert.SerializeObject(
                    new { success = false, error = error.Error.Message }
                ));
            }
            //when no error, do next.
            else await next();
        });
    });

    應用JwtBearerAuthentication

    app.UseJwtBearerAuthentication(new JwtBearerOptions {
        TokenValidationParameters = new TokenValidationParameters {
            IssuerSigningKey = TokenAuthOption.Key,
            ValidAudience = TokenAuthOption.Audience,
            ValidIssuer = TokenAuthOption.Issuer,
            ValidateIssuerSigningKey = true,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromMinutes(0)
        }
    });

    完整的代碼應該是這樣

    using System;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Logging;
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Authentication.JwtBearer;
    using CSTokenBaseAuth.Auth;
    using Microsoft.AspNetCore.Diagnostics;
    using Microsoft.IdentityModel.Tokens;
    using Microsoft.AspNetCore.Http;
    using Newtonsoft.Json;
    
    namespace CSTokenBaseAuth
    {
        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);
    
                if (env.IsEnvironment("Development"))
                {
                    // This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately.
                    builder.AddApplicationInsightsSettings(developerMode: true);
                }
    
                builder.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.AddApplicationInsightsTelemetry(Configuration);
    
                // Enable the use of an [Authorize("Bearer")] attribute on methods and classes to protect.
                services.AddAuthorization(auth =>
                {
                    auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
                        .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme‌​)
                        .RequireAuthenticatedUser().Build());
                });
    
                services.AddMvc();
            }
    
            // 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();
    
                app.UseApplicationInsightsRequestTelemetry();
    
                app.UseApplicationInsightsExceptionTelemetry();
    
                #region Handle Exception
                app.UseExceptionHandler(appBuilder => {
                    appBuilder.Use(async (context, next) => {
                        var error = context.Features[typeof(IExceptionHandlerFeature)] as IExceptionHandlerFeature;
    
                        //when authorization has failed, should retrun a json message to client
                        if (error != null && error.Error is SecurityTokenExpiredException)
                        {
                            context.Response.StatusCode = 401;
                            context.Response.ContentType = "application/json";
    
                            await context.Response.WriteAsync(JsonConvert.SerializeObject(
                                new { authenticated = false, tokenExpired = true }
                            ));
                        }
                        //when orther error, retrun a error message json to client
                        else if (error != null && error.Error != null)
                        {
                            context.Response.StatusCode = 500;
                            context.Response.ContentType = "application/json";
                            await context.Response.WriteAsync(JsonConvert.SerializeObject(
                                new { success = false, error = error.Error.Message }
                            ));
                        }
                        //when no error, do next.
                        else await next();
                    });
                });
                #endregion
    
                #region UseJwtBearerAuthentication
                app.UseJwtBearerAuthentication(new JwtBearerOptions {
                    TokenValidationParameters = new TokenValidationParameters {
                        IssuerSigningKey = TokenAuthOption.Key,
                        ValidAudience = TokenAuthOption.Audience,
                        ValidIssuer = TokenAuthOption.Issuer,
                        ValidateIssuerSigningKey = true,
                        ValidateLifetime = true,
                        ClockSkew = TimeSpan.FromMinutes(0)
                    }
                });
                #endregion
    
                app.UseMvc(routes =>
                {
                    routes.MapRoute(
                        name: "default",
                        template: "{controller=Login}/{action=Index}");
                });
            }
        }
    }
  • 在Controllers中新建一個Web API Controller Class,命名爲TokenAuthController.cs。咱們將在這裏完成登陸受權

    在同文件下添加兩個類,分別用來模擬用戶模型,以及用戶存儲,代碼應該是這樣

    public class User
    {
        public Guid ID { get; set; }
        public string Username { get; set; }
        public string Password { get; set; }
    }
    
    public static class UserStorage
    {
        public static List<User> Users { get; set; } = new List<User> {
            new User {ID=Guid.NewGuid(),Username="user1",Password = "user1psd" },
            new User {ID=Guid.NewGuid(),Username="user2",Password = "user2psd" },
            new User {ID=Guid.NewGuid(),Username="user3",Password = "user3psd" }
        };
    }

    接下來在TokenAuthController.cs中添加以下方法

    private string GenerateToken(User user, DateTime expires)
    {
        var handler = new JwtSecurityTokenHandler();
        
        ClaimsIdentity identity = new ClaimsIdentity(
            new GenericIdentity(user.Username, "TokenAuth"),
            new[] {
                new Claim("ID", user.ID.ToString())
            }
        );
    
        var securityToken = handler.CreateToken(new SecurityTokenDescriptor
        {
            Issuer = TokenAuthOption.Issuer,
            Audience = TokenAuthOption.Audience,
            SigningCredentials = TokenAuthOption.SigningCredentials,
            Subject = identity,
            Expires = expires
        });
        return handler.WriteToken(securityToken);
    }

    該方法僅僅只是生成一個Auth Token,接下來咱們來添加另一個方法來調用它

    在相同文件中添加以下代碼

    [HttpPost]
    public string GetAuthToken(User user)
    {
        var existUser = UserStorage.Users.FirstOrDefault(u => u.Username == user.Username && u.Password == user.Password);
    
        if (existUser != null)
        {
            var requestAt = DateTime.Now;
            var expiresIn = requestAt + TokenAuthOption.ExpiresSpan;
            var token = GenerateToken(existUser, expiresIn);
    
            return JsonConvert.SerializeObject(new {
                stateCode = 1,
                requertAt = requestAt,
                expiresIn = TokenAuthOption.ExpiresSpan.TotalSeconds,
                accessToken = token
            });
        }
        else
        {
            return JsonConvert.SerializeObject(new { stateCode = -1, errors = "Username or password is invalid" });
        }
    }

    該文件完整的代碼應該是這樣

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Mvc;
    using Newtonsoft.Json;
    using System.IdentityModel.Tokens.Jwt;
    using System.Security.Claims;
    using System.Security.Principal;
    using Microsoft.IdentityModel.Tokens;
    using CSTokenBaseAuth.Auth;
    
    namespace CSTokenBaseAuth.Controllers
    {
        [Route("api/[controller]")]
        public class TokenAuthController : Controller
        {
            [HttpPost]
            public string GetAuthToken(User user)
            {
                var existUser = UserStorage.Users.FirstOrDefault(u => u.Username == user.Username && u.Password == user.Password);
    
                if (existUser != null)
                {
                    var requestAt = DateTime.Now;
                    var expiresIn = requestAt + TokenAuthOption.ExpiresSpan;
                    var token = GenerateToken(existUser, expiresIn);
    
                    return JsonConvert.SerializeObject(new {
                        stateCode = 1,
                        requertAt = requestAt,
                        expiresIn = TokenAuthOption.ExpiresSpan.TotalSeconds,
                        accessToken = token
                    });
                }
                else
                {
                    return JsonConvert.SerializeObject(new { stateCode = -1, errors = "Username or password is invalid" });
                }
            }
    
            private string GenerateToken(User user, DateTime expires)
            {
                var handler = new JwtSecurityTokenHandler();
                
                ClaimsIdentity identity = new ClaimsIdentity(
                    new GenericIdentity(user.Username, "TokenAuth"),
                    new[] {
                        new Claim("ID", user.ID.ToString())
                    }
                );
    
                var securityToken = handler.CreateToken(new SecurityTokenDescriptor
                {
                    Issuer = TokenAuthOption.Issuer,
                    Audience = TokenAuthOption.Audience,
                    SigningCredentials = TokenAuthOption.SigningCredentials,
                    Subject = identity,
                    Expires = expires
                });
                return handler.WriteToken(securityToken);
            }
        }
    
        public class User
        {
            public Guid ID { get; set; }
    
            public string Username { get; set; }
    
            public string Password { get; set; }
        }
    
        public static class UserStorage
        {
            public static List<User> Users { get; set; } = new List<User> {
                new User {ID=Guid.NewGuid(),Username="user1",Password = "user1psd" },
                new User {ID=Guid.NewGuid(),Username="user2",Password = "user2psd" },
                new User {ID=Guid.NewGuid(),Username="user3",Password = "user3psd" }
            };
        }
    }
  • 接下來咱們來完成受權驗證部分

    在Controllers中新建一個Web API Controller Class,命名爲ValuesController.cs

    在其中添加以下代碼

    public string Get()
    {
        var claimsIdentity = User.Identity as ClaimsIdentity;
    
        var id = claimsIdentity.Claims.FirstOrDefault(c => c.Type == "ID").Value;
    
        return $"Hello! {HttpContext.User.Identity.Name}, your ID is:{id}";
    }

    爲方法添加裝飾屬性

    [HttpGet]
    [Authorize("Bearer")]

    完整的文件代碼應該是這樣

    using System.Linq;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Authorization;
    using System.Security.Claims;
    
    namespace CSTokenBaseAuth.Controllers
    {
        [Route("api/[controller]")]
        public class ValuesController : Controller
        {
            [HttpGet]
            [Authorize("Bearer")]
            public string Get()
            {
                var claimsIdentity = User.Identity as ClaimsIdentity;
    
                var id = claimsIdentity.Claims.FirstOrDefault(c => c.Type == "ID").Value;
    
                return $"Hello! {HttpContext.User.Identity.Name}, your ID is:{id}";
            }
        }
    }
  • 最後讓咱們來添加視圖

    在Controllers中新建一個Web Controller Class,命名爲LoginController.cs

    其中的代碼應該是這樣

    using Microsoft.AspNetCore.Mvc;
    
    namespace CSTokenBaseAuth.Controllers
    {
        [Route("[controller]/[action]")]
        public class LoginController : Controller
        {
            public IActionResult Index()
            {
                return View();
            }
        }
    }

    在項目Views目錄下新建一個名爲Login的目錄,並在其中新建一個Index.cshtml文件。

    代碼應該是這個樣子

    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <title></title>
    </head>
    <body>
        <button id="getToken">getToken</button>
        <button id="requestAPI">requestAPI</button>
    
        <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
        <script>
            $(function () {
                var accessToken = undefined;
    
                $("#getToken").click(function () {
                    $.post(
                        "/api/TokenAuth",
                        { Username: "user1", Password: "user1psd" },
                        function (data) {
                            console.log(data);
                            if (data.stateCode == 1)
                            {
                                accessToken = data.accessToken;
    
                                $.ajaxSetup({
                                    headers: { "Authorization": "Bearer " + accessToken }
                                });
                            }
                        },
                        "json"
                    );
                })
    
                $("#requestAPI").click(function () {
                    $.get("/api/Values", {}, function (data) {
                        alert(data);
                    }, "text");
                })
            })
        </script>
    </body>
    </html>

最後:完整的代碼Sample以及運行手冊,請訪問:How to achieve a bearer token authentication and authorization in ASP.NET Core

相關文章
相關標籤/搜索