21. 令牌Token
提示
Admin.NET 采用 Token 鉴权,并且是双令牌模式。访问 AccessToken 和刷新 RefreshToken,AccessToken 过期后,系统会根据刷新 RefreshToken 去服务器换新的 AccessToken,前端更新保存 Token 可以继续访问接口。
如果需要对特定的 Action 或 Controller 允许匿名访问,则只需要贴 [AllowAnonymous]
即可。
同时,Admin.NET 已经实现了当前用户管理服务类,直接注入则可以直接获取当前登录用户的名称、用户Id等信息,无需再根据 Token 手动解析。直接拿到当前访问用户Id,可方便处理具体业务逻辑。
JWT 配置文件如下 Configuration/JWT.json,记得更换密钥串,防止多个系统一样,造成账号安全事故。密匙串建议直接用32位长度的MD5串。.NET8 时所需要的字符串长度更长,就再复制一份。
{
"$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json",
"JWTSettings": {
"ValidateIssuerSigningKey": true, // 是否验证密钥,bool 类型,默认true
"IssuerSigningKey": "3c1cbc3f546eda35168c3aa3cb91780fbe703f0996c6d123ea96dc85c70bbc0a", // 密钥,string 类型,必须是复杂密钥,长度大于16
"ValidateIssuer": true, // 是否验证签发方,bool 类型,默认true
"ValidIssuer": "Admin.NET", // 签发方,string 类型
"ValidateAudience": true, // 是否验证签收方,bool 类型,默认true
"ValidAudience": "Admin.NET", // 签收方,string 类型
"ValidateLifetime": true, // 是否验证过期时间,bool 类型,默认true,建议true
//"ExpiredTime": 20, // 过期时间,long 类型,单位分钟,默认20分钟,最大支持 13 年
"ClockSkew": 5, // 过期时间容错值,long 类型,单位秒,默认5秒
"Algorithm": "HS256", // 加密算法,string 类型,默认 HS256
"RequireExpirationTime": true // 验证过期时间,设置 false 将永不过期
}
}
Algorithm 加密算法
- HS256
- HS384
- HS512
- PS256
- PS384
- PS512
- ES256
- ES256K
- ES384
- ES512
- EdDSA
具体可参考 SecurityAlgorithms
Token 默认过期时间为 7 天,在系统参数配置里面可自行修改。
当前登录用户管理服务类 Admin.NET.Core/Service/User/UserManager.cs 实现如下:
namespace Admin.NET.Core;
/// <summary>
/// 当前登录用户
/// </summary>
public class UserManager : IScoped
{
private readonly IHttpContextAccessor _httpContextAccessor;
/// <summary>
/// 用户ID
/// </summary>
public long UserId => (_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.UserId)?.Value).ToLong();
/// <summary>
/// 租户ID
/// </summary>
public long TenantId => (_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.TenantId)?.Value).ToLong();
/// <summary>
/// 用户账号
/// </summary>
public string Account => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.Account)?.Value;
/// <summary>
/// 真实姓名
/// </summary>
public string RealName => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.RealName)?.Value;
/// <summary>
/// 是否超级管理员
/// </summary>
public bool SuperAdmin => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.AccountType)?.Value == ((int)AccountTypeEnum.SuperAdmin).ToString();
/// <summary>
/// 是否系统管理员
/// </summary>
public bool SysAdmin => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.AccountType)?.Value == ((int)AccountTypeEnum.SysAdmin).ToString();
/// <summary>
/// 组织机构Id
/// </summary>
public long OrgId => (_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.OrgId)?.Value).ToLong();
/// <summary>
/// 微信OpenId
/// </summary>
public string OpenId => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.OpenId)?.Value;
public UserManager(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
}
token
提示
若默认的 Claim 里面内容不满足业务无需,可重新登录接口自行设定 Claim 内容。系统内置服务接口一般都是 partial 类型,并且都是虚方法 virtual,实体类扩展也类似。 比如移动端登录时自定义 Token时,根据业务需要塞进 Claim 里面内容所需关键字即可,此时记得扩展用户管理类 Admin.NET.Core/Service/User/UserManager.cs,继承此类即可。
创建 Token 示例:
/// <summary>
/// 生成Token令牌 🔖
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
[NonAction]
public virtual async Task<LoginOutput> CreateToken(SysUser user)
{
// 单用户登录
await _sysOnlineUserService.SingleLogin(user.Id);
// 生成Token令牌
var tokenExpire = await _sysConfigService.GetTokenExpire();
var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>
{
{ ClaimConst.UserId, user.Id },
{ ClaimConst.TenantId, user.TenantId },
{ ClaimConst.Account, user.Account },
{ ClaimConst.RealName, user.RealName },
{ ClaimConst.AccountType, user.AccountType },
{ ClaimConst.OrgId, user.OrgId },
{ ClaimConst.OrgName, user.SysOrg?.Name },
{ ClaimConst.OrgType, user.SysOrg?.Type },
}, tokenExpire);
// 生成刷新Token令牌
var refreshTokenExpire = await _sysConfigService.GetRefreshTokenExpire();
var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, refreshTokenExpire);
// 设置响应报文头
_httpContextAccessor.HttpContext.SetTokensOfResponseHeaders(accessToken, refreshToken);
// Swagger Knife4UI-AfterScript登录脚本
// ke.global.setAllHeader('Authorization', 'Bearer ' + ke.response.headers['access-token']);
return new LoginOutput
{
AccessToken = accessToken,
RefreshToken = refreshToken
};
}
后端授权 实现,自动验证、刷新 Token Admin.NET.Web.Core/Handlers/JwtHandler.cs
using Admin.NET.Core;
using Admin.NET.Core.Service;
using Furion;
using Furion.Authorization;
using Furion.DataEncryption;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;
namespace Admin.NET.Web.Core
{
public class JwtHandler : AppAuthorizeHandler
{
private readonly IServiceProvider _serviceProvider;
public JwtHandler(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
/// <summary>
/// 自动刷新Token
/// </summary>
/// <param name="context"></param>
/// <param name="httpContext"></param>
/// <returns></returns>
public override async Task HandleAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext)
{
// var serviceProvider = context.GetCurrentHttpContext().RequestServices;
using var serviceScope = _serviceProvider.CreateScope();
// 若当前账号存在黑名单中则授权失败
var sysCacheService = serviceScope.ServiceProvider.GetRequiredService<SysCacheService>();
if (sysCacheService.ExistKey($"{CacheConst.KeyBlacklist}{context.User.FindFirst(ClaimConst.UserId)?.Value}"))
{
context.Fail();
context.GetCurrentHttpContext().SignoutToSwagger();
return;
}
var sysConfigService = serviceScope.ServiceProvider.GetRequiredService<SysConfigService>();
var tokenExpire = await sysConfigService.GetTokenExpire();
var refreshTokenExpire = await sysConfigService.GetRefreshTokenExpire();
if (JWTEncryption.AutoRefreshToken(context, context.GetCurrentHttpContext(), tokenExpire, refreshTokenExpire))
{
await AuthorizeHandleAsync(context);
}
else
{
context.Fail(); // 授权失败
var currentHttpContext = context.GetCurrentHttpContext();
if (currentHttpContext == null)
return;
// 跳过由于 SignatureAuthentication 引发的失败
if (currentHttpContext.Items.ContainsKey(SignatureAuthenticationDefaults.AuthenticateFailMsgKey))
return;
currentHttpContext.SignoutToSwagger();
}
}
public override async Task<bool> PipelineAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext)
{
// 已自动验证 Jwt Token 有效性
return await CheckAuthorizeAsync(httpContext);
}
/// <summary>
/// 权限校验核心逻辑
/// </summary>
/// <param name="httpContext"></param>
/// <returns></returns>
private static async Task<bool> CheckAuthorizeAsync(DefaultHttpContext httpContext)
{
// 排除超管权限
if (App.User.FindFirst(ClaimConst.AccountType)?.Value == ((int)AccountTypeEnum.SuperAdmin).ToString())
return true;
// 接口路由权限
var path = httpContext.Request.Path.ToString();
var apis = await App.GetRequiredService<SysRoleService>().GetUserApiList();
return apis.Exists(u => path.Contains(u, StringComparison.CurrentCulture));
}
}
}
前端通过 Web/src/utils/axios-utils.ts 自动解析 Token,访问自动带上 Token,自动判断是否过期、用刷新 Token 去换新 Token、更新本地 Token 都是自动化,无需手动处理。
/**
* 解密 JWT token 的信息
* @param token jwt token 字符串
* @returns <any>object
*/
export function decryptJWT(token: string): any {
token = token.replace(/_/g, '/').replace(/-/g, '+');
var json = decodeURIComponent(escape(window.atob(token.split('.')[1])));
return JSON.parse(json);
}