.Net Core 2.1 JWT Bearer 的认证

起因

最近想要学习一下 .net core 2.1 相关的知识,即是因为工作需要亦是在微服务和 docker 化的今天不得不去了解了解 .net core。API 第一步即是安全,即为认证(Authentication)以及授权(Authorization

Authentication 认证

这次项目中在公司第一次接触到了 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 的好处总结原文地址

  1. CORS: cookies + CORS 并不能跨不同的域名。而Bearer验证在任何域名下都可以使用HTTP header头部来传输用户信息。
  2. 对移动端友好: 当你在一个原生平台(iOS, Android, WindowsPhone等)时,使用Cookie验证并不是一个好主意,因为你得和Cookie容器打交道,而使用Bearer验证则简单的多。
  3. CSRF: 因为Bearer验证不再依赖于cookies, 也就避免了跨站请求攻击。
  4. 标准:在Cookie认证中,用户未登录时,返回一个302到登录页面,这在非浏览器情况下很难处理,而Bearer验证则返回的是标准的401 challenge

JWT(JSON WEB TOKEN)

一直看到都说 JWT,我还以为是图形化界面(深受JAVA之毒害 ),这里是一种 Bearer Token 的编码方式,有三部分组成,每个部分由 .隔开:
<first part>.<second part>.<third part>
这三部分如下:

  • 头部(Header)
    头部一般由alg(所使用的Hash 加密算法一般为 Hmac Sha256或者是 RSA)、type指的是 token 的传输和编码类型,我们这里为 JWT。把这以 BASE64 编码就成为了第一部分(<first part>

    {
    "alg":"HS256",
    "type":"JWT"
    }
  • 载荷(Payload)
    注意,这部分是BASE64编码 Not Safe!!,不要保存一些敏感信息在这个位置
    这部分存储了主要的用户信息以及认证信息,同样这部分也需要BASE64 编码成为第二部分(<second part>),在代码中这部分是System.Security.ClaimsClaim的一个实力化列表,其中可以包含如下的信息:
    标准中注册声明:比如认证时间,过期时间,用户信息,发布者,订阅者等
    公共的声明:这个是自定义的,可以添加一些业务上的信息,但是注意不要和标准(IANA JSON Web Token)重复,另外这个部分是可以解密的(BASE64 编码可对称解密),所以不要存储用户或者服务的敏感信息
    私有的声明:在这里存放一些发布者和订阅者共同约定的一些声明,但是这个只是 BASE64 编码(对称解密),所以也不要存放敏感信息。
    一个栗子

    { "iss": "lilibuy.com",
    "iat": 1532588852,
    "exp": 1533193652,
    "aud": "lilibuy.com",
    "sub": "891532752",
    "nickname": "smilesb101",
    "username": "123456",
    "scopes": [ "admin", "user" ]
    }

    具体描述可以查看
  • 签名(Signature)
    这部分加密之后成为第三部分(<third part>由于密钥是Service这边提供并且混入了前面的第一部分以及第二部分的值来混淆最终的签名值,所以会较为安全)
    这部分存在的意义就是为了解决前面部分的不安全性(利用前面配置的 alg算法以及 SecurityKey–Service 自己提供>128/8(即16位以上的)密钥),这部分是在 Service 端加解密的,加密算法如下 :

    HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    SECREATE_KEY
    )

实战一下

创建项目

一开始是创建项目,这个不多说了,创建一个 .net core 2.1 的 API 项目

添加 JWT 配置

修改 appsettings.json,添加如下节点,这里配置好了密钥用于JWT Token 的第三部分签名

"JWT":{
    "SecurityKey":"ABCDEFGHIJKLMNOPQRSTUVWXYZ1456789513"
},

修改 Startup.cs 注册 JWT

  • 修改后的 Configure(IApplicationBuilder app, IHostingEnvironment env) 方法如下:
    在这里添加了对验证的使用,注意调用UseAuthentication() 必须要在调用 UseMvc() 之前。
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseHsts();
            }

            app.UseAuthentication();

            app.UseHttpsRedirection();
            app.UseMvc();
        }
  • 修改后的 ConfigureServices(IServiceCollection services) 方法
    这里添加了一个 验证的处理方法,并且使用 JwtBearer 作为验证的处理,
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"]))
                    };

这一个就是对验证的一些设置,比如是否验证发布者,订阅者,密钥,以及生命时间等等

增加认证以及 Token 给予

给 Controller 增加认证

注册了认证之后,认证并没有作用于 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 ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "value1", "value2" };
        }
    }
}

现在访问这个Controller 会提示 401
401
我们要如何取得认证呢?可以想得到我们现在还需要一个接口来检查认证信息以及返回认证的结果给用户使用

Token 给予

我们新建一个 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 或者根据业务约定。
我们来访问一下这个接口试一下,先试一个错误的用户信息吧
UserName:smileSB101,password:1234567

再来试一试正确的
UserName:smilesb101,password:123456
这里可以看到我们已经拿到认证通过的信息了

{
"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 的请求如下
header

我们Send这个修改之后的请求,成功拿到数据:
成功

到这里,一个基本的获取 Token 的认证就完成了。代码地址

猜你喜欢

转载自blog.csdn.net/qq_21265915/article/details/81219294