ASP.NET Core 使用 JWT

登录方式对比

  1. 客户端向服务器发送用户名和密码
  2. 服务器验证通过,并把相关数据保存在 Session 中,例如登录时间之类的
  3. 服务器返回给用户一个 SessionId ,客户端把这个 SessionId 写入 Cookie
  4. 用户每次请求都会通过 Cookie 提交 SessionId 到服务器
  5. 服务器收到 SessionId 后查找数据,就可以知道用户身份

特点:

  • 数据存储在服务器,安全性较强,但是占用服务器资源
  • 因为使用到了 Cookie ,所以会被伪造
  • 如果服务器较多,或者跨域访问之类的操作,就要求共享 Session 资源,否则就需要用户和服务器重复登录验证操作,或者记录用户登录的服务器,对服务器和用户体验都不好。

JWT 方式登录

  1. 客户端向服务器发送用户名和密码
  2. 服务器验证通过,对用户数据进行加密,生成 Token 返回给客户端
  3. 浏览器(客户端)接收到 Token 后,将 Token 存储在 Local Storage,需要使用 JavaScript 代码获取,而 Cookie 是自动携带
  4. 用户每次请求都把 Token 提交到服务器
  5. 服务器对传来的 Token 进行解密,再去查询用户数据,一次知道用户身份

特点:

  • 存储在客户端,不占用服务器资源,但是同样会被伪造
  • 前后端分离,带上 Token 进行请求,不需要考虑用户是在哪个服务器上登录的,多服务器和跨域请求都没有问题

建议:对数据库的增删改,必须加上 Token 验证,查询不加 Token ,这样效率会比较高,同时查询操作也无法获取 Token ,更安全

如何强制token失效?
在数据库里保存一份 Token ,验证时再拿出来校验,重新登录就刷新覆盖这个值

JWT

JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案。

先看概念

官网:https://jwt.io/
这张图来自官网

JWT 结构

JWT 分为三个部分

  • Header,算法和令牌类型
{
  "alg": "HS256",
  "typ": "JWT"
}
  • Payload:数据,实际需要传递的 JSON 对象
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}
  • Verify Signature:签名,用于防伪验证
HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    your-256-bit-secret
)

加密之后的结果:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

终于可以上代码了

代码

首先,新建一个 ASP.NET Core 空项目,这里我就用 RESTful 吧

用 NuGet 安装一个库:Microsoft.AspNetCore.Authentication.JwtBearer

然后写 Startup 类

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace JWT_Program
{
    public class Startup
    {
        private readonly IConfiguration _configuration;

        public Startup(IConfiguration configuration)
        {
            this._configuration = configuration;
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            //添加jwt验证
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,//是否验证Issuer
                    ValidateAudience = true,//是否验证Audience
                    ValidateLifetime = true,//是否验证失效时间
                    ValidateIssuerSigningKey = true,//是否验证SecurityKey
                    ValidIssuer = this._configuration["Jwt:Issuer"],//Issuer
                    ValidAudience = this._configuration["Jwt:Issuer"],//Audience
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this._configuration["Jwt:Key"]))//SecurityKey
                };
            });

        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            //认证
            app.UseAuthentication();
            //授权
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

在此示例中,我们指定了必须考虑哪些参数才能将 JWT 视为有效。根据我们的代码,以下项目认为令牌有效:

  • 验证生成令牌的服务器 (ValidateIssuer = true)。
  • 验证令牌的接收者被授权接收(ValidateAudience = true)
  • 检查令牌是否未过期以及颁发者的签名密钥是否有效(ValidateLifetime = true)
  • 验证令牌的签名(ValidateIssuerSigningKey = true)
  • 此外,我们指定Issuer、Audience、SigningKey的值。在本例中,我将这些值存储在 appsettings.json 文件中,使用 IConfiguration 去读取。

appsettings.json,这个 Key有长度要求的,不然会报IDX10603: Decryption failed这个错

"Jwt": {
  "Key": "ThisIsMySecretKey",
  "Issuer": "Test.com"
}

可以写一个模型类,UserModel

public class UserModel
{
    public string Username { get; set; }
    public string Password { get; set; }
}

再来控制器
DemoController ,用于测试 Token 是否有效

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace JWT_Program.Controllers
{
    [Route("v1/[controller]")]
    [ApiController]
    public class DemoController : Controller
    {
        [HttpGet("Get1")]
        public async Task<ActionResult<IEnumerable<string>>> Foo_01()
        {
            List<string> list = new List<string>();

            list.Add("Foo_01");
            list.Add("Test");

            return Ok(list);
        }

        [HttpGet("Get2")]
        [Authorize]
        public async Task<ActionResult<IEnumerable<string>>> Foo_02()
        {
            List<string> list = new List<string>();

            list.Add("Foo_02");
            list.Add("Test");

            return Ok(list);
        }
    }
}

里面两个个函数,一个是没有 Token 验证的,加 [Authorize]就是有 Token 验证

再看 LoginController

using JWT_Program.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;

namespace JWT_Program.Controllers
{
    [Route("v1/[controller]")]
    [ApiController]
    public class LoginController : Controller
    {
        private readonly IConfiguration _configuration;

        public LoginController(IConfiguration configuration)
        {
            this._configuration = configuration;
        }

        [AllowAnonymous]//指定此属性应用于的类或方法不需要授权。
        [HttpPost("Login")]
        public IActionResult Login([FromBody] UserModel login)
        {
            if ("abc" == login.Username && "123456" == login.Password)
            {
                //包含的内容,对应 Payload 部分,是键值对数组
                var claims = new[]
                {
                    new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") ,
                    new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddMinutes(30)).ToUnixTimeSeconds()}"),
                    new Claim(ClaimTypes.Name, login.Username)
                };
                //密钥,从 appsetting.json 中读取
                var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this._configuration["Jwt:Key"]));
                //签名证书,使用密钥和算法加密
                var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
                //令牌
                var token = new JwtSecurityToken(
                    issuer: this._configuration["Jwt:Issuer"],
                    audience: this._configuration["Jwt:Issuer"],
                    claims: claims,
                    expires: DateTime.Now.AddMinutes(30),
                    signingCredentials: credentials);

                return Ok(new
                {
                    token = new JwtSecurityTokenHandler().WriteToken(token)
                });
            }
            else
            {
                return NoContent();
            }
        }
    }
}

用户名和密码的验证就写死了,图个方便
还有 Audience ,这玩意儿应该根据客户端来写,这里图省事也就用 Issuer 了

测试

因为懒得写前端,所以使用 postman 测试

首先,不登录测试 DemoController 里面的两个函数

可以看到是 401未认证错误

登录,以及生成的 Token

把生成的 Token 复制下来

{
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOiIxNjI2NTkyOTY0IiwiZXhwIjoxNjI2NTk0NzY0LCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWJjIiwiaXNzIjoiVGVzdC5jb20iLCJhdWQiOiJUZXN0LmNvbSJ9.e_F0PWFNfFjMOyCnwrgjRb7r7ccZJb9aAUS8xOpneDg"
}

使用 Token 再去测试需要 Token 的函数

成功了

ASP.NET Core 使用 JWT 结束

注意,我这里把逻辑全写在了控制器里,实际上应该把 Token 相关的功能封装成一个工具类或者服务类
项目结构

posted @ 2021-07-18 15:26  .NET好耶  阅读(439)  评论(0编辑  收藏  举报