ASP.NET Core 使用 JWT
登录方式对比
传统登录方式 Session + Cookie
- 客户端向服务器发送用户名和密码
- 服务器验证通过,并把相关数据保存在 Session 中,例如登录时间之类的
- 服务器返回给用户一个 SessionId ,客户端把这个 SessionId 写入 Cookie
- 用户每次请求都会通过 Cookie 提交 SessionId 到服务器
- 服务器收到 SessionId 后查找数据,就可以知道用户身份
特点:
- 数据存储在服务器,安全性较强,但是占用服务器资源
- 因为使用到了 Cookie ,所以会被伪造
- 如果服务器较多,或者跨域访问之类的操作,就要求共享 Session 资源,否则就需要用户和服务器重复登录验证操作,或者记录用户登录的服务器,对服务器和用户体验都不好。
JWT 方式登录
- 客户端向服务器发送用户名和密码
- 服务器验证通过,对用户数据进行加密,生成 Token 返回给客户端
- 浏览器(客户端)接收到 Token 后,将 Token 存储在 Local Storage,需要使用 JavaScript 代码获取,而 Cookie 是自动携带
- 用户每次请求都把 Token 提交到服务器
- 服务器对传来的 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 相关的功能封装成一个工具类或者服务类
项目结构