前面吐槽了一番 现在来记录下后端项目的搭建
当前 将写core项目的基本搭建,token鉴权的获取与使用
首先我们看下搭建的基本样子:
一、项目搭建
- 要搭建core 5 的项目需要将vs2019升级到最新。好像是升级到16版本就可以了
- 启动vs2019 找到这个ASP.NET Core 空项目 这是空的,什么都需要自己建,也可以选择其他有默认文件的
- 选择保存位置和设置项目名称就可以了
- 上面完成后会到这个页面 默认是选择3.1版本的 可以选择修改掉
上面是否启动Docker 看自己的需要,,之后点击创建就可以了
至此项目已经创建完成了 - 创建完成就是如下的样子了 一个空空如也的项目
appsettings.json:以键值对的形式保存的配置文件
Program.cs:项目的启动入口 一般不做修改
Startup.cs:core项目的配置入口,不管是AOP、过滤器还是管道的注册与使用 都是在这里面做配置,也是中间件的入口
二、配置JWT权限认证
- 添加一个类库 注意是core的类库 目标框架也要选择是net5
然后创建一个JWT的文件夹,这个文件夹里面存放token的相关配置和验证
添加如下两个类库
使用引入如下的nugget包:
Microsoft.IdentityModel.Tokens
System.IdentityModel.Tokens.Jwt
Microsoft.Extensions.Options
Newtonsoft.Json
Microsoft.AspNetCore.Mvc.Abstractions
Microsoft.AspNetCore.Mvc.Formatters.Json
之后创建如下的文件:
TokenType.cs: 存放token
public enum TokenType
{
Ok,
Fail,
Expired
}
/// <summary>
/// 存放Token 跟过期时间的类
/// </summary>
public class TnToken
{
/// <summary>
/// token
/// </summary>
public string TokenStr { get; set; }
/// <summary>
/// 过期时间
/// </summary>
public DateTime Expires { get; set; }
}
BaseResultModel:统一的返回实体
public class BaseResultModel
{
public BaseResultModel(int? code = null, string message = null,
object result = null, ReturnStatus returnStatus = ReturnStatus.Success)
{
this.Code = code;
this.Data = result;
this.Msg = message;
this.ReturnStatus = returnStatus;
}
public int? Code { get; set; }
public string Msg { get; set; }
public object Data { get; set; }
public ReturnStatus ReturnStatus { get; set; }
/// <summary>
/// token令牌
/// </summary>
public TnToken TnToken { get; set; }
}
public enum ReturnStatus
{
Success = 1,
Fail = 0,
ConfirmIsContinue = 2,
Error = -1
}
ErrCode:统一返回错误管理
/// <summary>
/// 错误代码描述
/// </summary>
public static class ErrCode
{
/// <summary>
/// 请求成功 文本
/// </summary>
public static string err0 = "请求成功";
/// <summary>
/// 请求成功代码 200
/// </summary>
public static int successCode = 200;
/// <summary>
/// 服务器错误代码 500
/// </summary>
public static int serverErrCode = 500;
/// <summary>
/// 请求失败 文本
/// </summary>
public static string err1 = "请求失败";
/// <summary>
/// 请求失败代码 201
/// </summary>
public static int failCode = 201;
/// <summary>
/// 获取access_token时AppID或AppSecret错误。请开发者认真比对appid和AppSecret的正确性,或查看是否正在为恰当的应用调用接口
/// </summary>
public static string err40001 = "获取access_token时AppID或AppSecret错误。请开发者认真比对appid和AppSecret的正确性,或查看是否正在为恰当的应用调用接口";
/// <summary>
/// 调用接口的服务器URL地址不正确,请联系供应商进行设置
/// </summary>
public static string err40002 = "调用接口的服务器URL地址不正确,请联系供应商进行授权";
/// <summary>
/// 没有token
/// </summary>
public static string err40003 = "未获取到用户令牌token。请开发者验证token是否包含在请求报文的头部中 并且key值为:\"token\" ";
/// <summary>
/// 不合法的凭证类型
/// </summary>
public static string err40004 = "不合法的凭证类型";
/// <summary>
/// 用户令牌accesstoken超时失效
/// </summary>
public static string err40005 = "用户令牌accesstoken超时失效";
/// <summary>
/// 您未被授权使用该功能,请重新登录试试或联系管理员进行处理
/// </summary>
public static string err40006 = "您未被授权使用该功能,请重新登录试试或联系系统管理员进行处理";
/// <summary>
/// 传递参数出现错误
/// </summary>
public static string err40007 = "传递参数出现错误";
/// <summary>
/// 用户未登录或超时
/// </summary>
public static string err40008 = "用户未登录或超时";
/// <summary>
/// token验证失败
/// </summary>
public static string err40009 = "token验证失败";
/// <summary>
/// 更新数据失败
/// </summary>
public static string err43001 = "新增数据失败";
/// <summary>
/// 更新数据失败
/// </summary>
public static string err43002 = "更新数据失败";
/// <summary>
/// 物理删除数据失败
/// </summary>
public static string err43003 = "删除数据失败";
/// <summary>
/// 程序异常提示
/// </summary>
public static string err500 = "程序异常";
/// <summary>
/// 该用户不存在
/// </summary>
public static string err50001 = "该用户不存在";
/// <summary>
/// 该用户已存在
/// </summary>
public static string err50002 = "用户已存在,请登录或重新注册!";
/// <summary>
/// 会员注册失败
/// </summary>
public static string err50003 = "会员注册失败";
/// <summary>
/// 查询数据不存在
/// </summary>
public static string err60001 = "查询数据不存在";
}
JWTConfig.cs :配置token生成信息
public class JWTConfig
{
/// <summary>
/// 用于识别用户信息
/// </summary>
public string UID { get; set; }
/// <summary>
/// Token发布者
/// </summary>
public string Issuer { get; set; }
/// <summary>
/// oken接受者
/// </summary>
public string Audience { get; set; }
/// <summary>
/// 秘钥
/// </summary>
public string IssuerSigningKey { get; set; }
/// <summary>
/// 过期时间
/// </summary>
public int AccessTokenExpiresMinutes { get; set; }
}
ITokenHelper.cs: token工具类的接口,方便使用依赖注入,很简单提供两个常用的方法
public interface ITokenHelper
{
/// <summary>
/// 根据一个对象通过反射提供负载生成token
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="user"></param>
/// <returns></returns>
TnToken CreateToken<T>(T user) where T : class;
/// <summary>
/// 根据键值对提供负载生成token
/// </summary>
/// <param name="keyValuePairs"></param>
/// <returns></returns>
TnToken CreateToken(Dictionary<string, string> keyValuePairs);
/// <summary>
/// Token验证
/// </summary>
/// <param name="encodeJwt">token</param>
/// <param name="validatePayLoad">自定义各类验证; 是否包含那种申明,或者申明的值</param>
/// <returns></returns>
bool ValiToken(string encodeJwt, Func<Dictionary<string, string>, bool> validatePayLoad = null);
/// <summary>
/// 带返回状态的Token验证
/// </summary>
/// <param name="encodeJwt">token</param>
/// <param name="validatePayLoad">自定义各类验证; 是否包含那种申明,或者申明的值</param>
/// <param name="action"></param>
/// <returns></returns>
TokenType ValiTokenState(string encodeJwt, Func<Dictionary<string, string>, bool> validatePayLoad, Action<Dictionary<string, string>> action);
}
然后实例上面的接口方法
TokenHelper.cs:
public class TokenHelper : ITokenHelper
{
private readonly IOptions<JWTConfig> _options;
public TokenHelper(IOptions<JWTConfig> options)
{
_options = options;
}
/// <summary>
/// 根据一个对象通过反射提供负载生成token
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="user"></param>
/// <returns></returns>
public TnToken CreateToken<T>(T user) where T : class
{
//携带的负载部分,类似一个键值对
List<Claim> claims = new List<Claim>();
//这里我们用反射把model数据提供给它
foreach (var item in user.GetType().GetProperties())
{
object obj = item.GetValue(user);
string value = "";
if (obj != null)
value = obj.ToString();
claims.Add(new Claim(item.Name, value));
}
//创建token
return CreateToken(claims);
}
/// <summary>
/// 根据键值对提供负载生成token
/// </summary>
/// <param name="keyValuePairs"></param>
/// <returns></returns>
public TnToken CreateToken(Dictionary<string, string> keyValuePairs)
{
//携带的负载部分,类似一个键值对
List<Claim> claims = new();
//这里我们通过键值对把数据提供给它
foreach (var item in keyValuePairs)
{
claims.Add(new Claim(item.Key, item.Value));
}
//创建token
return CreateTokenString(claims);
}
/// <summary>
/// 生成token
/// </summary>
/// <param name="claims">List的 Claim对象</param>
/// <returns></returns>
private TnToken CreateTokenString(List<Claim> claims)
{
var now = DateTime.Now;
var expires = now.Add(TimeSpan.FromMinutes(_options.Value.AccessTokenExpiresMinutes));
var token = new JwtSecurityToken(
issuer: _options.Value.Issuer,//Token发布者
audience: _options.Value.Audience,//Token接受者
claims: claims,//携带的负载
notBefore: now,//当前时间token生成时间
expires: expires,//过期时间
signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Value.IssuerSigningKey)), SecurityAlgorithms.HmacSha256));
return new TnToken { TokenStr = new JwtSecurityTokenHandler().WriteToken(token), Expires = expires };
}
/// <summary>
/// 验证身份 验证签名的有效性
/// </summary>
/// <param name="encodeJwt"></param>
/// <param name="validatePayLoad">自定义各类验证; 是否包含那种申明,或者申明的值, </param>
public bool ValiToken(string encodeJwt, Func<Dictionary<string, string>, bool> validatePayLoad = null)
{
var success = true;
var jwtArr = encodeJwt.Split('.');
if (jwtArr.Length < 3)//数据格式都不对直接pass
{
return false;
}
_ = JsonConvert.DeserializeObject<Dictionary<string, string>>(Base64UrlEncoder.Decode(jwtArr[0]));
var payLoad = JsonConvert.DeserializeObject<Dictionary<string, string>>(Base64UrlEncoder.Decode(jwtArr[1]));
//配置文件中取出来的签名秘钥
var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(_options.Value.IssuerSigningKey));
//验证签名是否正确(把用户传递的签名部分取出来和服务器生成的签名匹配即可)
success = success && string.Equals(jwtArr[2],
Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(jwtArr[0], ".", jwtArr[1])))));
if (!success)
{
return success;//签名不正确直接返回
}
//其次验证是否在有效期内(也应该必须)
var now = ToUnixEpochDate(DateTime.UtcNow);
success = success && (now >= long.Parse(payLoad["nbf"].ToString()) && now < long.Parse(payLoad["exp"].ToString()));
//不需要自定义验证不传或者传递null即可
if (validatePayLoad == null)
return true;
//再其次 进行自定义的验证
success = success && validatePayLoad(payLoad);
return success;
}
/// <summary>
/// 时间转换
/// </summary>
/// <param name="date"></param>
/// <returns></returns>
private long ToUnixEpochDate(DateTime date)
{
return (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);
}
/// <summary>
///
/// </summary>
/// <param name="encodeJwt"></param>
/// <param name="validatePayLoad"></param>
/// <param name="action"></param>
/// <returns></returns>
public TokenType ValiTokenState(string encodeJwt, Func<Dictionary<string, string>, bool> validatePayLoad, Action<Dictionary<string, string>> action)
{
if (string.IsNullOrEmpty(encodeJwt))
{
return TokenType.Fail;
}
var jwtArr = encodeJwt.Split('.');
if (jwtArr.Length < 3)//数据格式 不对直接pass
{
return TokenType.Fail;
}
_ = JsonConvert.DeserializeObject<Dictionary<string, string>>(Base64UrlEncoder.Decode(jwtArr[0]));
var payLoad = JsonConvert.DeserializeObject<Dictionary<string, string>>(Base64UrlEncoder.Decode(jwtArr[1]));
var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(_options.Value.IssuerSigningKey));
//验证签名是否正确(把用户传递的签名部分取出来和服务器生成的签名匹配即可)
if (!string.Equals(jwtArr[2], Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(jwtArr[0], ".", jwtArr[1]))))))
{
return TokenType.Fail;
}
//其次验证是否在有效期内(必须验证)
var now = ToUnixEpochDate(DateTime.UtcNow);
if (!(now >= long.Parse(payLoad["nbf"].ToString()) && now < long.Parse(payLoad["exp"].ToString())))
{
return TokenType.Expired;
}
//不需要自定义验证不传或者传递null即可
if (validatePayLoad == null)
{
action(payLoad);
return TokenType.Ok;
}
//再其次 进行自定义的验证
if (!validatePayLoad(payLoad))
{
return TokenType.Fail;
}
//可能需要获取jwt摘要里边的数据,封装一下方便使用
action(payLoad);
return TokenType.Ok;
}
}
然后添加相关的引入就可以
TokenFilter.cs:token的过滤器 验证权限就在这里做
public class TokenFilter : Attribute, IActionFilter
{
private readonly ITokenHelper tokenHelper;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IOptions<JWTConfig> _options;
//通过依赖注入得到数据访问层实例
public TokenFilter(ITokenHelper _tokenHelper, IHttpContextAccessor httpContextAccessor, IOptions<JWTConfig> options)
{
tokenHelper = _tokenHelper;
//获取头部
_httpContextAccessor = httpContextAccessor;
_options = options;
}
public void OnActionExecuted(ActionExecutedContext context)
{
}
public void OnActionExecuting(ActionExecutingContext context)
{
BaseResultModel ret = new();
try
{
//获取token
object tokenobj = null;
//tokenobj = context.ActionArguments["token"];
var headers = _httpContextAccessor.HttpContext.Request.Headers;
tokenobj = headers["token"];
if (tokenobj == null || string.IsNullOrWhiteSpace(tokenobj.ToString()))
{
ret.Code = 40003;
ret.Msg = ErrCode.err40003;
context.Result = new JsonResult(ret);
return;
}
string token = tokenobj.ToString();
string userId = "";
//验证jwt,同时取出来jwt里边的用户ID
TokenType tokenType = tokenHelper.ValiTokenState(token,
a => a["iss"] == _options.Value.Issuer && a["aud"] == _options.Value.Audience,
action => { userId = action[_options.Value.UID]; });
if (tokenType == TokenType.Fail)
{
ret.Code = 40009;
ret.Msg = ErrCode.err40009;
context.Result = new JsonResult(ret);
return;
}
if (tokenType == TokenType.Expired)
{
ret.Code = 40008;
ret.Msg = ErrCode.err40008;
context.Result = new JsonResult(ret);
}
if (!string.IsNullOrEmpty(userId))
{
//给控制器传递参数(需要什么参数其实可以做成可以配置的,在过滤器里边加字段即可)
//context.ActionArguments.Add("userId", Convert.ToInt32(userId));
}
//如果token值只剩半小时 则重新生成token并存入返回体
}
catch (Exception ex)
{
ret.Code = ErrCode.serverErrCode;
ret.Msg = "token验证失败,异常:" + ex.Message;
context.Result = new JsonResult(ret);
}
}
}
在appsettings文件里面加入如下代码:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
//JWT 令牌相关配置
"JWTConfig": {
"UID": "UID", //存入的用户识别,,注意与JWTConfig中的配置一致
"Issuer": "October", //Token发布者
"Audience": "OCT", //Token接受者
"IssuerSigningKey": "October&YL889455200IBook_API", //秘钥可以构建服务器认可的token;签名秘钥长度最少16
"AccessTokenExpiresMinutes": "180" //过期时间 分钟
}
}
相关的设置做完之后 我们进行token的注入并使用
回到Startup文件中
在iBook-core中添加引用:
Microsoft.AspNetCore.Authentication.JwtBearer
添加AOP项目的引入
Startup 中的 ConfigureServices 添加如下代码:
namespace iBook_core
{
public class Startup
{
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
#region jwt配置
services.AddTransient<ITokenHelper, TokenHelper>();
//读取配置文件配置的jwt相关配置
services.Configure<JWTConfig>(Configuration.GetSection("JWTConfig"));
//启用JWT
services.AddAuthentication(Options =>
{
Options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
Options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).
AddJwtBearer();
//注入token过滤器
services.AddScoped<TokenFilter>();
#endregion
}
}
}
测试使用:
添加测试的控制器
添加两个接口:
namespace iBook_core.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
private readonly ITokenHelper _tokenHelper;
private readonly BaseResultModel model;
public ValuesController(ITokenHelper tokenHelper)
{
_tokenHelper = tokenHelper;
model = new();
}
/// <summary>
/// 获取token
/// </summary>
/// <param name="uid"></param>
/// <returns></returns>
[HttpGet]
[Route("getToken")]
public IActionResult GetToken(string uid)
{
Dictionary<string, string> keyValuePairs = new()
{
{ "UID", uid }
};
var token = _tokenHelper.CreateToken(keyValuePairs);
model.Code = 200;
model.Msg = "获取token成功";
model.Data = token;
model.TnToken = token;
return Ok(model);
}
[ServiceFilter(typeof(TokenFilter))]// 在需要token验证的地方加上这个注解 也可以直接加在控制器上 那么那个控制器下所有的接口都将需要提供token才可以
[Route("getToken")]
[HttpPost]
public IActionResult verifyToken()
{
return null;
}
}
}
添加 Swagger ,这里可以不添加,用接口处测试工具进行测试也是可以的
引入
Swashbuckle.AspNetCore
将 Startup 修改如下:
namespace iBook_core
{
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "iBook-core", Version = "v1" });
});
#region jwt配置
services.AddTransient<ITokenHelper, TokenHelper>();
//读取配置文件配置的jwt相关配置
services.Configure<JWTConfig>(Configuration.GetSection("JWTConfig"));
//启用JWT
services.AddAuthentication(Options =>
{
Options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
Options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).
AddJwtBearer();
//注入token过滤器
services.AddScoped<TokenFilter>();
//注入请求头部解析
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
#endregion
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "iBook-core v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
//启用认证中间件 要写在授权UseAuthorization()的前面
app.UseAuthentication();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
启动项目
如果swagger没正常跳转 那就在地址加上https://localhost:44351/swagger/index.html
token获取:
token验证失败的情况:
[ServiceFilter(typeof(TokenFilter))]
// 在需要token验证的地方加上这个注解 也可以直接加在控制器上 那么那个控制器下所有的接口都将需要提供token才可以
这个jwt的使用是几年前在其他地方看到的,找不到原作者了 就没法加上转载链接。为了配合自己的项目 改了一些。
若有侵权 联系删除