前言
OAuth 2.0 是一种开放标准的授权框架,用于授权第三方应用程序访问受保护资源的流程。
OAuth 2.0 认证是指在这个框架下进行的身份验证和授权过程。
在 OAuth 2.0 认证中,涉及以下主要参与方:
- 资源所有者(Resource Owner): 拥有受保护资源的用户。
- 客户端(Client): 第三方应用程序,希望访问资源所有者的受保护资源。
- 授权服务器(Authorization Server): 负责验证资源所有者的身份并颁发访问令牌。
- 资源服务器(Resource Server): 存储受保护资源的服务器,用于接收和响应客户端请求。
OAuth 2.0 认证的流程通常包括以下步骤:
- 客户端注册: 客户端向授权服务器注册,并获得客户端标识和客户端密钥。
- 请求授权: 客户端向资源所有者请求授权,以获取访问受保护资源的权限。
- 授权许可: 资源所有者同意授权,授权服务器颁发授权码给客户端。
- 获取访问令牌: 客户端使用授权码向授权服务器请求访问令牌。
- 访问受保护资源: 客户端使用访问令牌向资源服务器请求访问受保护资源。
OAuth 2.0 认证的优势在于可以实现用户授权而无需透露密码,同时提供了更安全和灵活的授权机制,更好地保护用户数据和系统安全。
以下是一个 ASP.NET WebApi 简单使用 OAuth2.0 认证的 Step By Step 例子。
Step By Step 步骤
-
新建一个空 ASP.NET WebApi 项目,比如 TokenExample
-
在 Models 目录下新建一个 Product 实体类:
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace TokenExample.Models { public class Product { public int Id { get; set; } public string Name { get; set; } public string Category { get; set; } public decimal Price { get; set; } } }
-
在 Controllers 目录下新建一个 ProductsController 控制器
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; using TokenExample.Models; namespace TokenExample.Controllers { public class ProductsController : ApiController { // 初始化数据 Product[] products = new Product[] { new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 }, new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M }, new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M } }; // 查找所有的产品 public IEnumerable<Product> GetAllProducts() { return products; } // 根据 id 查找产品 public Product GetProductById(int id) { var product = products.FirstOrDefault((p) => p.Id == id); if (product == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } return product; } // 根据 类别 查找产品 public IEnumerable<Product> GetProductsByCategory(string category) { return products.Where(p => string.Equals(p.Category, category, StringComparison.OrdinalIgnoreCase)); } } }
-
将网站部署到 IIS, 端口为 8080,使用 Postman 工具测试以下 api:
GET http://localhost:8080/api/Products GET http://localhost:8080/api/Products/1 GET http://localhost:8080/api/Products?category=Groceries
可以看到这些 API 都是可以公开访问的,没有任何验证
-
在 WebApi 项目右键,选择 “管理 Nuget 程序包”,打开 Nuget 包管理器 GUI, 安装以下包:
Microsoft.AspNet.WebApi.Owin
Microsoft.Owin.Host.SystemWeb
Microsoft.AspNet.Identity.Owin
Microsoft.Owin.Cors
EntityFramework -
在项目根目录下添加 “Startup” 类, 这是 Owin 的启动类(注意是项目根目录,即跟 Global.asax 同一位置)
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Http; using Owin; using Microsoft.Owin; using Microsoft.Owin.Security.OAuth; [assembly: OwinStartup(typeof(TokenExample.Startup))] namespace TokenExample { public class Startup { public void Configuration(IAppBuilder app) { HttpConfiguration config = new HttpConfiguration(); ConfigureOAuth(app); WebApiConfig.Register(config); app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll); app.UseWebApi(config); } public void ConfigureOAuth(IAppBuilder app) { OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions() { AllowInsecureHttp = true, // 这里设置获取 token 有 url path TokenEndpointPath = new PathString("/token"), AccessTokenExpireTimeSpan = TimeSpan.FromDays(1), Provider = new SimpleAuthorizationServerProvider() }; app.UseOAuthAuthorizationServer(OAuthServerOptions); app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()); } } }
-
删除 Global.asax
- NOTE: 设置了 Startup 类, 就不需要 Global.asax 了,可以删除,也可以留着
-
在项目根目录下添加验证类 SimpleAuthorizationServerProvider
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Threading; using System.Threading.Tasks; using Microsoft.Owin; using Microsoft.Owin.Security.OAuth; using System.Security.Claims; namespace TokenExample { public class SimpleAuthorizationServerProvider: OAuthAuthorizationServerProvider { public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { context.Validated(); } public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) { // 设置允许跨域 context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" }); /* * 对用户名、密码进行数据校验,这里我们省略 using (AuthRepository _repo = new AuthRepository()) { IdentityUser user = await _repo.FindUser(context.UserName, context.Password); if (user == null) { context.SetError("invalid_grant", "The user name or password is incorrect."); return; } } */ var identity = new ClaimsIdentity(context.Options.AuthenticationType); identity.AddClaim(new Claim("sub", context.UserName)); identity.AddClaim(new Claim("role", "user")); context.Validated(identity); } } }
-
修改 ProductsController 类,在 Action 上增加 [Authorize] 特性,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; using TokenExample.Models; namespace TokenExample.Controllers { public class ProductsController : ApiController { Product[] products = new Product[] { new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 }, new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M }, new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M } }; // [Authorize] 特性是启用 OAuth 的 Access Token 验证,让 CORS 起作用 [Authorize] public IEnumerable<Product> GetAllProducts() { return products; } [Authorize] public Product GetProductById(int id) { var product = products.FirstOrDefault((p) => p.Id == id); if (product == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } return product; } // [AllowAnonymous] 特性是允许匿名访问,即无需 Access Token 验证 [AllowAnonymous] public IEnumerable<Product> GetProductsByCategory(string category) { return products.Where(p => string.Equals(p.Category, category, StringComparison.OrdinalIgnoreCase)); } } }
测试
-
重新在 Postman 运行以下命令:
GET http://localhost:8080/api/Products 返回: { "Message": "已拒绝为此请求授权。" } 这是预期的
-
在 Postman 运行以下命令:
POST/GET http://localhost:23477/token 参数 BODY x-www-form-urlencoded 格式: grant_type=password username=admin password=123456 返回: { "access_token": "ESWxgOCWDDPBRg37cX2RIAb8h--AYgz55rheYumSEU9YVjikYowyih1EdkVUg5vEeuLEeuhZPFJFGe33N3yvieYCzVQ2r0FKYBj0vydKnHAZ7CpLry4DaOhZ8JKIxa159QiBZubA_YgtFliUggSefiosrXW-FaUUO-m5th4YwInw2_5aGPL73uB5FYE0LcLN51U8ZlqoeLDChO3MdTigTc90rVUNiiZ3UBHn-HWvSnI", "token_type": "bearer", "expires_in": 86399 }
-
在以下 api 的 Headers 加上:
GET http://localhost:8080/api/Products Headers Key: Authorization Value: bearer ESWxgOCWDDPBRg37cX2RIAb8h--AYgz55rheYumSEU9YVjikYowyih1EdkVUg5vEeuLEeuhZPFJFGe33N3yvieYCzVQ2r0FKYBj0vydKnHAZ7CpLry4DaOhZ8JKIxa159QiBZubA_YgtFliUggSefiosrXW-FaUUO-m5th4YwInw2_5aGPL73uB5FYE0LcLN51U8ZlqoeLDChO3MdTigTc90rVUNiiZ3UBHn-HWvSnI
-
重新运行,即可正常访问,至此就完成了简单的 ASP.NET WebApi 使用 OAuth2.0 认证
总结
- OAuth2.0 有 Client 和 Scope 的概念,JWT 没有,如果只是拿来用于颁布 Token 的话,二者没区别,如本例
- OAuth2.0 和 JWT 在使用 Token 进行身份验证时有相似之处,但实际上它们是完全不同的两种东西,OAuth2.0 是授权认证的框架,JWT 则是认证验证的方式方法(轻量级概念)
- OAuth2.0 更多用在使用第三方账号登录的情况(比如使用 weibo,qq,github 等登录某个 app)