文章目录
前言
Web API安全措施的主要流程:
角色主要有三类:应用程序、授权机构、资源(包含Web API)
安全措施的流程:注册信息、身份验证、创建Token、验证Token、授权
从图中可以看出,首先会在授权机构中进行身份信息注册(保存到数据库),然后应用程序将身份信息发送给授权机构。身份信息通过后,授权机构会返回Token给应用程序,应用程序请求资源时将Token一起发送,资源会通过授权机构验证Token,通过后会将资源返回给应用程序,从而实现Web API的安全。
以下是Web API安全措施的具体实现
一、注册信息
通过界面等方式将信息注册保存到数据库中,此处直接硬编码将信息写到程序里(省略注册的具体实现)。
定义程序类(代表数据库中保存的注册信息):
public class Apllication
{
public int ApplicationId { get; set; }
public string? ApplicationName { get; set; }
public string? ClientId { get; set; }
public string? Secret { get; set; }
public string? Scopes { get; set; }
}
硬编码的注册信息:
public static class AppRepositry
{
private static List<Apllication> _applications = new List<Apllication>()
{
new Apllication
{
ApplicationId = 1,
ApplicationName = "Test",
ClientId = "123",
Secret = "admin",
Scopes = "read,write"
}
};
public static Apllication? GetApllicationByClientId(string clientId)
{
return _applications.FirstOrDefault(x => x.ClientId == clientId);
}
}
二、身份验证
应用程序向授权机构发送请求,验证id和密码是否正确,若正确则返回Token。
请求方法:
[HttpPost("auth")]
public IActionResult Authenticate([FromBody]AppCredential credential)
{
var expiresAt = DateTime.UtcNow.AddMinutes(10);
if (Authenticator.Authenticate(credential.ClientId, credential.Secret))
{
return Ok(new
{
access_token = Authenticator.CreateToken(credential.ClientId, expiresAt, configuration.GetValue<string>("SecretKey")),
expires_at = expiresAt
});
}
else
{
ModelState.AddModelError("Unauthorized", "You are not authorized.");
var problemDetails = new ValidationProblemDetails(ModelState)
{
Status = StatusCodes.Status401Unauthorized
};
return new UnauthorizedObjectResult(problemDetails);
}
}
验证:
public static bool Authenticate(string clientId, string secret)
{
var app = AppRepositry.GetApllicationByClientId(clientId);
if (app == null) return false;
return (app.ClientId == clientId && app.Secret == secret);
}
三、创建Token
在进行身份验证时,就会创建Token,可以进行自定义Token(需要将token保存到数据库中,和数据库进行交互),这里使用JWT(不需与数据库交互)。
3.1 安装依赖
在Nuget安装System.IdentityModel.Tokens.Jwt
3.2 代码实现
JWT由算法、负载、签证三部分组成。算法就是加密算法(使用secretKey),负载就是封装的键值对信息,签证就是加密后的标识(服务端用来比对是否篡改过前两部分信息)。
secretKey在配置文件appsettings.json中,程序通过IConfiguration直接获取
代码如下:
public static object CreateToken(string clientId, DateTime expiresAt, string strSecretKey)
{
//算法
//负载
//签证
var secretKey = Encoding.ASCII.GetBytes(strSecretKey);
var app = AppRepositry.GetApllicationByClientId(clientId);
var claims = new List<Claim>() {
new Claim("AppName", app?.ApplicationName??string.Empty),
new Claim("Read", (app?.Scopes??string.Empty).Contains("read")?"true":"false"),
new Claim("Write", (app?.Scopes??string.Empty).Contains("write")?"true":"false")
};
var jwt = new JwtSecurityToken(
signingCredentials: new SigningCredentials(
new SymmetricSecurityKey(secretKey),
SecurityAlgorithms.HmacSha256Signature),
claims: claims,
expires: expiresAt,
notBefore: DateTime.UtcNow
);
return new JwtSecurityTokenHandler().WriteToken(jwt);
}
3.3 接口访问
直接使用id和密码访问认证接口,返回token
3.4 解析Token
打开https://jwt.io/网站,输入token信息,能看到负载信息。
四、验证Token
4.1 代码实现
验证Token代码如下:
public static bool VerifyToken(string token, string strSecretKey)
{
if (string.IsNullOrWhiteSpace(token)) return false;
var secretKey = Encoding.ASCII.GetBytes(strSecretKey);
SecurityToken securityToken;
try
{
var tokenHandler = new JwtSecurityTokenHandler();
tokenHandler.ValidateToken(token, new TokenValidationParameters()
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(secretKey),
ValidateLifetime = true,
ValidateAudience = false,
ValidateIssuer = false,
ClockSkew = TimeSpan.Zero
}, out securityToken);
}
catch (SecurityException)
{
return false;
}
catch (Exception)
{
throw;
}
return securityToken != null;
}
4.2 授权筛选器
使用授权筛选器应用Token验证
public class JwtTokenAuthFilterAttribute : Attribute, IAsyncAuthorizationFilter
{
public async Task OnAuthorizationAsync(AuthorizationFilterContext filterContext)
{
if (!filterContext.HttpContext.Request.Headers.TryGetValue("Authorization", out var token))
{
filterContext.Result = new UnauthorizedResult();
return;
}
var configuration = filterContext.HttpContext.RequestServices.GetService<IConfiguration>();
if (!Authenticator.VerifyToken(token, configuration.GetValue<string>("SecretKey")))
{
filterContext.Result = new UnauthorizedResult();
}
}
}
使用属性特性启动授权筛选器
4.3 功能验证
使用Postman在header里加上Authorization: token(jwt有时间限制)
不加token
五、授权
从JWT中获取负载信息(claim),将属性特性标注到属性上,在授权筛选器中比较属性信息和负载信息从而实现权限控制。
5.1 新建属性类
新建声明权限类
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true )]
public class RequiredClaimAttribute : Attribute
{
public string ClaimType { get; }
public string ClaimValue { get; }
public RequiredClaimAttribute(string claimType, string claimValue)
{
ClaimType = claimType;
ClaimValue = claimValue;
}
}
5.2 使用属性类
在控制器方法中声明权限属性值
5.3 判断权限
在授权筛选器中获取出属性特性中声明的权限,再从token里获取出负载信息里的权限,然后比较属性值。
public async Task OnAuthorizationAsync(AuthorizationFilterContext filterContext)
{
if (!filterContext.HttpContext.Request.Headers.TryGetValue("Authorization", out var token))
{
filterContext.Result = new UnauthorizedResult();
return;
}
var configuration = filterContext.HttpContext.RequestServices.GetService<IConfiguration>();
var claims = Authenticator.VerifyToken(token, configuration.GetValue<string>("SecretKey"));
if (claims == null)
{
filterContext.Result = new UnauthorizedResult(); //401
}
else
{
var requiredClaims = filterContext.ActionDescriptor.EndpointMetadata.OfType<RequiredClaimAttribute>().ToList();
// 403
if (requiredClaims != null && !requiredClaims.All(rc=>claims.Any(c=>c.Type.ToLower()==rc.ClaimType.ToLower() &&
c.Value.ToLower()==rc.ClaimValue.ToLower())))
{
filterContext.Result = new StatusCodeResult(403);
}
}
}