手把手教你 .NET Core 3.1 JWT身份认证
概述
什么是JWT?
JWT:Json Web Token,是基于Json的一个公开规范,这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息,他的两大使用场景是:认证和数据交换使用起来就是,由服务端根据规范生成一个令牌(token),并且发放给客户端。此时客户端请求服务端的时候就可以携带者令牌,以令牌来证明自己的身份信息。作用:类似session保持登录状态 的办法,通过token来代表用户身份。
先来看一下它的流程图:
流程描述:
1、用户使用账号、密码登录应用,登录的请求发送到Authentication Server。
2、Authentication Server进行用户验证,然后创建JWT字符串返回给客户端。
3、客户端请求接口时,在请求头带上JWT。
4、Application Server验证JWT合法性,如果合法则继续调用应用接口返回结果。
JWT 的数据结构
JWT主要由三部分组成,如下:HEADER.PAYLOAD.SIGNATURE
完整的Token,如下所示:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJleHAiOjE1Nzg2NDU1MzYsImlzcyI6IndlYmFwaS5jbiIsImF1ZCI6IldlYkFwaSJ9.2_akEH40LR2QWekgjm8Tt3lesSbKtDethmJMo_3jpF4
HEADER 头部
上面示例中 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. 即为 HEADER , 它包含token的元数据,主要是加密算法,和签名的类型,原始信息为 {"alg":"HS256","typ":"JWT"} ,通过 BASE64编码后成为 token 的第一部分。
Payload 消息体(载荷)
payload 信息存放的是Claims声明信息。载荷其实就是自定义的数据,一般存储用户Id,过期时间等信息。也就是JWT的核心所在,因为这些数据就是使后端知道此token是哪个用户已经登录的凭证。而且这些数据是存在token里面的,由前端携带,所以后端几乎不需要保存任何数据。
Signature 签名
JWT第三部分是签名。是这样生成的,首先需要指定一个secret,该secret仅仅保存在服务器中,保证不能让其他用户知道。然后使用Header指定的算法对Header和Payload进行计算,然后就得出一个签名哈希。也就是Signature。那么Application Server如何进行验证呢?可以利用JWT前两段,用同一套哈希算法和同一个secret计算一个签名值,然后把计算出来的签名值和收到的JWT第三段比较,如果相同则认证通过。
代码示例
新建WebAPI项目
这里新建的是.NET Core 3.1的 WebAPI项目,如下图所示,完成项目的创建后,通过Nuget添加 Microsoft.AspNetCore.Authentication.JwtBearer 的包,因为项目是 .NET Core 3.1的项目,这里的Nuget包选 3.1.24。
添加JWT配置
在appsetting.json 添加 JWT 的配置,如下图所示:
"JWTConfig": {
"Secret": "abcdefghijklmnop",
"Issuer": "cnblogs.com",
"Audience": "WebApi",
"AccessExpiration": 30,
"RefreshExpiration": 60
}
这里需要注意的是 Secret 必须是16位以上的字符,否则在认证的时候会报错。
1、在项目下新建目录 Model ,然后创建类 JWTConfig ,代码如下:
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace JwtTest.WebApi
{
public class JWTConfig
{
/// <summary>
/// 密钥 长度不少于16位
/// </summary>
[JsonProperty("Secret")]
public string Secret { get; set; }
/// <summary>
/// 发行人
/// </summary>
[JsonProperty("Issuer")]
public string Issuer { get; set; }
/// <summary>
/// 观众
/// </summary>
[JsonProperty("Audience")]
public string Audience { get; set; }
/// <summary>
/// 访问过期
/// </summary>
[JsonProperty("AccessExpiration")]
public int AccessExpiration { get; set; }
/// <summary>
/// 刷新过期
/// </summary>
[JsonProperty("RefreshExpiration")]
public int RefreshExpiration { get; set; }
}
}
2、在 Startup 的 ConfigureServices 方法添加 读取配置,以及认证配置,添加后,代码会提示引用其他的一些命名空间,这里都按提示的引用即可。
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.Configure<JWTConfig>(Configuration.GetSection("JWTConfig"));
var tokenConfigs = Configuration.GetSection("JWTConfig").Get<JWTConfig>();
//Authentication
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x => {
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tokenConfigs.Secret)),
ValidIssuer = tokenConfigs.Issuer,
ValidAudience = tokenConfigs.Audience,
ValidateIssuer = false,
ValidateAudience = false
};
});
}
接下来,还需要在 Configure 方法中,添加启用认证。如下图:
添加 Model 对象
在Model 目录添加的两个对象,主要是用于认证的信息对象(LoginModel)以及用户实体(UserModel)(例子为了演示便利,没有连接数据库),代码如下:
/// <summary>
/// 认证信息对象
/// </summary>
public class LoginModel
{
/// <summary>
/// 用户名
/// </summary>
[Required]
[JsonProperty("username")]
public string UserName { get; set; }
/// <summary>
/// 密码
/// </summary>
[Required]
[JsonProperty("password")]
public string Password { get; set; }
}
/// <summary>
/// 用户对象
/// </summary>
public class UserModel
{
/// <summary>
/// 用户Id
/// </summary>
public string UserId { get; set; }
/// <summary>
/// 用户名
/// </summary>
public string UserName { get; set; }
/// <summary>
/// 密码
/// </summary>
public string Password { get; set; }
/// <summary>
/// 地址
/// </summary>
public string Address { get; set; }
/// <summary>
/// 手机号
/// </summary>
public string MobilePhone { get; set; }
/// <summary>
/// 邮箱
/// </summary>
public string Email { get; set; }
/// <summary>
/// 性别
/// </summary>
public string Gender { get; set; }
/// <summary>
/// QQ号码
/// </summary>
public string QQ { get; set; }
}
添加服务类和接口
1、在目录下新建service目录,然后新建 接口 IJWTService 以及实现类 JWTService,用于JWT的认证处理,代码如下:
/// <summary>
/// 认证处理接口
/// </summary>
public interface IJWTService
{
bool IsAuthenticated(LoginModel model, out string token);
}
/// <summary>
/// 认证处理服务
/// </summary>
public class JWTService : IJWTService
{
public bool IsAuthenticated(LoginModel model, out string token)
{
throw new NotImplementedException();
}
}
2、在目录下新建service目录,然后新建 接口 IUserService 以及实现类 UserService,用于用户数据的验证处理,代码如下:
/// <summary>
/// 用户数据服务接口
/// </summary>
public interface IUserService
{
/// <summary>
/// 根据用户名,获取用户信息
/// </summary>
/// <param name="username"></param>
/// <returns></returns>
UserModel GetUserByUserName(string username);
}
/// <summary>
/// 用户数据处理服务
/// </summary>
public class UserService : IUserService
{
public UserModel GetUserByUserName(string username)
{
throw new NotImplementedException();
}
}
3、首先,我们先来实现用户数据处理的服务的方法,这里为了简化例子,并没有连接数据库,只是模拟了数据的查询。完整代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace JwtTest.WebApi
{
public class UserService : IUserService
{
private readonly List<UserModel> _userList;
public UserService()
{
_userList = new List<UserModel>
{
new UserModel { UserId = "123", UserName = "Jack", Address = "北京", Email = "jack@mail.com", Gender = "男", MobilePhone = "13800138000", Password = "123456", QQ = "83849484", },
new UserModel { UserId = "456", UserName = "Lily", Address = "上海", Email = "lily@mail.com", Gender = "女", MobilePhone = "13800138001", Password = "654321", QQ = "23423456", },
new UserModel { UserId = "123", UserName = "Tom", Address = "深圳", Email = "tom@mail.com", Gender = "男", MobilePhone = "13800138003", Password = "abcdef", QQ = "54387678", },
};
}
/// <summary>
/// 检查用户名和密码
/// </summary>
/// <param name="username"></param>
/// <param name="pasword"></param>
/// <returns></returns>
public UserModel GetUserByUserName(string username)
{
var user = _userList.Where(o => o.UserName == username).FirstOrDefault();
return user;
}
}
}
4、接下来便是实现JWT的认证处理的服务了,想要使用用户数据处理服务和JWT配置信息,我们就需要通过构造函数注入。然后才能在 IsAuthenticated 方法里面引用相应的服务或配置信息。完整代码如下:
using Microsoft.Extensions.Options;
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 JwtTest.WebApi
{
/// <summary>
/// 认证处理服务
/// </summary>
public class JWTService : IJWTService
{
private readonly IUserService _userService;
private readonly JWTConfig _jwtConfig;
public JWTService(IUserService userService, IOptions<JWTConfig> jwtConfig)
{
this._userService = userService;
this._jwtConfig = jwtConfig.Value;
}
/// <summary>
/// 认证用户,生成Token
/// </summary>
/// <param name="model"></param>
/// <param name="token"></param>
/// <returns></returns>
public bool IsAuthenticated(LoginModel model, out string token)
{
token = string.Empty;
var user = _userService.GetUserByUserName(model.UserName);
//用户不存在
if (user == null)
{
return false;
}
//密码不正确
if (user.Password != model.Password)
{
return false;
}
//把有需要的信息写到Token
var claims = new[] {
new Claim(ClaimTypes.NameIdentifier, user.UserId),
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.Email, user.Email),
new Claim(ClaimTypes.Gender, user.Gender),
new Claim(ClaimTypes.MobilePhone, user.MobilePhone),
new Claim(ClaimTypes.StreetAddress, user.Address),
new Claim("QQ", user.QQ), //自定义
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtConfig.Secret));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var jwtToken = new JwtSecurityToken(_jwtConfig.Issuer,
_jwtConfig.Audience,
claims,
expires: DateTime.Now.AddMinutes(_jwtConfig.AccessExpiration),
signingCredentials: credentials);
token = new JwtSecurityTokenHandler().WriteToken(jwtToken);
return true;
}
}
}
完成了上面的服务类之后,我们还需要在Startup类的 ConfigureServices 方法中,完成注册到容器中。这样具体的构造函数才能完成注入。如下所示:
添加控制器,实现认证功能
BaseController
经过了上面的准备,我们可以着手实现认证的功能了,我们先添加 BaseController 控制器,并添加 [Authorize] 的特性,如下所示:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace JwtTest.WebApi.Controllers
{
[Authorize]
public class BaseController : Controller
{
}
}
使其作为基础控制器,其他的控制器如果希望通过认证才能使用,只需要继承这个控制器就可以了。
AuthenticationController
新增 AuthenticationController 控制器,使其继承 BaseController 控制器,添加构造函数,实现服务的注入,新增 Login 接口,使用 LoginModel 作为参数,在此接口中实现用户的认证,并返回 Token,完整代码如下:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace JwtTest.WebApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthenticationController : BaseController
{
private readonly IJWTService _authenticateService;
public AuthenticationController(IJWTService authenticateService)
{
this._authenticateService = authenticateService;
}
/// <summary>
/// 用户认证
/// </summary>
/// <param name="model"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost, Route("Login")]
public ActionResult Login([FromBody] LoginModel model)
{
if (!ModelState.IsValid)
{
return BadRequest("非法请求。");
}
string token;
if (_authenticateService.IsAuthenticated(model, out token))
{
return Ok(token);
}
return BadRequest("认证失败。");
}
}
}
WeatherForecastController
修改 WeatherForecastController 控制器,使其继承 BaseController , 这样便需要认证才能访问这个控制器下的接口了。这里,为了让大家进一步了解 JWT,我添加了个 UserInfo 接口,可以从这个接口,获取到用户的基本信息,完整代码如下所示:
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
namespace JwtTest.WebApi.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : BaseController
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
/// <summary>
/// 获取用户名
/// </summary>
/// <returns></returns>
[HttpGet, Route("UserInfo")]
public IActionResult UserInfo()
{
//获取用户信息
var claimsPrincipal = Response.HttpContext.User;
var user = new UserModel
{
UserId = claimsPrincipal.Claims.Where(o => o.Type == ClaimTypes.NameIdentifier).FirstOrDefault().Value,
UserName = claimsPrincipal.Claims.Where(o => o.Type == ClaimTypes.Name).FirstOrDefault().Value,
Email = claimsPrincipal.Claims.Where(o => o.Type == ClaimTypes.Email).FirstOrDefault().Value,
Gender = claimsPrincipal.Claims.Where(o => o.Type == ClaimTypes.Gender).FirstOrDefault().Value,
MobilePhone = claimsPrincipal.Claims.Where(o => o.Type == ClaimTypes.MobilePhone).FirstOrDefault().Value,
Address = claimsPrincipal.Claims.Where(o => o.Type == ClaimTypes.StreetAddress).FirstOrDefault().Value,
QQ = claimsPrincipal.Claims.Where(o => o.Type == "QQ").FirstOrDefault().Value,
};
return Ok(user);
}
}
}
至此,代码部分已经完毕,大家可以通过 Postman 测试,先请求 /api/Authentication/login 接口,拿到 token 后,再附加到 Authorization 标签页面的 Token 输入框,记得 Type 需要选项 Bearer Token 。
整个示例项目的结构,如下图所示:
源代码可在 https://github.com/jlonghe/JwtWebAPI 获取。