最近新开发一个需要给App使用的API项目。开发API肯定会想到JASON Web Token(JWT)和OAuthor2(之前一篇随笔记录过OAuthor2)。 JWT和OAuthor2的比较 要像比较JWT和OAuthor2,首先要明白一点就是,这是两个完全不同的东西,没有可比性。 JWT是一种认证协议 官网:http:// JWT提供了一种用于发布介入灵摆(Access Token),并对发布的签名介入令牌进行验证的方法。令牌(Token)本身包含了一系列声明,应用程序可以根据这些声明限制用户对资源的访问。 在新开发的API中,我选择的是使用JWT,稍后会简单介绍其在.net core中的使用。 OAuthor2是一种授权框架 OAuthor2是一种授权框架,提供了一套详细的授权机制(指导)。用户或应用可以通过公开的或私有的设置,授权第三方应用访问特定资源。 既然JWT和OAuthor2没有可比性,为什么还要把这两个放在一起说呢?实际中,会有很多人拿JWT和OAuthor2作比较,或者分不清楚。很多情况下,在讨论OAuthor2的实现时,会把JSON Web Token作为一种认证机制使用。这也是为什么他们会经常一起出现。 JSON Web Token(JWT) JWT是一种安全标准。基本思路就是用户提供用户名和密码给认证服务器,服务器验证用户提交的信息的合法性,如果认证成功,会产生并返回一个Token(令牌),用户可以使用这个token访问服务器上受保护的资源。 一个token的例子: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYW1lIjoibGl1dGFvIiwicm9sZSI6InNob3BVc2VycyIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6InNob3BVc2VycyIsImFjdCI6IjEiLCJuYmYiOjE1NzQyNTAyMTgsImV4cCI6MTU3NTExNDIxOCwiaXNzIjoiWXVZdWUiLCJhdWQiOiJZdVl1ZSJ9.t39iwO-r_YgX5-7XyIV-by2duHfThqTQayI595VtqF 一个token包含三个部分: header.claims.signature 为了安全的在url中使用,所有部分都base64 URL-safe进行编码处理。 Header头部分 头部分简单声明了类型(JWT)以及产生签名所使用的的算法。 { "alg" : "AES256", "typ" : "JWT" } Claims声明 声明部分是整个token的核心,表示要发送的用户详细信息。游学情况下,我们和有可能要在一个服务器上实现认证,然后访问另一台服务器上的资源,或者,通过单独的接口来生成token,token被保存在应用程序客户端(比如浏览器)使用。 一个简单的声明(claim)的例子: { "sub": "1234567890", "name": "John Doe", "admin": true } Signature签名 签名的目的是为了保证上边两部分信息不被篡改。如果尝试使用Bas64对解码后的token进行修改,签名信息就会失效。一般使用一个私钥(private key)通过特定算法对Header和Claims进行混淆产生签名信息,所以只有原始的token才能于签名信息匹配。 OAuthor2是什么? 官网:http:///2/ 相反,OAuthor2不是一个标准协议,而是一个安全的授权框架,它详细描述了系统中不同角色、用户、服务前端应用(比如API),以及客户端(比如网站或移动APP)之间怎么实现相互认证。 OAuthor2的基本概念,可以去阅读之前的一片随笔。点击此处 使用HTTPS保护用户密码 在进一步讨论OAuthor2和JWT的实现之前,有必要说一下,两种方案都需要SSL安全保护,也就是对要传输的数据进行加密编码。安全地传输用户提供的私密信息,在任何一个安全的系统里都是必要的。否则任何人都可以通过侵入私人wifi,在用户登录的时候窃取用户的用户名和密码等信息。 JWT和OAuthor2应该如何选择 在做选择之前,参考一下下边提到的几点。 1、时间投入 OAuthor2是一个安全框架,描述了在各种不同场景下,多个应用之间的授权问题。有海量的资料需要学习,要完全理解需要花费大量时间。甚至对于一些有经验的开发工程师来说,也会需要大概一个月的时间来深入理解OAuth2。 这是个很大的时间投入。相反,JWT是一个相对轻量级的概念。可能花一天时间深入学习一下标准规范,就可以很容易地开始具体实施。 2、出现错误的风险 OAuth2不像JWT一样是一个严格的标准协议,因此在实施过程中更容易出错。尽管有很多现有的库,但是每个库的成熟度也不尽相同,同样很容易引入各种错误。在常用的库中也很容易发现一些安全漏洞。当然,如果有相当成熟、强大的开发团队来持续OAuth2实施和维护,可以一定成都上避免这些风险。 3、社交登录的好处 在很多情况下,使用用户在大型社交网站的已有账户来认证会方便。如果期望你的用户可以直接使用Facebook或者Gmail之类的账户,使用现有的库会方便得多。 JWT的使用场景 无状态的分布式API JWT的主要优势在于使用无状态、可扩展的方式处理应用中的用户会话。服务端可以通过内嵌的声明信息,很容易地获取用户的会话信息,而不需要去访问用户或会话的数据库。在一个分布式的面向服务的框架中,这一点非常有用。但是,如果系统中需要使用黑名单实现长期有效的token刷新机制,这种无状态的优势就不明显了。 优势: 1、快速开发 2、不需要cookie 3、JSON在移动端的广泛应用 4、不依赖与社交登录 5、相对简单的概念理解 限制 1、token有长度限制 2、token不能撤销 3、需要token有失效时间限制(exp) OAuthor2使用场景 外包认证服务器 上边已经讨论过,如果不介意API的使用依赖于外部的第三方认证提供者,你可以简单地把认证工作留给认证服务商去做。也就是常见的,去认证服务商(比如facebook)那里注册你的应用,然后设置需要访问的用户信息,比如电子邮箱、姓名等。当用户访问站点的注册页面时,会看到连接到第三方提供商的入口。用户点击以后被重定向到对应的认证服务商网站,获得用户的授权后就可以访问到需要的信息,然后重定向回来。 优势: 1、快速开发 2、实施代码量小 3、维护工作减少 大型企业解决方案 如果设计的API要被不同的App使用,并且每个App使用的方式也不一样,使用OAuth2是个不错的选择。考虑到工作量,可能需要单独的团队,针对各种应用开发完善、灵活的安全策略。当然需要的工作量也比较大! 优势 1、灵活的实现方式 2、可以和JWT同时使用 3、可以针对不同的应用扩展 简单介绍下在.net core的项目中是如何使用JWT的。 首先,我们的服务是基于组件化的,当然需要先把身份认证的服务注册进来。在Startup类中的ConfigureServices()方法中: services.AddSingleton<ITokenHelper, TokenHelper>(); // configure jwt authentication
下面是上面所需要用到一些自定义类型: AdminUserAccountConst public class AdminUserAccountConst { public const string AdminUserCookie = "AdminUserCookies"; public const string AdminUserCookieName = "AdminUserCookieName"; public const string AdminUserLoginPath = "/account/login"; public const string AdminUserJwt = "AdminUserJwt"; public const string AdminUserRole = "adminuser"; } JWTConfig public class JWTConfig { public string Issuer { get; set; } public string Audience { get; set; } public string IssuerSigningKey { get; set; } public int AccessTokenExpiresMinutes { get; set; } public string RefreshTokenAudience { get; set; } public int RefreshTokenExpiresMinutes { get; set; } } 至于这些类型的字段,可以自行在appsettings.json中去赋值。 "Authentication": { "JwtBearer": { "Issuer": "Bingle", "Audience": "Bingle", "IssuerSigningKey": "Bingle_C421AAEE0D114EAAACVD", "AccessTokenExpiresMinutes": "14400", "RefreshTokenAudience": "RefreshTokenAudience", "RefreshTokenExpiresMinutes": "43200" //60*24*30 } }, ITokenHelper与TokenHepler public interface ITokenHelper { ComplexToken CreateToken(User user); ComplexToken CreateToken(Claim[] claims); (Result result, string userCode) ConfirmRefreshToken(string refreshToken); } public class TokenHelper : ITokenHelper { private readonly IOptions<JWTConfig> _options; public TokenHelper(IOptions<JWTConfig> options) { _options = options; } public ComplexToken CreateToken(User user) { Claim[] claims = new Claim[] { new Claim(JwtClaimTypes.Id, user.UserCode), new Claim(JwtClaimTypes.Name, user.UserName), new Claim(JwtClaimTypes.Role, user.UserRole.GetExtendDescription()), new Claim(ClaimTypes.Role, user.UserRole.GetExtendDescription()), new Claim(JwtClaimTypes.Actor, user.PartyId) }; return CreateToken(claims); } public ComplexToken CreateToken(Claim[] claims) { return new ComplexToken { AccessToken = CreateToken(claims, TokenType.AccessToken), RefreshToken = CreateToken(new Claim[]{claims.First(x=>x.Type == JwtClaimTypes.Id)}, TokenType.RefreshToken) }; } /// <summary> /// 用于创建AccessToken和RefreshToken。 /// 这里AccessToken和RefreshToken只是过期时间不同,【实际项目】中二者的claims内容可能会不同。 /// 因为RefreshToken只是用于刷新AccessToken,其内容可以简单一些。 /// 而AccessToken可能会附加一些其他的Claim。 /// </summary> /// <param name="claims"></param> /// <param name="tokenType"></param> /// <returns></returns> private Token CreateToken(Claim[] claims, TokenType tokenType) { var now = DateTime.Now; var expires = now.Add(TimeSpan.FromMinutes(tokenType.Equals(TokenType.AccessToken) ? _options.Value.AccessTokenExpiresMinutes : _options.Value.RefreshTokenExpiresMinutes)); var token = new JwtSecurityToken( issuer: _options.Value.Issuer, audience: tokenType.Equals(TokenType.AccessToken) ? _options.Value.Audience : _options.Value.RefreshTokenAudience, claims: claims, notBefore: now, expires: expires, signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Value.IssuerSigningKey)), SecurityAlgorithms.HmacSha256)); return new Token { TokenContent = new JwtSecurityTokenHandler().WriteToken(token), Expires = expires }; } public (Result result, string userCode) ConfirmRefreshToken(string refreshToken) { var tokenHandler = new JwtSecurityTokenHandler(); if (!tokenHandler.CanReadToken(refreshToken)) return (Result.FromCode(ResultCode.InvalidToken, "RefreshToken不正确"), null); var jwtSecurityToken = tokenHandler.ReadJwtToken(refreshToken); if (jwtSecurityToken.Issuer != _options.Value.Issuer || !jwtSecurityToken.Audiences.Contains(_options.Value.RefreshTokenAudience)) return (Result.FromCode(ResultCode.InvalidToken, "RefreshToken不正确"), null); if (jwtSecurityToken.ValidTo < DateTime.Now) return (Result.FromCode(ResultCode.InvalidToken, "RefreshToken已经过期了"), null); return (Result.Ok(), jwtSecurityToken.Claims.First(x => x.Type == JwtClaimTypes.Id).Value); } } 还要在Configure方法中使用中间件: app.UseAuthentication(); 首先,定义一个API的基类,后面的API继承此基类就可以了 [Route("[controller]/[action]")] [ApiController] [Authorize( AuthenticationSchemes = AdminUserAccountConst.AdminUserCookie, Roles = AdminUserAccountConst.AdminUserRole)] public class BasicAdminController : ControllerBase { } 现在新建一个用户登录和退出的APIController继承与上面那个基类就可以了。这里简化 了代码 [HttpPost] [AllowAnonymous] [ProducesResponseType(typeof(Result<TokenResultDto>), 200)] public JsonResult Login([FromBody]LoginDto model) { var user = new User();//这里需要去数据库中进行校验 if (user == null) return Json(new {IsSuccess=false,Msg="参数错误"}); var result = _tokenHelper.CreateToken(new User { UserCode = user.UserCode, UserName = user.UserName, Telphone = user.Telphone, PartyId = user.ShopCode, UserRole = UserRoleEnum.user, }); user.RefreshToken = result.RefreshToken.TokenContent; return Json(new TokenResultDto { AccessToken = result.AccessToken.TokenContent, Expires = result.AccessToken.Expires, RefreshToken = result.RefreshToken.TokenContent, }); } 这里使用AllowAnonymous标签,是因为登录并不需要进行身份验证。当需要授权才能访问的接口,不需要加上这个标签。 |
|