ASP.NET Core 使用 JWT 进行身份认证

JWT (JSON Web Token) 是当前非常流行的无状态身份验证的一种解决方案,其原理为通过加密算法生成一个字符串,将字符串置于每次请求的Header中来进行身份验证。

JWT的构成

一个有效的JWT字符串示例如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODMyNDM0NzQsImlzcyI6Iklzc3VlciIsImF1ZCI6IkF1ZGllbmNlIn0.nrVYSkcIKMFdd9HSjkZz_7Kkcx3sdjEdFI9nT42OKac

可以看出JWT 字符串由.分隔开来的三部分构成:

  • Header
  • Payload
  • Signature

Header

JWTHeader部分eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9通过Base64解码可以得到其内容为:

{
  "alg": "HS256",
  "typ": "JWT"
}

容易看出Header包含了两个部分:

类型 全称 释义
alg algorithm 签名算法名称
typ type token类型

Payload

Payload即载荷,载荷中主要放一些声明(Claims)性的信息,比如用户名,角色等信息,因为是未加密的,所以不要放置密码等敏感的信息。
在这里同样将JWTPayload部分eyJleHAiOjE1ODMyNDM0NzQsImlzcyI6Iklzc3VlciIsImF1ZCI6IkF1ZGllbmNlIn0通过Base64解码得到其内容为:

{
    "exp":1583243474,
    "iss":"Issuer",
    "aud":"Audience"
}

容易看出这些键值对所代表的含义:

类型 全称 释义
exp expires 过期时间
iss issuer 发行者
aud audience 受众

Signature

Signature签名部分,由HeaderPayload两部分编码用Header里面注明的加密算法加密而成。

HMACSHA256(
  Base64UrlEncoder.Encode(header) + "." +
  Base64UrlEncoder.Encode((payload),
  Secret)

这里的Secret是自己确定的密钥。

ASP.NET Core 实现JWT

开发环境准备

  • 操作系统:Windows 10 1909
  • IDE:Visual Studio Enterprise 2019 16.4.5
  • .NET Core Runtime:3.1.102

新建项目

Visual Studio中新建一个ASP.NET Core Web API项目,如下所示:

新建项目

通过Nuget安装JWT相关包

Nuget包管理器中安装Microsoft.AspNetCore.Authentication.JwtBearer包,如下所示

Microsoft.AspNetCore.Authentication.JwtBearer包

Startup中配置JWT信息

ConfigureServices方法中加入配置后如下:

       public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,
                        ValidIssuer = "Issuer",

                        ValidateAudience = true,
                        ValidAudience = "Audience",

                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("{069BD1DF-72D8-474B-8950-2C3EB03B2D03}")),

                        ValidateLifetime = true,
                        ClockSkew = TimeSpan.FromMinutes(30),
                    };
                });
            services.AddSingleton<WeatherForecast>();
        }

其中services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)是将Bearer作为身份认证的默认方案,JwtBearerDefaults.AuthenticationSchemeBearer字符串常量。
services.AddJwtBearer是进行JWT的相关配置。

添加认证管道

Configure方法中加入:

 app.UseAuthentication();

注意它需要添加在app.UseAuthorization();之前,app.UseRouting();之后,顺序很重要,否则身份认证不会生效。

新建LoginController签发token

namespace JwtTokenDemo.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class LoginController : ControllerBase
    {
        // POST: api/Login
        [HttpPost]
        public IActionResult Index([FromBody] LoginViewModel model)
        {
            if (_names.Contains(model.Username) && model.Password == "admin")
            {
                var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("{069BD1DF-72D8-474B-8950-2C3EB03B2D03}"));
                var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
                var token = new JwtSecurityToken(
                    "Issuer",
                    "Audience",
                    new List<Claim>
                    {
                        new Claim(ClaimTypes.Role, "admin"),
                        new Claim(ClaimTypes.Gender,"Male"),
                        new Claim(ClaimTypes.Name,model.Username),
                    },
                    expires: DateTime.UtcNow.AddMinutes(30),
                    signingCredentials: credentials
                );
                return Ok(new
                {
                    access_token = new JwtSecurityTokenHandler().WriteToken(token)
                });
            }
            return Unauthorized();
        }
   }
}

在上面,我们定义了token的过期时间为令牌颁发后的30分钟后,同时定义了RoleGenderNameClaim声明,这些都可以通过解码Payload部分查看到。
这里用了一个用户类接受账号密码:

public class LoginViewModel
{
    [JsonProperty("username")]
    public string Username { get; set; }
    [JsonProperty("password")]
    public string Password { get; set; }
}

设置受保护的资源

在需要验证的控制器或者HTTP请求方法上添加[Authorize]修饰头,请求的时候就会服务器验证当前请求是否有权限访问此资源。如,我们给Weather控制器加上需要授权才能访问:

    [Authorize]
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;
        private readonly WeatherForecast _weatherForecast;


        public WeatherForecastController(ILogger<WeatherForecastController> logger,WeatherForecast weatherForecast)
        {
            _logger = logger;
            _weatherForecast = weatherForecast;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var user = HttpContext.User;
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => _weatherForecast)
            .ToArray();
        }
    }

Postman 测试

FromBody提交用户名和密码来获取access_token

登录

验证资源

access_token的值加入到HTTP请求的Header中的Authorization去,正确格式为:

Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJhZG1pbiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL2dlbmRlciI6Ik1hbGUiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJleHAiOjE1ODMzMzk4MTMsImlzcyI6Iklzc3VlciIsImF1ZCI6IkF1ZGllbmNlIn0.ln6ZZ2b_eNgcDxW7zrmI6xY4clxdu4f-zScJ43FjmwY

注意Bearer与后面的access_token之间有一个空格。
我们请求资源可以看到200 OK,请求成功。如下图:

请求成功

当我们修改token中某个字符再次请求,就会发现401 Unauthorized 未授权响应。如下图:
请求失败

欲知如何进行基于角色、基于声明、基于策略的授权,请听下回分解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354