用JWT来保护我们的ASP.NET Core Web API

在用Middleware给ASP.NET Core Web API添加自己的授权验证 中,自己动手写了一个Middleware来处理API的授权验证,现在就采用另外一种方式来处理这个授权验证的问题,毕竟现在也有不少开源的东西可以用,今天用的是JWT。

  什么是JWT呢?JWT的全称是JSON WEB TOKENS,是一种自包含令牌格式。官方网址:https://jwt.io/,或多或少应该都有听过这个。

  先来看看下面的两个图:

  站点是通过RPC的方式来访问api取得资源的,当站点是直接访问api,没有拿到有访问权限的令牌,那么站点是拿不到相关的数据资源的。

就像左图展示的那样,发起了请求但是拿不到想要的结果;当站点先去授权服务器拿到了可以访问api的access_token(令牌)后,再通过这个

access_token去访问api,api才会返回受保护的数据资源。

  这个就是基于令牌验证的大致流程了。可以看出授权服务器占着一个很重要的地位。

  下面先来看看授权服务器做了些什么并如何来实现一个简单的授权。

  做了什么?授权服务器在整个过程中的作用是:接收客户端发起申请access_token的请求,并校验其身份的合法性,最终返回一个包含

access_token的json字符串。

  如何实现?我们还是离不开中间件这个东西。这次我们写了一个TokenProviderMiddleware,主要是看看invoke方法和生成access_token

的方法。

/// <summary>

        /// invoke the middleware

        /// </summary>

        /// <param name="context"></param>

        /// <returns></returns>

        public async Task Invoke(HttpContext context)

        {           

            if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal))

            {

                await _next(context);

            }


            // Request must be POST with Content-Type: application/x-www-form-urlencoded

            if (!context.Request.Method.Equals("POST")

               || !context.Request.HasFormContentType)

            {

                await ReturnBadRequest(context);             

            }

            await GenerateAuthorizedResult(context);

        }

  Invoke方法其实是不用多说的,不过我们这里是做了一个控制,只接收POST请求,并且是只接收以表单形式提交的数据,GET的请求和其

他contenttype类型是属于非法的请求,会返回bad request的状态。

  下面说说授权中比较重要的东西,access_token的生成。

/// <summary>

        /// get the jwt

        /// </summary>

        /// <param name="username"></param>

        /// <returns></returns>

        private string GetJwt(string username)

        {

            var now = DateTime.UtcNow;


            var claims = new Claim[]

            {

                new Claim(JwtRegisteredClaimNames.Sub, username),

                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),

                new Claim(JwtRegisteredClaimNames.Iat, now.ToUniversalTime().ToString(),

                          ClaimValueTypes.Integer64)

            };


            var jwt = new JwtSecurityToken(

                issuer: _options.Issuer,

                audience: _options.Audience,

                claims: claims,

                notBefore: now,

                expires: now.Add(_options.Expiration),

                signingCredentials: _options.SigningCredentials);

            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);


            var response = new

            {

                access_token = encodedJwt,

                expires_in = (int)_options.Expiration.TotalSeconds,

                token_type = "Bearer"

            };   

            return JsonConvert.SerializeObject(response, new JsonSerializerSettings { Formatting = Formatting.Indented });

        }

  

  claims包含了多个claim,你想要那几个,可以根据自己的需要来添加,JwtRegisteredClaimNames是一个结构体,里面包含了所有的可选项。

public struct JwtRegisteredClaimNames

    {

        public const string Acr = "acr";

        public const string Actort = "actort";

        public const string Amr = "amr";

        public const string AtHash = "at_hash";

        public const string Aud = "aud";

        public const string AuthTime = "auth_time";

        public const string Azp = "azp";

        public const string Birthdate = "birthdate";

        public const string CHash = "c_hash";

        public const string Email = "email";

        public const string Exp = "exp";

        public const string FamilyName = "family_name";

        public const string Gender = "gender";

        public const string GivenName = "given_name";

        public const string Iat = "iat";

        public const string Iss = "iss";

        public const string Jti = "jti";

        public const string NameId = "nameid";

        public const string Nbf = "nbf";

        public const string Nonce = "nonce";

        public const string Prn = "prn";

        public const string Sid = "sid";

        public const string Sub = "sub";

        public const string Typ = "typ";

        public const string UniqueName = "unique_name";

        public const string Website = "website";

    }


JwtRegisteredClaimNames


还需要一个JwtSecurityToken对象,这个对象是至关重要的。有了时间、Claims和JwtSecurityToken对象,只要调用JwtSecurityTokenHandler

的WriteToken就可以得到类似这样的一个加密之后的字符串,这个字符串由3部分组成用‘.’分隔。每部分代表什么可以去官网查找。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

  最后我们要用json的形式返回这个access_token、access_token的有效时间和一些其他的信息。

  还需要在Startup的Configure方法中去调用我们的中间件。

var audienceConfig = Configuration.GetSection("Audience");

            var symmetricKeyAsBase64 = audienceConfig["Secret"];

            var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64);

            var signingKey = new SymmetricSecurityKey(keyByteArray);


            app.UseTokenProvider(new TokenProviderOptions

            {

                Audience = "Catcher Wong",

                Issuer = "http://catcher1994.cnblogs.com/",

                SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256),

            });


  到这里,我们的授权服务站点已经是做好了。下面就编写几个单元测试来验证一下这个授权。

  测试一:授权服务站点能生成正确的jwt。

[Fact]

        public async Task authorized_server_should_generate_token_success()

        {

            //arrange

            var data = new Dictionary<string, string>();

            data.Add("username", "Member");

            data.Add("password", "123");

            HttpContent ct = new FormUrlEncodedContent(data);


            //act

            System.Net.Http.HttpResponseMessage message_token = await _client.PostAsync("http://127.0.0.1:8000/auth/token", ct);

            string res = await message_token.Content.ReadAsStringAsync();

            var obj = Newtonsoft.Json.JsonConvert.DeserializeObject<Token>(res);


            //assert

            Assert.NotNull(obj);

            Assert.Equal("600", obj.expires_in);

            Assert.Equal(3, obj.access_token.Split('.').Length);

            Assert.Equal("Bearer", obj.token_type);

        }

 

  测试二:授权服务站点因为用户名或密码不正确导致不能生成正确的jwt。


[Fact]

        public async Task authorized_server_should_generate_token_fault_by_invalid_app()

        {

            //arrange

            var data = new Dictionary<string, string>();

            data.Add("username", "Member");

            data.Add("password", "123456");

            HttpContent ct = new FormUrlEncodedContent(data);


            //act

            System.Net.Http.HttpResponseMessage message_token = await _client.PostAsync("http://127.0.0.1:8000/auth/token", ct);

            var res = await message_token.Content.ReadAsStringAsync();

            dynamic obj = Newtonsoft.Json.JsonConvert.DeserializeObject(res);


            //assert

            Assert.Equal("invalid_grant", (string)obj.error);

            Assert.Equal(HttpStatusCode.BadRequest, message_token.StatusCode);

        }

 

  测试三:授权服务站点因为不是发起post请求导致不能生成正确的jwt。


Fact]

        public async Task authorized_server_should_generate_token_fault_by_invalid_httpmethod()

        {

            //arrange

            Uri uri = new Uri("http://127.0.0.1:8000/auth/token?username=Member&password=123456");


            //act

            System.Net.Http.HttpResponseMessage message_token = await _client.GetAsync(uri);

            var res = await message_token.Content.ReadAsStringAsync();

            dynamic obj = Newtonsoft.Json.JsonConvert.DeserializeObject(res);


            //assert

            Assert.Equal("invalid_grant", (string)obj.error);

            Assert.Equal(HttpStatusCode.BadRequest, message_token.StatusCode);

        }

 

  再来看看测试的结果:

   

  都通过了。

  断点拿一个access_token去http://jwt.calebb.net/ 解密看看

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJNZW1iZXIiLCJqdGkiOiI2MzI1MmE1My0yMjY5LTQ4YzEtYmQwNi1lOWRiMzdmMTRmYTQiLCJpYXQiOiIyMDE2LzExLzEyIDI6NDg6MTciLCJuYmYiOjE0Nzg5MTg4OTcsImV4cCI6MTQ3ODkxOTQ5NywiaXNzIjoiaHR0cDovL2NhdGNoZXIxOTk0LmNuYmxvZ3MuY29tLyIsImF1ZCI6IkNhdGNoZXIgV29uZyJ9.Cu2vTJ4JAHgbJGzwv2jCmvz17HcyOsRnTjkTIEA0EbQ

  下面就是API的开发了。

  这里是直接用了新建API项目生成的ValueController作为演示,毕竟跟ASP.NET Web API是大同小异的。这里的重点是配置

JwtBearerAuthentication,这里是不用我们再写一个中间件了,我们是定义好要用的Option然后直接用JwtBearerAuthentication就可以了。


public void ConfigureJwtAuth(IApplicationBuilder app)

        {            

            var audienceConfig = Configuration.GetSection("Audience");

            var symmetricKeyAsBase64 = audienceConfig["Secret"];

            var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64);

            var signingKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(keyByteArray);            


            var tokenValidationParameters = new TokenValidationParameters

            {

                // The signing key must match!

                ValidateIssuerSigningKey = true,

                IssuerSigningKey = signingKey,


                // Validate the JWT Issuer (iss) claim

                ValidateIssuer = true,

                ValidIssuer = "http://catcher1994.cnblogs.com/",


                // Validate the JWT Audience (aud) claim

                ValidateAudience = true,

                ValidAudience = "Catcher Wong",


                // Validate the token expiry

                ValidateLifetime = true,

         

                ClockSkew = TimeSpan.Zero

            };


            app.UseJwtBearerAuthentication(new JwtBearerOptions

            {

                AutomaticAuthenticate = true,

                AutomaticChallenge = true,

                TokenValidationParameters = tokenValidationParameters,

            });                        

        }

 

  然后在Startup的Configure中调用上面的方法即可。


public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)

        {

            loggerFactory.AddConsole(Configuration.GetSection("Logging"));

            loggerFactory.AddDebug();


            ConfigureJwtAuth(app);


            app.UseMvc();

        }

  到这里之后,大部分的工作是已经完成了,还有最重要的一步,在想要保护的api上加上Authorize这个Attribute,这样Get这个方法就会要

求有access_token才会返回结果,不然就会返回401。这是在单个方法上的,也可以在整个控制器上面添加这个Attribute,这样控制器里面的方

法就都会受到保护。


// GET api/values/5

        [HttpGet("{id}")]

        [Authorize]

        public string Get(int id)

        {

            return "value";

        }

  OK,同样编写几个单元测试验证一下。

  测试一:valueapi在没有授权的请求会返回401状态。

[Fact]

        public void value_api_should_return_unauthorized_without_auth()

        {           

            //act         

            HttpResponseMessage message = _client.GetAsync("http://localhost:63324/api/values/1").Result;

            string result = message.Content.ReadAsStringAsync().Result;

         

            //assert

            Assert.False(message.IsSuccessStatusCode);

            Assert.Equal(HttpStatusCode.Unauthorized,message.StatusCode);

            Assert.Empty(result);

        }


  

   测试二:valueapi请求没有[Authorize]标记的方法时能正常返回结果。


[Fact]

        public void value_api_should_return_result_without_authorize_attribute()

        {

            //act         

            HttpResponseMessage message = _client.GetAsync("http://localhost:63324/api/values").Result;

            string result = message.Content.ReadAsStringAsync().Result;

            var res = Newtonsoft.Json.JsonConvert.DeserializeObject<string[]>(result);


            //assert

            Assert.True(message.IsSuccessStatusCode);

            Assert.Equal(2, res.Length);

        }

   测试三:valueapi在授权的请求中会返回正确的结果。


[Fact]

        public void value_api_should_success_by_valid_auth()

        {

            //arrange

            var data = new Dictionary<string, string>();

            data.Add("username", "Member");

            data.Add("password", "123");

            HttpContent ct = new FormUrlEncodedContent(data);


            //act

            var obj = GetAccessToken(ct);                        

            _client.DefaultRequestHeaders.Add("Authorization", "Bearer " + obj.access_token);

            HttpResponseMessage message = _client.GetAsync("http://localhost:63324/api/values/1").Result;

            string result = message.Content.ReadAsStringAsync().Result;


            //assert

            Assert.True(message.IsSuccessStatusCode);

            Assert.Equal(3, obj.access_token.Split('.').Length);

            Assert.Equal("value",result);            

        }

   再来看看测试的结果:

  

  测试通过。

  再通过浏览器直接访问那个受保护的方法。响应头就会提示www-authenticate:Bearer,这个是身份验证的质询,告诉客户端必须要提供相

应的身份验证才能访问这个资源(api)。

   

   这也是为什么在单元测试中会添加一个Header的原因,正常的使用也是要在请求的报文头中加上这个。

   _client.DefaultRequestHeaders.Add("Authorization""Bearer " + obj.access_token); 

  其实看一下源码,更快知道为什么。JwtBearerHandler.cs

  下图是关于头部加Authorization的源码解释。

 

  

 

原文地址:http://www.cnblogs.com/catcher1994/p/6057484.html


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/327907.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JDK 6中新增的Java Console类功能概览

转载自 JDK 6中新增的Java Console类功能概览JDK 6中提供了java.io.Console类专用来访问基于字符的控制台设备。如果你的Java程序要与Windows下的cmd或者Linux下的Terminal交互&#xff0c;就可以用这个Java Console类代劳。 TestConsole.java 代码&#xff1a;package com.si…

mysql---批量插入数据:100w条数据

DROP FUNCTION IF EXISTS mock_data; DELIMITER $$ --写函数之前必写 CREATE FUNCTION mock_data() RETURNS INT BEGIN DECLARE num INT DEFAULT 1000000; DECLARE i INT DEFAULT 0; WHILE i < num DO INSERT INTO app_user(name, email, phone, gender, password, age) VAL…

一次性加载树结构数据表 mapper加载

************************************************************* Override public List<Organization> getOrganizationTree() throws UnionException {//缓存有就从缓存拿List<Organization> redisObjectOrganizationList (List<Organization>) redisObj…

DIP原则、IoC以及DI

一、DIP原则 高层模块不应该依赖于底层模块&#xff0c;二者都应该依赖于抽象。抽象不应该依赖于细节&#xff0c;细节应该依赖于抽象。 该原则理解起来稍微有点抽象&#xff0c;我们可以将该原则通俗的理解为&#xff1a;"依赖于抽象”。 该规则告诉我们&#xff0c;程序…

Java IO最详解

转载自 Java IO最详解初学java&#xff0c;一直搞不懂java里面的io关系&#xff0c;在网上找了很多大多都是给个结构图草草描述也看的不是很懂。而且没有结合到java7 的最新技术&#xff0c;所以自己来整理一下&#xff0c;有错的话请指正&#xff0c;也希望大家提出宝贵意见…

hibernate在分层架构中修改数据(update)时遇到的问题!!

开发软件&#xff1a;Myeclipse 10.0 数据库&#xff1a;oracle 开发人员&#xff1a;1111 问题简单描述&#xff1a;修改数据的时候不能正常修改&#xff0c;要么修改不成功&#xff0c;要么报错 nice,下面就来看看怎么解决这个bug的。 首先&#xff0c;我做的是一个租房网站&…

intellij idea 如何一键清除所有断点

intellij idea 如何一键清除所有断点 2017-06-19 11:37:20 yanziit 阅读数 50429更多 分类专栏&#xff1a; idea工具 我之前写了一个百度经验,但是搜不到,现在复制一遍,自己留个记录. 注:此方法适用 intellij idea 2016.2.5版本,其他版本我没用过,暂时不知道 1.在idea左下…

整理下.net分布式系统架构的思路

最近看到有部分招聘信息&#xff0c;要求应聘者说一下分布式系统架构的思路。今天早晨正好有些时间&#xff0c;我也把我们实际在.net方面网站架构的演化路线整理一下&#xff0c;只是我自己的一些想法&#xff0c;欢迎大家批评指正。 首先说明的是.net下开源内容较少&#xff…

SQL注入问题及预防方法

SQL注入问题 sql存在漏洞&#xff0c;会被攻击导致数据泄露 SQL会被拼接 or package com.kuang.lesson02; import com.kuang.lesson02.utils.jdbcUtils; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; …

Java transient关键字使用小记

转载自 Java transient关键字使用小记1. transient的作用及使用方法我们都知道一个对象只要实现了Serilizable接口&#xff0c;这个对象就可以被序列化&#xff0c;java的这种序列化模式为开发者提供了很多便利&#xff0c;我们可以不必关系具体序列化的过程&#xff0c;只要…

mysql sample函数_Oracle SAMPLE 语法应用

Sample抽样函数用于支持数据挖掘。Sample 函数使得数据分析操作在样本数据上进行&#xff0c;而不是在整张表上进行。Sample抽样函数用于支持数据挖掘。Sample 函数使得数据分析操作在样本数据上进行&#xff0c;而不是在整张表上进行。选择10%的记录select * from atest sampl…

docker 买了腾讯服务器后的学习

腾讯云买了 打算用 登录成功后&#xff1a; Last login: Mon Nov 11 13:37:02 2019 from 221.12.17.87 [rootVM_0_13_centos ~]# ip addr #查看ip地址 [rootVM_0_13_centos ~]# uname -r #查看centeros内核版本 docker要求 centeros内核在3.10以上 3.10.0-862.e…

Hibernate中使用Criteria查询及注解——(Dept.java)

Dept.java: 部门表的实体类&#xff1a; package cn.bdqn.hibernate_Criteria.entity;import java.util.HashSet; import java.util.Set;/*** 部门表的实体类* author 1111**/ public class Dept implements java.io.Serializable {// Fieldsprivate Integer deptno;private St…

Connect 2016 白话脱口秀将在B站直播,我们的口号是quot; 微软大法好quot;

今年 Connect 大会的主题是 Big possibilities. Bold technology. 北京时间 11月16日 23&#xff1a;00&#xff0c;Connect();//2016 将开启在线直播&#xff0c;届时红衣主教 Scott Guthrie 和 Scott Hanselman 携众多微软技术大咖将为大家带来超级精彩的主题演讲。丰富的内容…

MySQL(笔记)

数据库总览 有时候查的数据错乱&#xff0c;可以重启MySQL 关系型数据库 ( SQL ) MySQL , Oracle , SQL Server , SQLite , DB2 , …关系型数据库通过外键关联来建立表与表之间的关系 非关系型数据库 ( NOSQL )not only Redis , MongoDB , …非关系型数据库通常指数据以对…

第10章尚硅谷SpringBoot检索

第10章尚硅谷SpringBoot检索 P20、尚硅谷-SpringBoot高级-检索-Elasticsearch简介&安装 P21、尚硅谷-SpringBoot高级-检索-Elasticsearch快速入门 P22、尚硅谷-SpringBoot高级-检索-SpringBoot整合Jest操作ES P23、尚硅谷-SpringBoot高级-检索-整合SpringDataElasticsearch…

python 高维数据_Python数据分析入门|利用NumPy高效处理高维数据

矢量化NumPy数组可以将许多数据处理任务表述为简洁的数组表达式&#xff0c;否则需要编写循环。用数组表达式代替循环的做法&#xff0c;通常被称为矢量化。通常矢量化数组运算要比等价的纯Python方式快上一两个数量级&#xff0c;尤其是各种数值计算。假设我们想要在一组值(网…

Hi Visual Studio for Mac

今晚Connect 2016 , 或者你会兴奋地看到Visual Studio 2017在Docker上调试 &#xff0c; MS SQL on Linux &#xff0c;Azure Functions 还有一堆黑技术....但个人还是十分喜欢一个新产品Visual Studio for Mac 。 八个多月前微软收购了Xamarin, 对于Xamarin 的IDE进行了整合&a…

Python3总结

一、输入输出 &#xff08;1&#xff09;控制台输入 input(promptNone) &#xff08;2&#xff09;控制台输出 print([obj1,...][,sep ][,end\n][,filesys.stdout][,flushFalse])二、数据类型 &#xff08;1&#xff09;整数 class int(x, base10) &#xff08;2&#xff09;浮…

Hibernate中使用Criteria查询及注解——(Emp.java)

Emp.java 员工表的实体类&#xff1a; package cn.bdqn.hibernate_Criteria.entity;import java.util.Date;/*** 员工表的实体类* author Administrator**/ public class Emp implements java.io.Serializable {private Integer empno;private Dept dept;private String ename;…