如何简单的在 ASP.NET Core 中集成 JWT 认证?
前情提要:ASP.NET Core 使用 JWT 搭建分布式无状态身份验证系统
文章超长预警(1万字以上),不想看全部实现过程的同学可以直接跳转到末尾查看成果或者一键安装相关的 nuget 包
自上一篇介绍如何在 ASP.NET Core 中集成 JWT 的博文发布接近一年后,我又想来分享一些使用 JWT 的经验了。过去的一年中,我每次遇到一些小的,垃圾的项目,就会按照去年的那片文章来进行配置,虽然代码不多,但是每次写这么一些模板代码,又感觉很枯燥、冗余,而且稍不注意就有可能配置的有问题,导致验证不成功。前几天,我继续写自己的垃圾毕设,写到集成 JWT 的时候,我终于忍受不了这种重复的配置工作了,于是便着手封装一个简单易用的 JWT 插件。
之前集成 JWT 的方法在 ConfigureServices
方法里面添加了太多细节上的东西,所以在新的实现里面,添加服务依赖的 API 一定要足够简单,其次,之前的实现里面,签发一个 Token 步骤太多且比较复杂,所以签发 Token 的步骤也要简化。最后,之前在 Cookie 中添加 JWT 支持也比较 hack,跟 ASP.NET Core 的集成也不是很好。带着这些痛点,我在网上经历了一番搜索,最终找到了这个仓库 ,本来都想直接用他的实现了,不过他的配置看起来还是有些麻烦的,所以没办法,只好自己手写一个了。
从设计配置 API 开始
其实不管是我之前写的实现还是 GitHub 上找到的那个仓库的实现,最让我不满意的地方就是配置,很多时候,我就只想快速地搭建一个项目,根本不想去研究“怎样配置”,所以我的第一步的目标就是设计一个简单的配置接口:
public abstract class EasyJwtOption
{
public string Audience { get; set; }
public string Issuer { get; set; }
public bool EnableCookie { get; set; }
/// <summary>
/// 自定义 Cookie 选项,可空
/// </summary>
public Action<CookieAuthenticationOptions> CookieOptions { get; set; }
/// <summary>
/// 自定义 jwt 选项,可空
/// </summary>
public Action<JwtBearerOptions> JwtOptions { get; set; }
public abstract SecurityKey GenerateKey();
public abstract SigningCredentials GenerateCredentials();
}
EasyJwtOption
是用来进行描述 EasyJwt
配置的类型,它的每个属性都是我们可以进行配置的地方,同时为了避免把 ASP.NET Core 自带的对 JWT 跟 Cookie 的配置项目重写一遍,我就定义了 CookieOptions
跟 JwtOptions
这两个属性,用来向微软的 AuthenticationBuilder
传递配置。
GenerateKey()
GenerateCredentials()
这两个抽象方法则跟加密算法相关,在 JWT 中,我们可以使用两类算法进行加密:对称加密与非对称加密,在我之前写的文章中,我使用的是非对称加密的 RSA 算法,将原先的配置写成新的 EasyJwtOption
就是:
public class EasyRSAOptions : EasyJwtOption
{
public EasyRSAOptions(string path)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentException("Path can not be null", nameof(path));
}
Path = path;
}
public string Path { get; set; }
public override SecurityKey GenerateKey()
{
if (RsaUtils.TryGetKeyParameters(Path, true, out var rsaParams) == false)
{
rsaParams = RsaUtils.GenerateAndSaveKey(Path);
}
return new RsaSecurityKey(rsaParams);
}
public override SigningCredentials GenerateCredentials()
{
return new SigningCredentials(GenerateKey(), SecurityAlgorithms.RsaSha256);
}
}
由于 RSA 算法的私钥与密钥只能机器生成,所以我还是延续了以前的做法,把算法参数导出成 json 保存在本地,故 EasyRSAOptions
的构造函数接受一个存储位置作为必须参数。但是这种做法普适性不太好,更好的做法是把 RSA 私钥与公钥导出成标准格式的文本,这样其他的应用也可以导入,不过我比较懒,先这么凑活吧。
在 GitHub 找到的那个项目中,作者使用的是对称加密算法,把这个算法改成我的 EasyJwtOption
就是:
public class EasySymmetricOptions : EasyJwtOption
{
public EasySymmetricOptions(string secret)
{
Secret = secret ?? throw new ArgumentNullException(nameof(secret));
Secret = Secret.GetMd5();
}
public string Secret { get; set; }
public override SecurityKey GenerateKey()
{
return new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Secret));
}
public override SigningCredentials GenerateCredentials()
{
return new SigningCredentials(GenerateKey(), SecurityAlgorithms.HmacSha256);
}
}
在非对称加密算法中,我们需要提供一个密钥供加密、解密使用,所以 EasySymmetricOptions
的构造函数接受一个任意的字符串作为参数,又因为 SymmetricSecurityKey
对安全性的要求,密钥的长度太短会报出异常,用户的输入的密钥字符串进行了一些转换,来满足密钥长度条件。
方便的签发 Token
为了能够让网站的各个组件能够方便的随时签发 Token ,我设计了下面这个类,它的构造函数接受一个 EasyJwtOption
作为参数:
public class EasyJwt
{
private readonly EasyJwtOption _option;
public EasyJwt(EasyJwtOption option)
{
_option = option;
}
public string GenerateToken(string userName, IEnumerable<Claim> claims, DateTime expiratoin)
{
ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(userName));
identity.AddClaims(claims);
var handler = new JwtSecurityTokenHandler();
var token = handler.CreateEncodedJwt(new SecurityTokenDescriptor
{
Issuer = _option.Issuer,
Audience = _option.Audience,
SigningCredentials = _option.GenerateCredentials(),
Subject = identity,
Expires = expiratoin
});
return token;
}
}
只要我们在 Starpup.ConfigureServices
方法中把这个类添加进 IoC 容器,任何依赖 EasyJwt
的对象都可以非常简便的为用户生成 Token,调用方法大致如下:
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, userName, ClaimValueTypes.String)
};
var token = _jwt.GenerateToken(userName, claims, DateTime.Now.AddDays(1));
claims
是 Identity 中的概念,表示用户的信息,例如:用户名、邮箱。签发 token 需要指定用户名、用户相关的信息以及 token 过期时间。我们 EasyJwt
得到了签发 token 所需要的参数后会创建一个 ClaimsIdentity
对象,这同样也是 Identity 中的概念,用来表示用户的一些身份信息的集合,我们可以把一个 Identity 对象想象成一张通行证,上面记录着用户的身份信息。一个用户可以有多张通行证,这些通行证既可以由我们自己的应用生成,也可以由第三方授权的应用生成,不过具体的细节就涉及到了 Identity 的身份认证设计,在此就不拓展来讲了。
为应用添加 JWT 认证支持
上面说了这么多还只是停留在签发 Token 的阶段,进行身份认证从这里才真正开始。微软早就已经提供了一个添加 JWT 认证支持的拓展,不过那个还不算特别的简单易用,所以我就在微软的 API 之上设计了一个新的拓展方法来结合之前的 EasyJwt
配置 JWT 认证:
public static IServiceCollection AddEasyJwt(this IServiceCollection services, EasyJwtOption option)
{
var easyJwt = new EasyJwt(option);
var jwtParams = easyJwt.ExportTokenParameters();
services.AddSingleton(easyJwt);
var authBuilder = services.AddAuthentication()
.AddJwtBearer(jwtOptions =>
{
jwtOptions.Audience = option.Audience;
jwtOptions.ClaimsIssuer = option.Issuer;
jwtOptions.TokenValidationParameters = jwtParams;
option.JwtOptions?.Invoke(jwtOptions);
});
return services;
}
这个拓展方法接受一个 EasyJwtOption
的子类实例作为参数,并通过这个参数初始化一个 EasyJwt
对象,并将其添加进 IoC 容器中。接着就是简单的调用微软的拓展方法,为应用程序添加 JwtBearer 认证。这里的 jwtParams
是由 EasyJwt
对象导出的,具体的导出代码实现可以在我的 GitHub 上看到,并不是很重要的代码,所以就不在这里贴出来了。
为 Cookie 添加 JWT 支持
为 Cookie 添加 Jwt 支持是最让人头疼的了,而且还要让我们的 API 跟 ASP.NET Core 自己的机制能够较完美的结合起来,这里就需要比较多的代码了。
首先我们需要自定义一个 Cookie 中存储 Jwt Token 的格式,也就是下面这个 EasyJwtAuthTicketFormat
:
/// <summary>
/// user info |> jwt |> store in ticket |> serialize |> data protection |> base64 encode
/// https://amanagrawal.blog/2017/09/18/jwt-token-authentication-with-cookies-in-asp-net-core/
/// </summary>
public class EasyJwtAuthTicketFormat : ISecureDataFormat<AuthenticationTicket>
{
private readonly TokenValidationParameters _validationParameters;
private readonly IDataSerializer<AuthenticationTicket> _ticketSerializer;
private readonly IDataProtector _dataProtector;
/// <summary>
/// Create a new instance of the <see cref="EasyJwtAuthTicketFormat"/>
/// </summary>
/// <param name="validationParameters">
/// instance of <see cref="TokenValidationParameters"/> containing the parameters you
/// configured for your application
/// </param>
/// <param name="ticketSerializer">
/// an implementation of <see cref="IDataSerializer{TModel}"/>. The default implemenation can
/// also be passed in"/>
/// </param>
/// <param name="dataProtector">
/// an implementation of <see cref="IDataProtector"/> used to securely encrypt and decrypt
/// the authentication ticket.
/// </param>
public EasyJwtAuthTicketFormat(TokenValidationParameters validationParameters,
IDataSerializer<AuthenticationTicket> ticketSerializer,
IDataProtector dataProtector)
{
_validationParameters = validationParameters ??
throw new ArgumentNullException($"{nameof(validationParameters)} cannot be null");
_ticketSerializer = ticketSerializer ??
throw new ArgumentNullException($"{nameof(ticketSerializer)} cannot be null"); ;
_dataProtector = dataProtector ??
throw new ArgumentNullException($"{nameof(dataProtector)} cannot be null");
}
/// <summary>
/// Does the exact opposite of the Protect methods i.e. converts an encrypted string back to
/// the original <see cref="AuthenticationTicket"/> instance containing the JWT and claims.
/// </summary>
/// <param name="protectedText"></param>
/// <returns></returns>
public AuthenticationTicket Unprotect(string protectedText)
=> Unprotect(protectedText, null);
/// <summary>
/// Does the exact opposite of the Protect methods i.e. converts an encrypted string back to
/// the original <see cref="AuthenticationTicket"/> instance containing the JWT and claims.
/// Additionally, optionally pass in a purpose string.
/// </summary>
/// <param name="protectedText"></param>
/// <param name="purpose"></param>
/// <returns></returns>
public AuthenticationTicket Unprotect(string protectedText, string purpose)
{
var authTicket = _ticketSerializer.Deserialize(
_dataProtector.Unprotect(
Base64UrlTextEncoder.Decode(protectedText)));
var embeddedJwt = authTicket
.Properties?
.GetTokenValue(JwtBearerDefaults.AuthenticationScheme);
try
{
// 校验并读取 jwt 中的用户信息(Claims)
var principal = new JwtSecurityTokenHandler()
.ValidateToken(embeddedJwt, _validationParameters, out var token);
if (!(token is JwtSecurityToken))
{
throw new SecurityTokenValidationException("JWT token was found to be invalid");
}
// todo: 此处还可以校验 token 是否被吊销
// 将 jwt 中的用户信息与 Cookie 中的包含的用户信息合并起来
authTicket.Principal.AddIdentities(principal.Identities);
return authTicket;
}
catch (Exception)
{
return null;
}
}
/// <summary>
/// Protect the authentication ticket and convert it to an encrypted string before sending
/// out to the users.
/// </summary>
/// <param name="data">an instance of <see cref="AuthenticationTicket"/></param>
/// <returns>encrypted string representing the <see cref="AuthenticationTicket"/></returns>
public string Protect(AuthenticationTicket data) => Protect(data, null);
/// <summary>
/// Protect the authentication ticket and convert it to an encrypted string before sending
/// out to the users. Additionally, specify the purpose of encryption, default is null.
/// </summary>
/// <param name="data">an instance of <see cref="AuthenticationTicket"/></param>
/// <param name="purpose">a purpose string</param>
/// <returns>encrypted string representing the <see cref="AuthenticationTicket"/></returns>
public string Protect(AuthenticationTicket data, string purpose)
{
var array = _ticketSerializer.Serialize(data);
return Base64UrlTextEncoder.Encode(_dataProtector.Protect(array));
}
}
这个类我借鉴了前面提到的 Github 上面的那个项目的实现,并花了一些功夫对它做了一些改动。你可以看到,这个真的是非常大的一坨代码,不过我们还是先克服困难,从构造函数来阅读吧。
EasyJwtAuthTicketFormat
的构造函数接受三个参数,第一个我们已经见过了,是 EasyJwt
导出的 TokenValidationParameters
,用来对 Jwt token 进行验证、解密。另外两个参数与 ASP.NET Core 的安全机制有关,IDataSerializer<AuthenticationTicket> ticketSerializer
用来将要存入 Cookie 中的数据序列化或者从 Cookie 中反序列化我们需要读出来的数据。IDataProtector dataProtector
则是用来对 Cookie 进行加密、解密的工具。
据 Github 上那个项目的作者说,他的代码是从微软的默认实现里面魔改出来的,所以我个人认为其中有些东西对于 Jwt 来说其实不是必须的,理由我会在下面详细解释。
首先一起来看看 Unprotect
方法,他的 protectedText
参数就是存储在 Cookie 中的字符串,首先我们需要对他用 Base64 进行解码,然后接着要用之前的 dataProtector
进行解密,最后再用 ticketSerializer
反序列化出 AuthenticationTicket
对象,这个 AuthenticationTicket
中存储的就是一些跟身份认证相关的数据,在我们这里,主要就是存储着 jwt Token。当我们把 token 中的用户数据解密并提取出来之后,再跟 Cookie 中可能含有的其他的身份信息合并起来(虽然可能并不会有什么其他信息。。。),最终就把结果返回出去。
Protect
方法就很简单了,基本上就是 Unprotect
开头一部分的逆序,先把 AuthenticationTicket
序列化,然后使用 dataProtector
加密,最终 Base64 编码成字符串返回出去。
那么很有意思的事情就出现了,jwt 本身的设计就是可以直接在 HTTP 协议中直接传递的,一般来说,并不需要我们重新对其进行 Base64 编码,而且 JWT 本身的内容就是有加密校验的,也就是说信息可读但是不可被修改,那么使用 dataProtector
对其加密的过程也应该是可以省略的。不过由于我比较懒,而且对这里不太肯定,所以就没有移除这部分的代码。
向响应头 Cookie 中添加 JWT
你可能觉得这有啥意思,不就是直接 Cookies.Add()
就好了?然而这样做是没法让认证中间件正确的提取出 Token 的,我们需要用到 HttpContext.SignInAsync
这个方法。这个方法的一个重载是接受一个 ClaimsPrincipal
跟一个 AuthentifactionProperties
作为参数,而这两个东西就是上一节提到的 AuthenticationTicket
的重要组成。
所以,我们除了要让 EasyJwt
签发 token 之外,还要它能够生成 AuthTicket,方便我们跟自带的认证中间件结合使用,相关的实现代码如下:
// EasyJwt.cs
public (ClaimsPrincipal, AuthenticationProperties) GenerateAuthTicket(string userName, IEnumerable<Claim> claims, DateTime expiratoin)
{
ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(userName));
var principal = new ClaimsPrincipal(identity);
var authProps = new AuthenticationProperties();
var token = GenerateToken(userName, claims, expiratoin);
authProps.StoreTokens(new[]
{
new AuthenticationToken
{Name = JwtBearerDefaults.AuthenticationScheme ,Value = token}
});
return (principal, authProps);
}
这个方法跟签发 Token 的方法长得一个样,接受一个 Claims
集合,然后用这些 claims 构建出一张通行证(ClaimsIdentity),然后把这个 identity 对象扔进一个 ClaimsPrincipal
里面。同时,我们还需要把 token 塞进一个 AuthentifactionProperties
对象里面。最后,把这两个创建出来的东西返回出去。
为了能够简化这部分的调用,我又写了一个拓展方法把 SignInAsync
重新包装了一下:
public static async Task SignInAsync(this HttpContext context, string userName, IEnumerable<Claim> claims, DateTime expiratoin)
{
var jwt = context.RequestServices.GetService<EasyJwt>();
var (principal, authProps) = jwt.GenerateAuthTicket(userName, claims, expiratoin);
// 调用自带的 SignInAsync
await context.SignInAsync(principal, authProps);
}
这样,在用户登录的时候就可以非常的简单的同时把 token 显式的返回并设置在 Cookie 中了:
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user, ClaimValueTypes.String)
};
var token = _jwt.GenerateToken(user, claims, DateTime.Now.AddDays(1));
await HttpContext.SignInAsync(user, claims, DateTime.Now.AddDays(1));
return Json(new {Token = token});
看起来我们终于能够正确的签发 token 了,然而事情并没有结束,我们还没有把 Cookie 认证及其相关依赖添加到 IoC 容器中,让我们直接修改一下前面的操作注册服务的拓展方法好了:
public static IServiceCollection AddEasyJwt(this IServiceCollection services, EasyJwtOption option)
{
// 略
var authBuilder = services.AddAuthentication(authOptions =>
{
// 默认使用 Cookie 认证方式
authOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwtOptions =>
{
// 略
});
// 启用
if (option.EnableCookie)
{
// 注册 DataProtector 服务
services.AddDataProtection(dpOptions =>
{
dpOptions.ApplicationDiscriminator = $"app-{option.Issuer}";
});
// 注册 TicketSerializer 服务
services.AddScoped<IDataSerializer<AuthenticationTicket>, TicketSerializer>();
var tmpProvider = services.BuildServiceProvider();
var protectionProvider = tmpProvider.GetService<IDataProtectionProvider>();
var dataProtector = protectionProvider.CreateProtector("jwt-cookie");
authBuilder.AddCookie(options =>
{
// 设置 Cookie 内容格式
options.TicketDataFormat =
new EasyJwtAuthTicketFormat(jwtParams,
tmpProvider.GetService<IDataSerializer<AuthenticationTicket>>(),
dataProtector);
options.ClaimsIssuer = option.Issuer;
options.LoginPath = "/Login";
options.AccessDeniedPath = "/Login";
options.Cookie.HttpOnly = true;
options.Cookie.Name = "tk";
option.CookieOptions?.Invoke(options);
});
}
return services;
}
至此,我们终于能够完整的让 Jwt 的功能运行起来了。
成果展示
那么如何在一个空白的项目中使用 EasyJwt 认证呢?
1. 注册服务
// Startup.ConfigureServices
// 使用对称加密算法
services.AddEasyJwt(new EasySymmetricOptions("test")
{
Audience = "test",
Issuer = "test",
EnableCookie = true
});
// 或者你可以使用非对称加密算法
services.AddEasyJwt(new EasyRSAOptions(PlatformServices.Default.Application.ApplicationBasePath)
{
Audience = "test",
Issuer = "test",
EnableCookie = true
});
2. 添加认证中间件
// Startup.Configure
app.UseAuthentication();
3. 编写自己的登录注册控制器
/// <summary>
/// 演示性登录 API,返回新的 token 并设置 Cookie
/// </summary>
/// <param name="user"></param>
[HttpPost]
[Consumes("application/x-www-form-urlencoded")]
[Produces("application/json")]
public async Task<IActionResult> Post([FromForm]string user)
{
// 假的用户信息
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user, ClaimValueTypes.String)
};
var token = _jwt.GenerateToken(user, claims, DateTime.Now.AddDays(1));
await HttpContext.SignInAsync(user, claims, DateTime.Now.AddDays(1));
return Json(new {Token = token});
}
4. 使用 EasyJwtAuthorize
认证过滤器保护你的 API 或者 MVC 控制器
// POST api/<controller>
[EasyJwtAuthorize]
[HttpPost]
[Consumes("application/x-www-form-urlencoded")]
[Produces("application/json")]
public string Post([FromForm]string value)
{
var userName = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
return userName;
}
终于,经过我们一系列的魔改,我们可以非常快速的来构建一个使用 Jwt 来进行身份认证的网站了。
本文的全部代码您都可以在我的这个项目中找到,或者,如果您想在您的项目中试试我写的这个小拓展,可以直接使用 dotnet cli 来安装:
dotnet add package ZeekoUtilsPack.AspNetCore --source https://www.myget.org/F/zeekoget/api/v3/index.json
可以改进的地方
- 加入吊销 token 的功能
- 移除
EasyJwtAuthTicketFormat
中冗余的代码
如何简单的在 ASP.NET Core 中集成 JWT 认证?的更多相关文章
- 在 ASP.NET Core 中集成 Skywalking APM
前言 大家好,今天给大家介绍一下如何在 ASP.NET Core 项目中集成 Skywalking,Skywalking 是 Apache 基金会下面的一个开源 APM 项目,有些同学可能会 APM ...
- ASP.NET Core 中的那些认证中间件及一些重要知识点
前言 在读这篇文章之间,建议先看一下我的 ASP.NET Core 之 Identity 入门系列(一,二,三)奠定一下基础. 有关于 Authentication 的知识太广,所以本篇介绍几个在 A ...
- [转]ASP.NET Core 中的那些认证中间件及一些重要知识点
本文转自:http://www.qingruanit.net/c_all/article_6645.html 在读这篇文章之间,建议先看一下我的 ASP.NET Core 之 Identity 入门系 ...
- 在Asp.Net Core中集成Kafka(中)
在上一篇中我们主要介绍如何在Asp.Net Core中同步Kafka消息,通过上一篇的操作我们发现上面一篇中介绍的只能够进行简单的首发kafka消息并不能够消息重发.重复消费.乐观锁冲突等问题,这些问 ...
- 在Asp.Net Core中集成Kafka
在我们的业务中,我们通常需要在自己的业务子系统之间相互发送消息,一端去发送消息另一端去消费当前消息,这就涉及到使用消息队列MQ的一些内容,消息队列成熟的框架有多种,这里你可以读这篇文章来了解这些MQ的 ...
- 在Asp.Net Core中集成ABP Dapper
在实际的项目中,除了集成ABP框架的EntityFrameworkCore以外,在有些特定的场景下不可避免地会使用一些SQL查询语句,一方面是由于现在的EntityFrameworkCore2.X有些 ...
- 在asp.net core中使用cookie认证
以admin控制器为要认证的控制器举例 1.对控制器设置权限特性 //a 认证命名空间 using Microsoft.AspNetCore.Authorization; using Microsof ...
- ASP.NET Core系列:JWT身份认证
1. JWT概述 JSON Web Token(JWT)是目前流行的跨域身份验证解决方案. JWT的官网地址:https://jwt.io JWT的实现方式是将用户信息存储在客户端,服务端不进行保存. ...
- 在 ASP.NET CORE 中使用 SESSION
Session 是保存用户和 Web 应用的会话状态的一种方法,ASP.NET Core 提供了一个用于管理会话状态的中间件.在本文中我将会简单介绍一下 ASP.NET Core 中的 Session ...
随机推荐
- drf1 rest & restful规范
web服务交互 我们在浏览器中能看到的每个网站,都是一个web服务.那么我们在提供每个web服务的时候,都需要前后端交互,前后端交互就一定有一些实现方案,我们通常叫web服务交互方案. 目前主流的三种 ...
- python_requests随笔
#coding=utf-8 import requests url = "http://oj.jxust.edu.cn" #如果需要持续的链接,可以使用里面的Session方法(保 ...
- HDU 2639 01背包(分解)
http://acm.hdu.edu.cn/showproblem.php?pid=2639 01背包第k优解,把每次的max分步列出来即可 #include<stdio.h> #incl ...
- Alpha冲刺 - (6/10)
Part.1 开篇 队名:彳艮彳亍团队 组长博客:戳我进入 作业博客:班级博客本次作业的链接 Part.2 成员汇报 组员1(组长)柯奇豪 - 过去两天完成了哪些任务 1. 基于ssm框架的前后端交互 ...
- uart通讯协议
本次设计的源码在http://download.csdn.net/detail/noticeable/9912383 下载 实验目的:通过uart通讯协议的编写,了解FPGA的通讯协议编写的方法. 实 ...
- Python学习第一章
1.Python保留字即是关键字,我们不可以把他们当作任何标识符名称. python的标准库提供了一个keyword模板,可以输出当前版本的关键字: >>>import keywor ...
- 查看服务器tcp连接及服务器并发
一.查看哪些IP连接本机netstat -an二.查看TCP连接数1)统计80端口连接数netstat -nat|grep -i "80"|wc -l 2)统计httpd协议连接数 ...
- Monkey测试简介【转载】
转载:https://www.xuebuyuan.com/3182523.html 一.Monkey测试简介 Monkey测试是Android平台自动化测试的一种手段,通过Monkey程序模拟用户触摸 ...
- Service启动过程分析
Service是一种计算型组件,用于在后台执行一系列的计算任务.由于工作在后台,因此用户是无法直接感知到它的存在.Service组件和Activity组件略有不同,Activity组件只有一种运行模式 ...
- redis 分布式读写锁
http://zhangtielei.com/posts/blog-redlock-reasoning.html 链接里这篇 blog 讨论了 redis 分布式锁的实现以及安全性 我要参考 基于单R ...