1. 程式人生 > >webapi中使用token驗證(JWT驗證)

webapi中使用token驗證(JWT驗證)

後端 erro filters missing 參考 做的 方法調用 reading minute

本文介紹如何在webapi中使用JWT驗證

  1. 準備

    安裝JWT安裝包 System.IdentityModel.Tokens.Jwt
    你的前端api登錄請求的方法,參考
        axios.get("api/token?username=cuong&password=1").then(function (res) {
            // 返回一個token
            /*
                token示例如下
                "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6Inllamlhd2VpIiwibmJmIjoxNTE0NjQyNTA0LCJleHAiOjE1MTQ2NDk3MDQsImlhdCI6MTUxNDY0MjUwNH0.ur97ZRviC_sfeFgDOHgaRpDePcYED6qmlfOvauPt9EA"
    */ }).catch(function (err) { console.log(err); }) 你的前端請求後端數據執行的任意方法,傳遞token,參考 var axiosInstance = window.axios.create({ headers: { common: { Authorization: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNTE0NjE4MDgzLCJleHAiOjE1MTQ2MjUyODMsImlhdCI6MTUxNDYxODA4M30.khgxAzTEgQ86uoxJjACygTkB0Do6i_9YcmLLh97eZtE"
    } /* 上面Authorization會自動映射成後端request.Headers.Authorization對象 { Parameter: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNTE0NjE4MDgzLCJleHAiOjE1MTQ2MjUyODMsImlhdCI6MTUxNDYxODA4M30.khgxAzTEgQ86uoxJjACygTkB0Do6i_9YcmLLh97eZtE"
    Scheme: "Bearer" } */ } }) axiosInstance.get("api/value").then(function (res) { }).catch(function (err) { console.log(err); })
  2. 創建TokenHelper類

    在項目跟目錄下創建一個TokenHelper.cs類,代碼如下
    using System;
    using System.IdentityModel.Tokens.Jwt;
    using System.Security.Claims;
    using Microsoft.IdentityModel.Tokens;
    namespace TokenTest
    {
        public class TokenHelper
        {
            /// <summary>
            /// Use the below code to generate symmetric Secret Key
            ///     var hmac = new HMACSHA256();
            ///     var key = Convert.ToBase64String(hmac.Key);
            /// </summary>
            private const string Secret = "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw==";
            public static string GenerateToken(string username, int expireMinutes = 120)
            { // 此方法用來生成 Token 
                var symmetricKey = Convert.FromBase64String(Secret);  // 生成二進制字節數組
                var tokenHandler = new JwtSecurityTokenHandler(); // 創建一個JwtSecurityTokenHandler類用來生成Token
                var now = DateTime.UtcNow; // 獲取當前時間
                var tokenDescriptor = new SecurityTokenDescriptor // 創建一個 Token 的原始對象
                { 
                    Subject = new ClaimsIdentity(new[] // Token的身份證,類似一個人可以有身份證,戶口本
                            {
                                new Claim(ClaimTypes.Name, username) // 可以創建多個
                            }),
    
                    Expires = now.AddMinutes(Convert.ToInt32(expireMinutes)), // Token 有效期
    
                    SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(symmetricKey), SecurityAlgorithms.HmacSha256)
                    // 生成一個Token證書,第一個參數是根據預先的二進制字節數組生成一個安全秘鑰,說白了就是密碼,第二個參數是編碼方式
                }; 
                var stoken = tokenHandler.CreateToken(tokenDescriptor); // 生成一個編碼後的token對象實例
                var token = tokenHandler.WriteToken(stoken); // 生成token字符串,給前端使用
                return token;
            }
            public static ClaimsPrincipal GetPrincipal(string token)
            { // 此方法用解碼字符串token,並返回秘鑰的信息對象
                try
                {
                    var tokenHandler = new JwtSecurityTokenHandler(); // 創建一個JwtSecurityTokenHandler類,用來後續操作
                    var jwtToken = tokenHandler.ReadToken(token) as JwtSecurityToken; // 將字符串token解碼成token對象
                    if (jwtToken == null)
                        return null;
                    var symmetricKey = Convert.FromBase64String(Secret); // 生成編碼對應的字節數組
                    var validationParameters = new TokenValidationParameters() // 生成驗證token的參數
                    {
                        RequireExpirationTime = true, // token是否包含有效期
                        ValidateIssuer = false, // 驗證秘鑰發行人,如果要驗證在這裏指定發行人字符串即可
                        ValidateAudience = false, // 驗證秘鑰的接受人,如果要驗證在這裏提供接收人字符串即可
                        IssuerSigningKey = new SymmetricSecurityKey(symmetricKey) // 生成token時的安全秘鑰
                    };
                    SecurityToken securityToken; // 接受解碼後的token對象
                    var principal = tokenHandler.ValidateToken(token, validationParameters, out securityToken); 
                    return principal; // 返回秘鑰的主體對象,包含秘鑰的所有相關信息
                }
    
                catch (Exception ex)
                {
                    return null;
                }
            }
        }
    }
  3. 創建過濾器類

    當前端發送一個請求,需要接收並處理token
    在當前項目下創建一個名為Filter的文件夾
    創建一個AuthenticationAttribute類,代碼如下
        using System;
        using System.Collections.Generic;
        using System.Security.Claims;
        using System.Security.Principal;
        using System.Threading;
        using System.Threading.Tasks;
        using System.Web.Http.Filters;
        namespace TokenTest.Filter
        {
            // IAuthenticationFilter用來自定義一個webapi控制器方法屬性
            public class AuthenticationAttribute : Attribute, IAuthenticationFilter
            {
                public bool AllowMultiple => false;
                public string Realm { get; set; }
                public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
                {
                    // 當api發送請求,自動調用這個方法
                    var request = context.Request; // 獲取請求的請求體
                    var authorization = request.Headers.Authorization; // 獲取請求的token對象
                    if (authorization == null || authorization.Scheme != "Bearer") return;
                    if(string.IsNullOrEmpty(authorization.Parameter))
                    {
                        // 給ErrorResult賦值需要一個類實現了IHttpActionResult接口
                        // 此類聲明在AuthenticationFailureResult.cs文件中,此文件用來處理錯誤信息。
                        context.ErrorResult = new AuthenticationFailureResult("Missing Jwt Token", request);
                        return;
                    }
                    var token = authorization.Parameter; // 獲取token字符串
                    var principal = await AuthenticateJwtToken(token); // 調用此方法,根據token生成對應的"身份證持有人"
                    if(principal == null)
                    {
                        context.ErrorResult = new AuthenticationFailureResult("Invalid token", request);
                    }
                    else
                    {
                        context.Principal = principal; // 設置身份驗證的主體
                    }
                    // 此法調用完畢後,會調用ChallengeAsync方法,從而來完成WWW-Authenticate驗證
                }
                private Task<IPrincipal> AuthenticateJwtToken(string token)
                {
                    string userName;
                    if(ValidateToken(token, out userName))
                    {
                        // 這裏就是驗證成功後要做的邏輯,也就是處理WWW-Authenticate驗證
                        var info = new List<Claim>
                        {
                            new Claim(ClaimTypes.Name, userName)
                        }; // 根據驗證token後獲取的用戶名重新在建一個聲明,你個可以在這裏創建多個聲明
                        // 作者註: claims就像你身份證上面的信息,一個Claim就是一條信息,將這些信息放在ClaimsIdentity就構成身份證了
                        var infos = new ClaimsIdentity(info, "Jwt");
                        // 將上面的身份證放在ClaimsPrincipal裏面,相當於把身份證給持有人
                        IPrincipal user = new ClaimsPrincipal(infos);
                        return Task.FromResult(user);
                    }
                    return Task.FromResult<IPrincipal>(null);
                }
                private bool ValidateToken(string token, out string userName)
                {
                    userName = null;
                    var simplePrinciple = TokenHelper.GetPrincipal(token); // 調用自定義的GetPrincipal獲取Token的信息對象
                    var identity = simplePrinciple?.Identity as ClaimsIdentity; // 獲取主聲明標識
                    if (identity == null) return false;
                    if (!identity.IsAuthenticated) return false;
                    var userNameClaim = identity.FindFirst(ClaimTypes.Name); // 獲取聲明類型是ClaimTypes.Name的第一個聲明
                    userName = userNameClaim?.Value; // 獲取聲明的名字,也就是用戶名
                    if (string.IsNullOrEmpty(userName)) return false;
                    return true;
                    // 到這裏token本身的驗證工作已經完成了,因為用戶名可以解碼出來
                    // 後續要驗證的就是瀏覽器的 WWW-Authenticate
                    /*
                        什麽是WWW-Authenticate驗證???
                        WWW-Authenticate是早期的一種驗證方式,很容易被破解,瀏覽器發送請求給後端,後端服務器會解析傳過來的Header驗證
                        如果沒有類似於本文格式的token,那麽會發送WWW-Authenticate: Basic realm= "." 到前端瀏覽器,並返回401
                    */
                }
                public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
                {
                    // 此方法在AuthenticateAsync方法調用完成之後自動調用
                    ChallengeAsync(context);
                    return Task.FromResult(0);
                }
                private void ChallengeAsync(HttpAuthenticationChallengeContext context)
                {
                    string parameter = null;
                    if (!string.IsNullOrEmpty(Realm))
                    {
                        parameter = "realm=\"" + Realm + "\"";
                    } // token的parameter部分已經通過jwt驗證成功,這裏只需要驗證scheme即可
                    context.ChallengeWith("Bearer", parameter); // 這個自定義擴展方法定義在HttpAuthenticationChallengeContextExtensions.cs文件中
                    // 主要用來驗證token的Schema是不是Bearer
                }
            }
        }
    創建AuthenticationFailureResult類,代碼如下
        using System;
        using System.Collections.Generic;
        using System.Linq;
        using System.Net;
        using System.Net.Http;
        using System.Threading;
        using System.Threading.Tasks;
        using System.Web.Http;
        namespace TokenTest.Filter
        {
            // 此類比較簡單不做過多註釋
            public class AuthenticationFailureResult : IHttpActionResult
            {
                public string _FailureReason { get; }
                public HttpRequestMessage _Request { get; }
                public AuthenticationFailureResult(string FailureReason, HttpRequestMessage request)
                {
                    _FailureReason = FailureReason;
                    _Request = request;
                }
                HttpResponseMessage HandleResponseMessage()
                {
                    HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized)
                    {
                        RequestMessage = _Request,
                        ReasonPhrase = _FailureReason
                    };
                    return response;
                }
                public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
                {
                    return Task.FromResult(HandleResponseMessage());
                }
            }
        }
    創建HttpAuthenticationChallengeContextExtensions類,寫的context的擴展方法,代碼如下
        using System;
        using System.Net.Http.Headers;
        using System.Web.Http.Filters;
        namespace TokenTest.Filter
        {
            public static class HttpAuthenticationChallengeContextExtensions
            {
                public static void ChallengeWith(this HttpAuthenticationChallengeContext context, string scheme)
                {
                    ChallengeWith(context, new AuthenticationHeaderValue(scheme));
                }
                private static void ChallengeWith(HttpAuthenticationChallengeContext context, AuthenticationHeaderValue challenge)
                {
                    if(context == null)
                    {
                        throw new ArgumentNullException(nameof(context));
                    }
                    context.Result = new AddChallengeOnUnauthorizedResult(challenge, context.Result);
                }
                public static void ChallengeWith(this HttpAuthenticationChallengeContext context, string scheme, string parameter)
                {
                    // 第二個參數的作用是根據傳進來的scheme也就是"Bearer"和parameter這裏為null,創建一個驗證頭,和前端傳過來的token是一樣的
                    ChallengeWith(context, new AuthenticationHeaderValue(scheme, parameter));
                }
            }
        }
    創建AddChallengeOnUnauthorizedResult類,代碼如下
        using System.Linq;
        using System.Net;
        using System.Net.Http;
        using System.Net.Http.Headers;
        using System.Threading;
        using System.Threading.Tasks;
        using System.Web.Http;
        namespace TokenTest.Filter
        {
            public class AddChallengeOnUnauthorizedResult: IHttpActionResult
            {
                public AuthenticationHeaderValue _Challenge { get; }
                public IHttpActionResult _InnerResult { get; }
                public AddChallengeOnUnauthorizedResult(AuthenticationHeaderValue challenge, IHttpActionResult innerResult)
                {
                    _Challenge = challenge;
                    _InnerResult = innerResult;
                }
                public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
                {
                    // 這裏講schemee也就是"Bearer"生成後的response返回給瀏覽器去做判斷,如果瀏覽器請求的Authenticate中含有含有名為"Bearer"的scheme會返回200狀態碼否則返回401狀態碼
                    HttpResponseMessage response = await _InnerResult.ExecuteAsync(cancellationToken);
                    if(response.StatusCode == HttpStatusCode.Unauthorized)
                    {
                        // 如果這裏不成立,但是我們之前做的驗證都是成功的,這是不對的,可能出現意外情況啥的
                        // 這時我們手動添加一個名為"Bearer"的sheme,讓請求走通
                        // 到此,完畢。
                        if (response.Headers.WwwAuthenticate.All(h => h.Scheme != _Challenge.Scheme))
                        {
                            response.Headers.WwwAuthenticate.Add(_Challenge);
                        }
                    }
                    return response;
                }
            }
        }
  4. 配置

    在你的WebApiConfig.cs文件中添加
    config.Filters.Add(new AuthorizeAttribute()); // 開啟全局驗證服務
  5. 代碼使用

    創建一個webapi的控制器
    測試,用戶登錄
        [Route("yejiawei/haha")]
        [HttpGet]
        [AllowAnonymous] // 這個屬性是必須的,表示這個類是不需要token驗證的
        public string Get(string username, string password)
        {
            if (CheckUser(username, password))
            {
                return TokenHelper.GenerateToken(username);
            }
    
            throw new HttpResponseException(HttpStatusCode.Unauthorized);
        }
        public bool CheckUser(string username, string password)
        {
            // 在這裏你可以在數據庫中查看用戶名是否存在
            return true;
        }
    測試,訪問後端api數據
        [Route("yejiawei/haha")]
        [HttpGet]
        [Authentication] // 此方法驗證的token需要調用Authentication屬性方法
        public string Get()
        {
            return "value";
        }
    到此一切搞定。

webapi中使用token驗證(JWT驗證)