搭建认证服务器
搭建步骤
1、新建一个minimapAPI项目
2、添加JWTTokenOptions实体类
public class JWTTokenOptions
{
public string? Audience { get; set; }
public string? SecurityKey { get; set; }
public string? Issuer { get; set; }
}
3、在配置文件中写入元信息
"JWTTokenOptions": {
"Audience": "http://localhost:5200",
"Issuer": "http://localhost:5200",
"SecurityKey": "我是一个秘钥,秘钥长度尽量保证在16个字符以上"
}
4、在Program.cs中注册
//代表获取配置文件的JWTTokenOptions节点,映射到JWTTokenOptions对象中去
builder.Services.Configure<JWTTokenOptions>(builder.Configuration.GetSection("JWTTokenOptions"));
5、创建生成Token的抽象及实现类
抽象
public interface ICustomJWTService
{
string GetToken(CurrentUser user);
}
对称可逆加密
通过构造函数注入的方式,读取到配置文件的信息
对称可逆加密的Key可以是任意的
public class CustomHSJWTService : ICustomJWTService
{
#region Option注入
private readonly JWTTokenOptions _JWTTokenOptions;
public CustomHSJWTService(IOptionsMonitor<JWTTokenOptions> jwtTokenOptions)
{
_JWTTokenOptions = jwtTokenOptions.CurrentValue;
}
#endregion
/// <summary>
/// 获取Token
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
public string GetToken(CurrentUser user)
{
//1、准备有效载荷
Claim[] claims = new[]
{
new Claim(ClaimTypes.Name, user.Name),
new Claim("NickName",user.NikeName),
new Claim(ClaimTypes.Role,user.RoleList),//传递其他信息
new Claim("Description",user.Description),
new Claim("Age",user.Age.ToString()),
};
//2、准备加密key
SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_JWTTokenOptions.SecurityKey));
//Sha256 加密方式
SigningCredentials creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
JwtSecurityToken token = new JwtSecurityToken(
issuer: _JWTTokenOptions.Issuer,
audience: _JWTTokenOptions.Audience,
claims: claims,
expires: DateTime.Now.AddMinutes(5),//5分钟有效期
signingCredentials: creds);
string returnToken = new JwtSecurityTokenHandler().WriteToken(token);
return returnToken;
}
}
非对称可逆加密的Key需要帮助类库来生成
添加帮助类库
TryGetKeyParameters:判断路径中是否存在key,包含则读取,不包含则返回false
public class RSAHelper
{
/// <summary>
/// 从本地文件中读取用来签发 Token 的 RSA Key
/// </summary>
/// <param name="filePath">存放密钥的文件夹路径</param>
/// <param name="withPrivate"></param>
/// <param name="keyParameters"></param>
/// <returns></returns>
public static bool TryGetKeyParameters(string filePath, bool withPrivate, out RSAParameters keyParameters)
{
//存放的文件名:私钥key.json 公钥key.public.json
string filename = withPrivate ? "key.json" : "key.public.json";
string fileTotalPath = Path.Combine(filePath, filename);
keyParameters = default;
if (!File.Exists(fileTotalPath))
{
return false;
}
else
{
keyParameters = JsonConvert.DeserializeObject<RSAParameters>(File.ReadAllText(fileTotalPath));
return true;
}
}
/// <summary>
/// 生成并保存 RSA 公钥与私钥
/// </summary>
/// <param name="filePath">存放密钥的文件夹路径</param>
/// <returns></returns>
public static RSAParameters GenerateAndSaveKey(string filePath, bool withPrivate = true)
{
RSAParameters publicKeys, privateKeys;
using (var rsa = new RSACryptoServiceProvider(2048))//即时生成
{
try
{
privateKeys = rsa.ExportParameters(true);
publicKeys = rsa.ExportParameters(false);
}
finally
{
rsa.PersistKeyInCsp = false;
}
}
File.WriteAllText(Path.Combine(filePath, "key.json"), JsonConvert.SerializeObject(privateKeys));
File.WriteAllText(Path.Combine(filePath, "key.public.json"), JsonConvert.SerializeObject(publicKeys));
return withPrivate ? privateKeys : publicKeys;
}
}
非对称可逆加密
注意,想要在token中写入角色信息,不能自己写一个字符串"Role"作为键,而要用官方的ClaimTypes.Role(本质是一个长字符串)
//错误写法
new Claim("Role",user.RoleList!);
//正确写法
new Claim(ClaimTypes.Role,user.RoleList!);
public class CustomRSSJWTervice:ICustomJWTService
{
#region Option注入
private readonly JWTTokenOptions _JWTTokenOptions;
public CustomRSSJWTervice(IOptionsMonitor<JWTTokenOptions> jwtTokenOptions)
{
_JWTTokenOptions = jwtTokenOptions.CurrentValue;
}
/// <summary>
/// 返回token
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public string GetToken(CurrentUser user)
{
#region 使用加密解密Key 非对称
//指定当前的执行目录
string keyDir = Directory.GetCurrentDirectory();
if (RSAHelper.TryGetKeyParameters(keyDir, true, out RSAParameters keyParams) == false)
{
keyParams = RSAHelper.GenerateAndSaveKey(keyDir);
}
#endregion
//1、准备有效载荷
Claim[] claims = new[]
{
new Claim(ClaimTypes.Name, user.Name!),
new Claim("NickName",user.NikeName!),
new Claim(ClaimTypes.Role,user.RoleList!),//传递其他信息 "Role"
new Claim("http://schemas.microsoft.com/ws/2008/06/identity/claims/role","teacher"),//传递其他信息 "Role"
new Claim(ClaimTypes.Role,"Student"),
new Claim(ClaimTypes.Role,"User"),
new Claim("Description",user.Description!),
new Claim("Age",user.Age.ToString()),
};
//2、准备加密key
RsaSecurityKey key = new RsaSecurityKey(keyParams);
//Sha256 加密方式
SigningCredentials creds = new SigningCredentials(key, SecurityAlgorithms.RsaSha256Signature);
JwtSecurityToken token = new JwtSecurityToken(
issuer: _JWTTokenOptions.Issuer,
audience: _JWTTokenOptions.Audience,
claims: claims,
expires: DateTime.Now.AddMinutes(5),//5分钟有效期
signingCredentials: creds);
string returnToken = new JwtSecurityTokenHandler().WriteToken(token);
return returnToken;
}
#endregion
}
6、调用生成token的方法
builder.Services.AddTransient<ICustomJWTService, CustomHSJWTService>();//对称可逆加密
builder.Services.AddTransient<ICustomJWTService, CustomRSSJWTervice>();//非对称可逆加密
app.MapPost("Login", (string name, string password, ICustomJWTService _iJWTService) =>
{
//在这里需要去数据库中做数据验证
if ("Richard".Equals(name) && "123456".Equals(password))
{
//从数据库中查询出来的
var user = new CurrentUser()
{
Id = 123,
Name = "Richard",
Age = 36,
NikeName = "金牌讲师Richard老师",
Description = ".NET架构师",
RoleList = "admin"
};
//就应该生成Token
string token = _iJWTService.GetToken(user);
return JsonConvert.SerializeObject(new
{
result = true,
token
});
}
else
{
return JsonConvert.SerializeObject(new
{
result = false,
token = ""
});
}
});
授权
鉴权 --- 读取请求方带过来的各种渠道的用户信息,解析写入到HttpContext.user中去
授权 ---根据用户的信息,去检查当前用户是否可以去请求资源
如何对控制器类、方法授权访问
ASP.NET Core会按照HTTP协议的规范,从Authorization取出来令牌,并且进行校验、解析,然后把解析结果填充到User用户属性中,这一切都是ASP.NET Core完成的,不需要开发人员自己编写代码
- 控制器类上标注
[Authorize],则所有操作方法都会被进行身份验证和授权验证 - 对于标注了[Authorize]的控制器中,如果其中某个操作方法不想被验证,可以在操作方法上添加
[AllowAnonymous] - 如果没有在控制器类上标注[Authorize],那么这个控制器中的所有操作方法都允许被自由地访问
- 对于没有标注[Authorize]的控制器中,如果其中某个操作方法需要被验证,可以在操作方法上添加[Authorize]
一旦出现401,没有详细的报错信息,很难排查,这是初学者遇到的难题
使用步骤
1、新建JWTTokenOptions类。在配置文件中写入配置信息
public class JWTTokenOptions
{
public string? Audience { get; set; }
public string? SecurityKey { get; set; }
public string? Issuer { get; set; }
}
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"JWTTokenOptions": {
"Audience": "http://localhost:5200",
"Issuer": "http://localhost:5200",
"SecurityKey": "我是一个秘钥,秘钥长度尽量保证在16个字符以上"
}
}
2、在Program.cs中,启用鉴权、授权
对称可逆加密的鉴权、授权
JWTTokenOptions tokenOptions = new JWTTokenOptions();
builder.Configuration.Bind("JWTTokenOptions", tokenOptions);
builder.Services
.AddAuthorization() //启用授权
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)//鉴权,参数是鉴权的渠道jwt
.AddJwtBearer(options => //这里是配置的具体鉴权的逻辑
{
//授权时,需要先检查的参数
options.TokenValidationParameters = new TokenValidationParameters
{
//JWT有一些默认的属性,就是给鉴权时就可以筛选了
ValidateIssuer = true,//是否验证Issuer
ValidateAudience = true,//是否验证Audience
ValidateLifetime = true,//是否验证失效时间
ValidateIssuerSigningKey = true,//是否验证SecurityKey
ValidAudience = tokenOptions.Audience,//
ValidIssuer = tokenOptions.Issuer,//Issuer,这两项和前面签发jwt的设置一致
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tokenOptions.SecurityKey)),
AudienceValidator = (m, n, z) =>
{
//这里可以写自己定义的验证逻辑
//return m != null && m.FirstOrDefault().Equals(builder.Configuration["audience"]);
return true;
},
LifetimeValidator = (notBefore, expires, securityToken, validationParameters) =>
{
//return notBefore <= DateTime.Now
//&& expires >= DateTime.Now;
////&& validationParameters
return true;
}//自定义校验规则
};
});
}
app.UseAuthentication(); //鉴权
app.UseAuthorization(); //授权
非对称可逆加密的鉴权、授权
需要将公钥解密的key交给鉴权授权的项目
//先读取到公钥key
string path = Path.Combine(Directory.GetCurrentDirectory(), "key.public.json");
string key = File.ReadAllText(path);//this.Configuration["SecurityKey"];
Console.WriteLine($"KeyPath:{path}");
//反序列化为对象
RSAParameters keyParams = JsonConvert.DeserializeObject<RSAParameters>(key);
//读取配置文件的信息
JWTTokenOptions tokenOptions = new JWTTokenOptions();
builder.Configuration.Bind("JWTTokenOptions", tokenOptions);
builder.Services
.AddAuthorization(Options =>
{
//策略授权
Options.AddPolicy("PammionPolicy", builder =>
{
//在这里就是判断逻辑
builder.RequireRole("admin");
builder.RequireUserName("Richard");
builder.RequireAssertion(context =>
{
HttpContext httpcontext = context.Resource as HttpContext;
//if (context.User.FindFirst(c => c.Type == "Role") == null)
//{
// return false;
//}
//else
//{
// return true;
//}
//context.User.Claims.Count(c=>c.Type== ClaimTypes.Role)>=3
return true;
});
//扩展策略授权,PermissionRequirement是自定义的
builder.Requirements.Add(new PermissionRequirement());
});
}) //启用授权
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,//是否验证Issuer
ValidateAudience = true,//是否验证Audience
ValidateLifetime = true,//是否验证失效时间
ValidateIssuerSigningKey = true,//是否验证SecurityKey
ValidAudience = tokenOptions.Audience,//Audience
ValidIssuer = tokenOptions.Issuer,//Issuer,这两项和前面签发jwt的设置一致
IssuerSigningKey = new RsaSecurityKey(keyParams),
IssuerSigningKeyValidator = (m, n, z) =>
{
Console.WriteLine("This is IssuerValidator");
return true;
},
IssuerValidator = (m, n, z) =>
{
Console.WriteLine("This is IssuerValidator");
return "http://localhost:5726";
},
AudienceValidator = (m, n, z) =>
{
Console.WriteLine("This is AudienceValidator");
return true;
//return m != null && m.FirstOrDefault().Equals(this.Configuration["Audience"]);
},//自定义校验规则,可以新登录后将之前的无效
};
});
3、方法标注[Authorize]特性
app.MapGet("getStringPara", [Authorize] (string str) => str)
Swagger配置支持Token传参数
1、添加安全定义
2、添加安全要求
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new() { Title = "朝夕教育 MinimalApi", Version = "v1" });
options.SwaggerDoc("v2", new() { Title = "朝夕教育 MinimalApi", Version = "v2" });
options.OperationFilter<SwaggerFileUploadFilter>();
#region Swagger配置支持Token参数传递
//添加安全定义
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "请输入token,格式为 Bearer xxxxxxxx(注意中间必须有空格)",
Name = "Authorization",
In = ParameterLocation.Header,//从头信息传进来
Type = SecuritySchemeType.ApiKey,
BearerFormat = "JWT",
Scheme = "Bearer" //JwtBearerDefaults.AuthenticationScheme
});
//添加安全要求
options.AddSecurityRequirement(new OpenApiSecurityRequirement {
{
new OpenApiSecurityScheme
{
Reference =new OpenApiReference()
{
Type = ReferenceType.SecurityScheme,
Id ="Bearer"
}
},
new string[]{ }
}
});
#endregion
});
角色授权
先鉴权---再授权
在鉴权以后,已经正常解析了用户的信息,开始授权,就是判定用户信息中是否包含某一个角色;
//如果需要满足多个角色,多个角色之间并且关系---可以标记多个特性,分别指定角色
app.MapGet("getStringPara",
[Authorize(Roles = "admin")][Authorize(Roles = "student")]
(string str) => str);
//如果是或者关系,标记一次特性,角色以逗号分割
app.MapGet("getStringPara",
[Authorize(Roles = "admin,Student,Richard")]
(string str) => str);
策略授权
定义策略
在API标记特性的时候,指定策略,那么访问当前API,就需要按照定义的策略来授权验证。
app.MapGet("getStringPara",
[Authorize(policy: "PammionPolicy")]
(string str) => str);
扩展策略授权
1、添加PermissionRequirement
public class PermissionRequirement: IAuthorizationRequirement
{
}
2、添加PermissionHandler,用于验证业务逻辑
public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
//下面这个只是个注入示例
private ICompanyService _ICompanyService;
public PermissionHandler(ICompanyService companyService)
{
this._ICompanyService= companyService;
}
/// <summary>
/// 在这里就可以扩展逻辑
/// </summary>
/// <param name="context"></param>
/// <param name="requirement"></param>
/// <returns></returns>
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
HttpContext httpContext = context.Resource as HttpContext;
bool? isAuthenticated = context?.User?.Identity?.IsAuthenticated; //是否解析到用户信息了
if (isAuthenticated != null && isAuthenticated.Value)
{
context?.Succeed(requirement); // 验证成功了刷
}
else
{
context?.Fail();
}
return Task.CompletedTask;
}
}
3、注入自定义策略处理类型
builder.Services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
封装优化
为了解决什么问题?所有代码都写到了Program.cs中,导致代码太长,可以进行封装
示例:将鉴权授权的方法进行封装
1、新建静态类及封装的扩展方法
public static class AuthenticationExtension
{
public static void AuthenticationHsExt(this WebApplicationBuilder builder)
{
#region Jwt对称可逆加密方式-鉴权授权
{
JWTTokenOptions tokenOptions = new JWTTokenOptions();
builder.Configuration.Bind("JWTTokenOptions", tokenOptions);
builder.Services
.AddAuthorization() //启用授权
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => //这里是配置的鉴权的逻辑
{
options.TokenValidationParameters = new TokenValidationParameters
{
//JWT有一些默认的属性,就是给鉴权时就可以筛选了
ValidateIssuer = true,//是否验证Issuer
ValidateAudience = true,//是否验证Audience
ValidateLifetime = true,//是否验证失效时间
ValidateIssuerSigningKey = true,//是否验证SecurityKey
ValidAudience = tokenOptions.Audience,//
ValidIssuer = tokenOptions.Issuer,//Issuer,这两项和前面签发jwt的设置一致
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tokenOptions.SecurityKey)),
AudienceValidator = (m, n, z) =>
{
//这里可以写自己定义的验证逻辑
//return m != null && m.FirstOrDefault().Equals(builder.Configuration["audience"]);
return true;
},
LifetimeValidator = (notBefore, expires, securityToken, validationParameters) =>
{
//return notBefore <= DateTime.Now
//&& expires >= DateTime.Now;
////&& validationParameters
return true;
}//自定义校验规则
};
});
}
#endregion
}
public static void AuthenticationRssExt(this WebApplicationBuilder builder)
{
string path = Path.Combine(Directory.GetCurrentDirectory(), "key.public.json");
string key = File.ReadAllText(path);//this.Configuration["SecurityKey"];
Console.WriteLine($"KeyPath:{path}");
RSAParameters keyParams = JsonConvert.DeserializeObject<RSAParameters>(key);
JWTTokenOptions tokenOptions = new JWTTokenOptions();
builder.Configuration.Bind("JWTTokenOptions", tokenOptions);
builder.Services
.AddAuthorization(Options =>
{
Options.AddPolicy("PammionPolicy", builder =>
{
//在这里就是判断逻辑
builder.RequireRole("admin");
builder.RequireUserName("Richard");
builder.RequireAssertion(context =>
{
HttpContext httpcontext = context.Resource as HttpContext;
//if (context.User.FindFirst(c => c.Type == "Role") == null)
//{
// return false;
//}
//else
//{
// return true;
//}
//context.User.Claims.Count(c=>c.Type== ClaimTypes.Role)>=3
return true;
});
builder.Requirements.Add(new PermissionRequirement());
});
}) //启用授权
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,//是否验证Issuer
ValidateAudience = true,//是否验证Audience
ValidateLifetime = true,//是否验证失效时间
ValidateIssuerSigningKey = true,//是否验证SecurityKey
ValidAudience = tokenOptions.Audience,//Audience
ValidIssuer = tokenOptions.Issuer,//Issuer,这两项和前面签发jwt的设置一致
IssuerSigningKey = new RsaSecurityKey(keyParams),
IssuerSigningKeyValidator = (m, n, z) =>
{
Console.WriteLine("This is IssuerValidator");
return true;
},
IssuerValidator = (m, n, z) =>
{
Console.WriteLine("This is IssuerValidator");
return "http://localhost:5726";
},
AudienceValidator = (m, n, z) =>
{
Console.WriteLine("This is AudienceValidator");
return true;
//return m != null && m.FirstOrDefault().Equals(this.Configuration["Audience"]);
},//自定义校验规则,可以新登录后将之前的无效
};
});
}
}
2、使用此扩展方法
builder.AuthenticationRssExt();