最近想要学习一下 .net core 2.1 相关的知识,即是因为工作需要亦是在微服务和 docker 化的今天不得不去了解了解 .net core。API 第一步即是安全,即为认证(Authentication)以及授权(Authorization)
这次项目中在公司第一次接触到了 Oauth2.0 认证 token,其中使用的type 是Bearer,还要其他的常用认证模式 Basic、Digest,Basic 是属于明文认证(明文传输用户账号密码 只是对信息做了 BASE64 编码),但是 Digest 是加密认证,更加安全。
而Bearer 则是伴随着 Oauth 兴起的较为标准的认证,认证失败的情况下会把我们导向401(Unauthorized,未授权)状态码,并在WWW-Authenticate头中添加如何进行验证的信息。
通过 Bearer 认证的标准方式如下:
通过在请求的 Header 中添加如下 Header(注意 Bearer 后必须跟一个空格)
Authorization:Bearer [Access_Token]
这里引用一下大佬对 Bearer 的好处总结原文地址
- CORS: cookies + CORS 并不能跨不同的域名。而Bearer验证在任何域名下都可以使用HTTP header头部来传输用户信息。
- 对移动端友好: 当你在一个原生平台(iOS, Android, WindowsPhone等)时,使用Cookie验证并不是一个好主意,因为你得和Cookie容器打交道,而使用Bearer验证则简单的多。
- CSRF: 因为Bearer验证不再依赖于cookies, 也就避免了跨站请求攻击。
- 标准:在Cookie认证中,用户未登录时,返回一个
302
到登录页面,这在非浏览器情况下很难处理,而Bearer验证则返回的是标准的401 challenge
一直看到都说 JWT,我还以为是图形化界面(深受JAVA之毒害 ),这里是一种 Bearer Token 的编码方式,有三部分组成,每个部分由 .
隔开:
这三部分如下:
)
{
"alg":"HS256",
"type":"JWT"
}
Not Safe!!
,不要保存一些敏感信息在这个位置
),在代码中这部分是System.Security.ClaimsClaim
的一个实力化列表,其中可以包含如下的信息:
{ "iss": "lilibuy.com",
"iat": 1532588852,
"exp": 1533193652,
"aud": "lilibuy.com",
"sub": "891532752",
"nickname": "smilesb101",
"username": "123456",
"scopes": [ "admin", "user" ]
}
由于密钥是Service这边提供并且混入了前面的第一部分以及第二部分的值来混淆最终的签名值,所以会较为安全)
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
SECREATE_KEY
)
一开始是创建项目,这个不多说了,创建一个 .net core 2.1 的 API 项目
修改 appsettings.json,添加如下节点,这里配置好了密钥用于JWT Token 的第三部分签名
"JWT":{
"SecurityKey":"ABCDEFGHIJKLMNOPQRSTUVWXYZ1456789513"
},
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseMvc();
}
o.Events = new JwtBearerEvents()
{
OnMessageReceived = context =>
{
context.Token = context.Request.Query["access_token"];
return Task.CompletedTask;
}
};
这个方法就是为了我们能够验证通过 Url 传递过来的 JWT Token,当然你也可以进行其他的一些定义,比如还要验证是否拥有其他Header,或者其他必要的参数。
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "lilibuy.com",
ValidAudience = "lilibuy.com",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:SecurityKey"]))
};
这一个就是对验证的一些设置,比如是否验证发布者,订阅者,密钥,以及生命时间等等
注册了认证之后,认证并没有作用于 Controller 之上,所以我们需要通过给Controller 添加 AuthorizeAttribute 特性类标签来让认证在这个 Controller 上生效。
我们还是在示例工程的 ValuesController 上面动手脚,添加了认证后的代码如下
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LiliBuyMasterService.Controllers
{
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResultstring>> Get()
{
return new string[] { "value1", "value2" };
}
}
}
现在访问这个Controller 会提示 401
我们要如何取得认证呢?可以想得到我们现在还需要一个接口来检查认证信息以及返回认证的结果给用户使用
我们新建一个 OauthController,这个API Controller 类需要完成用户信息的比对以及如果比对结果显示这个是合法的用户,我们需要给用户返回 Token 信息。
修改后的 OauthController 如下
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;
using LiliBuyMasterService.Models.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
namespace LiliBuyMasterService.Controllers
{
[AllowAnonymous]
[Route("api/[controller]")]
public class OauthController : Controller
{
public IConfiguration Configuration { get; }
public OauthController(IConfiguration configuration)
{
Configuration = configuration;
}
[HttpPost("authenticate")]
public IActionResult RequestToken([FromBody]TokenRequest request)
{
if (request != null)
{
//验证账号密码,这里只是为了demo,正式场景应该是与DB之类的数据源比对
if ("smilesb101".Equals(request.UserName) && "123456".Equals(request.Password))
{
var claims = new[] {
//加入用户的名称
new Claim(ClaimTypes.Name,request.UserName)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:SecurityKey"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var authTime = DateTime.UtcNow;
var expiresAt = authTime.AddDays(7);
var token = new JwtSecurityToken(
issuer: "lilibuy.com",
audience: "lilibuy.com",
claims: claims,
expires: expiresAt,
signingCredentials: creds);
return Ok(new
{
access_token = new JwtSecurityTokenHandler().WriteToken(token),
token_type = "Bearer",
profile = new
{
name = request.UserName,
auth_time = new DateTimeOffset(authTime).ToUnixTimeSeconds(),
expires_at = new DateTimeOffset(expiresAt).ToUnixTimeSeconds()
}
});
}
}
return BadRequest("Could not verify username and password.Pls check your information.");
}
}
}
这里面的 AllowAnonymous 属性是说明这个不需要用户登录,
Return 里面的东西则是对我们具体的返回体的一个定义,至少包括token,其他信息可以具体和使用 API 或者根据业务约定。
我们来访问一下这个接口试一下,先试一个错误的用户信息吧
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoic21pbGVzYjEwMSIsImV4cCI6MTUzMzM3MzEzMSwiaXNzIjoibGlsaWJ1eS5jb20iLCJhdWQiOiJsaWxpYnV5LmNvbSJ9._DEksb3er3z-4ePaCMoFUzk0PDBV-MW5VTevS7C5Fas",
"token_type": "Bearer",
"profile": {
"name": "smilesb101",
"auth_time": 1532768331,
"expires_at": 1533373131
}
}
我们在再把这个信息加入到对 ValuesController 的请求里面去,我们由于实现的是默认的认证方式,所以这里我们也通过标准的方式来修改我们的 GET 请求,在 Header中加入如下信息:
Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoic21pbGVzYjEwMSIsImV4cCI6MTUzMzM3MzEzMSwiaXNzIjoibGlsaWJ1eS5jb20iLCJhdWQiOiJsaWxpYnV5LmNvbSJ9._DEksb3er3z-4ePaCMoFUzk0PDBV-MW5VTevS7C5Fas
这里 Bearer+空格是一个固定的写法,我们这里使用的就是 Bearer 认证所以这里是 Bearer,添加了 Header 的请求如下
到这里,一个基本的获取 Token 的认证就完成了。代码地址