一、Form表单认证
之前的项目以MVC为主,采用的是from表单认证,Forms认证示意图如下:
HTTP是一个无状态的协议,WEB服务器在处理所有传入HTTP请求时,根本就不知道某个请求是否是一个用户的第一次请求与后续请求,或者是另一个用户的请求。 WEB服务器每次在处理请求时,都会按照用户所访问的资源所对应的处理代码,从头到尾执行一遍,然后输出响应内容,WEB服务器根本不会记住已处理了哪些用户的请求,因此,我们通常说HTTP协议是无状态的。
虽然HTTP协议与WEB服务器是无状态,但我们的业务需求却要求有状态,典型的就是用户登录, 在这种业务需求中,要求WEB服务器端能区分某个请求是不是一个已登录用户发起的,或者当前请求是哪个用户发出的。 在开发WEB应用程序时,我们通常会使用Cookie来保存一些简单的数据供服务端维持必要的状态。
登录的操作通常会检查用户提供的用户名和密码,因此登录状态也必须具有足够高的安全性。 在Forms身份认证中,由于登录状态是保存在Cookie中,而Cookie又会保存到客户端,因此,为了保证登录状态不被恶意用户伪造, ASP.NET采用了加密的方式保存登录状态。 为了实现安全性,ASP.NET采用【Forms身份验证凭据】(即FormsAuthenticationTicket对象)来表示一个Forms登录用户, 加密与解密由FormsAuthentication的Encrypt与Decrypt的方法来实现。
1、用户登录的过程
-
检查用户提交的登录名和密码是否正确。
-
根据登录名创建一个FormsAuthenticationTicket对象。
-
调用FormsAuthentication.Encrypt()加密。
-
根据加密结果创建登录Cookie,并写入Response。在登录验证结束后,一般会产生重定向操作, 那么后面的每次请求将带上前面产生的加密Cookie,供服务器来验证每次请求的登录状态。
var userid = Request["userid"]; var password = Request["password"]; if (userid == "123456" && password == "123456")//检查用户提交的登录名和密码是否正确 { var ticket = new FormsAuthenticationTicket( 1, userid, DateTime.Now, DateTime.Now.AddMinutes(5), true, "角色信息", "/" );//根据登录名创建一个FormsAuthenticationTicket对象 var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket));//调用FormsAuthentication.Encrypt()加密 cookie.HttpOnly = true; HttpContext.Response.Cookies.Add(cookie);//根据加密结果创建登录Cookie,并写入Response return Redirect("/");
}
2、每次请求时的过程
-
FormsAuthenticationModule尝试读取登录Cookie。
-
从Cookie中解析出FormsAuthenticationTicket对象。过期的对象将被忽略。
-
根据FormsAuthenticationTicket对象构造FormsIdentity对象并设置HttpContext.User
-
UrlAuthorizationModule执行授权检查。
using System.Web; using System.Web.Mvc; using System.Web.Security; namespace FormsAuth { public class MyAuthorizeAttribute:AuthorizeAttribute { protected override bool AuthorizeCore(HttpContextBase httpContext) { var cookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName]; var ticket = FormsAuthentication.Decrypt(cookie.Value); var roles = ticket.UserData; var inRoles = false; foreach (var role in roles.Split(',')) { if (Roles.Contains(role)) { inRoles = true; break; } } return inRoles; } protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { ActionResult result = new ContentResult { Content = "没有页面访问权限!", ContentType = filterContext.HttpContext.Response.ContentType }; filterContext.Result = result ?? new HttpUnauthorizedResult(); } } }
在登录与认证的实现中,FormsAuthenticationTicket和FormsAuthentication是二个核心的类型, 前者可以认为是一个数据结构,后者可认为是处理前者的工具类。UrlAuthorizationModule是一个授权检查模块,其实它与登录认证的关系较为独立, 因此,如果我们不使用这种基于用户名与用户组的授权检查,也可以禁用这个模块。由于Cookie本身有过期的特点,然而为了安全,FormsAuthenticationTicket也支持过期策略, 不过,ASP.NET的默认设置支持FormsAuthenticationTicket的可调过期行为,即:slidingExpiration=true 。 这二者任何一个过期时,都将导致登录状态无效。
Request.IsAuthenticated可以告诉我们当前请求是否已经过身份验证, 我们来看一下这个属性是如何实现的:
public bool IsAuthenticated { get { return (((this._context.User != null) && (this._context.User.Identity != null)) && this._context.User.Identity.IsAuthenticated); } }
DEMO下载:https://github.com/qiuxianhu/FormsAuth
二、HTTP基本认证(HTTP Basic Auth)
客户端向服务端发送一个携带基于用户名/密码的认证凭证的请求。认证凭证的格式为“{UserName}:{Password}”,并采用Base64编码,经过编码的认证凭证被存放在请求报头Authorization中,Authorization报头值类似:Basic MTIzNDU2OjEyMzQ1Ng==。服务端接收到请求之后,从Authorization报头中提取凭证并对其进行解码,最后采用提取的用户名和密码实施认证。认证成功之后,该请求会得到正常的处理,并回复一个正常的响应。
新建一个Attribute:BasicAuthorizeAttribute用于实现basic认证
using System; using System.Net.Http; using System.Text; using System.Web.Http; using System.Web.Http.Controllers; namespace HttpAuth.App_Start { public class BasicAuthorizeAttribute : System.Web.Http.AuthorizeAttribute { protected override bool IsAuthorized(HttpActionContext actionContext) { //这个判断是在进行跨域访问时浏览器会发起一个Options请求去试探这个请求,但是他不会带着data参数和一些header参数,所以认证肯定没法通过导致无法继续进行,所以给他直接认证通过。(对非跨域的则没有影响) if (actionContext.Request.Method == HttpMethod.Options) { return true; } if (actionContext.Request.Headers.Authorization != null && actionContext.Request.Headers.Authorization.Parameter != null) { var authorizationParameter = Convert.FromBase64String(actionContext.Request.Headers.Authorization.Parameter); var basicArray = Encoding.Default.GetString(authorizationParameter).Split(':'); var userid = basicArray[0]; var password = basicArray[1]; if (userid == "123456" && password == "123456") { return true; } } return false; } /// <summary> /// 因为是继承重写的AuthorizeAttribute,在IsAuthorized 返回False的时候会执行这个方法 这里是返回一个401的错误信息 /// </summary> /// <param name="actionContext"></param> protected override void HandleUnauthorizedRequest(HttpActionContext actionContext) { var responseMessage = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized); //这句代码指示浏览器 认证方式为Basic 然后浏览器自动弹出一个登陆窗口并以basic 的方式 加密后每次通过header 传输到服务器进行认证然后得到授权 responseMessage.Headers.Add("WWW-Authenticate", "Basic"); throw new HttpResponseException(responseMessage); } } }
用法也很简单,在ApiController或方法上边加属性即可,如:
[BasicAuthorizeAttribute] // GET api/values [SwaggerOperation("GetAll")] public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; }
效果如下:
DEMO下载: https://github.com/qiuxianhu/HttpAuthorization
三、OAuth 2.0
OAuth 2.0 是目前最流行的授权机制,用来授权第三方应用,获取用户数据。这个标准比较抽象,使用了很多术语,初学者不容易理解。其实说起来并不复杂,下面我就通过一个简单的类比,帮助大家轻松理解,OAuth 2.0 到底是什么。
1、快递员问题
我住在一个大型的居民小区。小区有门禁系统。进入的时候需要输入密码。我经常网购和外卖,每天都有快递员来送货。我必须找到一个办法,让快递员通过门禁系统,进入小区。如果我把自己的密码,告诉快递员,他就拥有了与我同样的权限,这样好像不太合适。万一我想取消他进入小区的权力,也很麻烦,我自己的密码也得跟着改了,还得通知其他的快递员。有没有一种办法,让快递员能够自由进入小区,又不必知道小区居民的密码,而且他的唯一权限就是送货,其他需要密码的场合,他都没有权限?
2、授权机制的设计
于是,我设计了一套授权机制。
第一步,门禁系统的密码输入器下面,增加一个按钮,叫做"获取授权"。快递员需要首先按这个按钮,去申请授权。
第二步,他按下按钮以后,屋主(也就是我)的手机就会跳出对话框:有人正在要求授权。系统还会显示该快递员的姓名、工号和所属的快递公司。
我确认请求属实,就点击按钮,告诉门禁系统,我同意给予他进入小区的授权。
第三步,门禁系统得到我的确认以后,向快递员显示一个进入小区的令牌(access token)。令牌就是类似密码的一串数字,只在短期内(比如七天)有效。
第四步,快递员向门禁系统输入令牌,进入小区。
有人可能会问,为什么不是远程为快递员开门,而要为他单独生成一个令牌?这是因为快递员可能每天都会来送货,第二天他还可以复用这个令牌。另外,有的小区有多重门禁,快递员可以使用同一个令牌通过它们。
3、互联网场景
我们把上面的例子搬到互联网,就是 OAuth 的设计了。首先,居民小区就是储存用户数据的网络服务。比如,微信储存了我的好友信息,获取这些信息,就必须经过微信的"门禁系统"。其次,快递员(或者说快递公司)就是第三方应用,想要穿过门禁系统,进入小区。
最后,我就是用户本人,同意授权第三方应用进入小区,获取我的数据。简单说,OAuth 就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
4、令牌与密码
令牌(token)与密码(password)的作用是一样的,都可以进入系统,但是有三点差异。
(1)令牌是短期的,到期会自动失效,用户自己无法修改。密码一般长期有效,用户不修改,就不会发生变化。
(2)令牌可以被数据所有者撤销,会立即失效。以上例而言,屋主可以随时取消快递员的令牌。密码一般不允许被他人撤销。
(3)令牌有权限范围(scope),比如只能进小区的二号门。对于网络服务来说,只读令牌就比读写令牌更安全。密码一般是完整权限。
上面这些设计,保证了令牌既可以让第三方应用获得权限,同时又随时可控,不会危及系统安全。这就是 OAuth 2.0 的优点。
注意,只要知道了令牌,就能进入系统。系统一般不会再次确认身份,所以令牌必须保密,泄漏令牌与泄漏密码的后果是一样的。 这也是为什么令牌的有效期,一般都设置得很短的原因。OAuth 2.0 对于如何颁发令牌的细节,规定得非常详细。具体来说,一共分成四种授权类型(authorization grant),即四种颁发令牌的方式,适用于不同的互联网场景。
四、OWIN实现OAuth 2.0 之客户端模式
1、原理
客户端使用自己的名义,而不是用户的名义,向“服务提供商” 进行认证。如何理解这句话?
如上图,可以得出一个大概的结论
- 用户(User)通过客户端(Client)访问受限资源(Resource)
- 因为资源受限,所以需要授权;而这个授权是Client与Authentication之间完成的,可以说跟User没有什么关系
- 根据2得出,Resource与User没有关联关系,即User不是这个Resource的Owner(所有者)
2、过程
- Client网站向认证服务网站发出请求。
http://localhost:8007/grant_type=client_credentials&client_id=CLIENT_ID&client_secret=CLIENT_SECRET
上面 URL 中,grant_type
参数等于client_credentials
表示采用凭证式,client_id
和client_secret
用来让认证服务确认Client的身份。
- 认证服务网站验证通过以后,直接返回令牌。这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。
- Client网站拿到令牌以后,就可以向资源服务网站(资源服务网站和认证服务网站可以是一个)的 API 请求数据。此时,每个发到 API 的请求,都必须带有令牌。具体做法是在请求的头信息,加上一个
Authorization
字段,令牌就放在这个字段里面。
curl -H "Authorization: Bearer ACCESS_TOKEN" "http://localhost:8008/api/Values"
上面命令中,ACCESS_TOKEN
就是拿到的令牌。
2、适应场景
- 肯定不能用作登录认证!因为登录认证后需要得到用户的一些基本信息,如昵称,头像之类,这些信息是属于User的;
- 适用于一些对于权限要求不强的资源认证,比如:仅用于区分用户是否登录,排除匿名用户获取资源
3、示例说明
(1)新建资源项目:ResourceServer
引用owin:install-package Microsoft.Owin -Version 2.1.0
新增Startup.cs
[assembly: OwinStartup(typeof(ResourceServer.Startup))] namespace ResourceServer { public partial class Startup { public void Configuration(IAppBuilder app) { ConfigureAuth(app); } } }
新增Startup.Auth.cs
namespace ResourceServer { public partial class Startup { public void ConfigureAuth(IAppBuilder app) {
// 这句是资源服务器认证token的关键,认证逻辑在里边封装好了,我们看不到 app.UseOAuthBearerAuthentication(new Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationOptions()); } } }
新增ValuesController.cs
namespace ResourceServer.Controllers { [Authorize] public class ValuesController : ApiController { public string Get() { return "qiuxianhu"; } } }
(2)新建认证服务项目:AuthorizationServer
新增Startup.cs
using Microsoft.Owin; using Owin; [assembly: OwinStartup(typeof(OAuth2._0ClientCredentialService.Startup))] namespace OAuth2._0ClientCredentialService { public partial class Startup { public void Configuration(IAppBuilder app) { ConfigureAuth(app); } } }
新增Startup.Auth.cs
using ConfigHelper; using Microsoft.Owin; using Microsoft.Owin.Security; using Microsoft.Owin.Security.OAuth; using Owin; using System; using System.Linq; using System.Security.Claims; using System.Security.Principal; using System.Threading.Tasks; namespace OAuth2._0ClientCredentialService { public partial class Startup { public void ConfigureAuth(IAppBuilder app) { //创建OAuth授权服务器 app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions { TokenEndpointPath = new PathString(ConfigSetting.TOKENPATH),//获取 access_token 授权服务请求地址,即http://localhost:端口号/token; ApplicationCanDisplayErrors = true, AccessTokenExpireTimeSpan=TimeSpan.FromDays(10),//access_token 过期时间 #if DEBUG AllowInsecureHttp = true, #endif // Authorization server provider which controls the lifecycle of Authorization Server Provider = new OAuthAuthorizationServerProvider { OnValidateClientAuthentication = ValidateClientAuthentication, OnGrantClientCredentials = GrantClientCredetails } }); } /// <summary> /// ValidateClientAuthentication方法用来对third party application 认证, /// 获取客户端的 client_id 与 client_secret 进行验证 /// context.Validated(); 表示允许此third party application请求。 /// </summary> /// <param name="context"></param> /// <returns></returns> private Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { string clientId=null; string clientSecret=null; if (context.TryGetBasicCredentials(out clientId, out clientSecret) || context.TryGetFormCredentials(out clientId, out clientSecret)) { if (clientId == "123456" && clientSecret == "abcdef") { context.Validated(); } } return Task.FromResult(0); } /// <summary> /// 该方法是对客户端模式进行授权的时候使用的 /// 对客户端进行授权,授了权就能发 access token 。 /// 只有这两个方法(ValidateClientAuthentication和GrantClientCredetails)同时认证通过才会颁发token。 private Task GrantClientCredetails(OAuthGrantClientCredentialsContext context) { GenericIdentity genericIdentity = new GenericIdentity(context.ClientId, OAuthDefaults.AuthenticationType); ClaimsIdentity claimsIdentity = new ClaimsIdentity(genericIdentity, context.Scope.Select(x => new Claim("urn:oauth:scope", x))); context.Validated(claimsIdentity); return Task.FromResult(0); } } }
自此,认证服务项目算是建好了,因为对于客户端模式,认证服务器只需要返回token
(3)新增Client项目
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
using ConfigHelper; using DotNetOpenAuth.OAuth2; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Text; namespace ClientIDSecretCredentialGrant { class Program { private static string OAUTH_SERVER_URL = ConfigSetting.OAUTH_SERVER_URL; private static string OAUTH_TOKEN_PATH = ConfigSetting.OAUTH_TOKEN_PATH; private static string RESOURCE_SERVER_URL = ConfigSetting.RESOURCE_SERVER_URL; private static string RESOURCE_ME_PATH = ConfigSetting.RESOURCE_ME_PATH; private static readonly string ClientID = ConfigSetting.CLIENTID; private static readonly string Secret = ConfigSetting.SECRET; private static WebServerClient _WebServerClient; private static string _AccessToken; static void Main(string[] args) { #region 方式一 Call_WebAPI_By_Access_Token(); #endregion #region 方式二 DotNetOpenAuth.OAuth2 //InitializeWebServerClient(); //Console.WriteLine("Requesting Token..."); //RequestToken(); //Console.WriteLine("Access Token: {0}", _AccessToken); //Console.WriteLine("Access Protected Resource"); //AccessProtectedResource(); Console.ReadKey(); #endregion } public static void Call_WebAPI_By_Access_Token() { HttpClient _httpClient = new HttpClient(); Dictionary<string, string> parameters = new Dictionary<string, string>(); parameters.Add("grant_type", "client_credentials"); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes(ClientID + ":" + Secret)) ); var response = _httpClient.PostAsync(new Uri(new Uri(OAUTH_SERVER_URL), OAUTH_TOKEN_PATH), new FormUrlEncodedContent(parameters)).Result; var responseValue = response.Content.ReadAsStringAsync().Result; if (response.StatusCode == System.Net.HttpStatusCode.OK) { _AccessToken = JObject.Parse(responseValue)["access_token"].Value<string>(); } _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _AccessToken); Console.WriteLine(_httpClient.GetAsync(new Uri(new Uri(RESOURCE_SERVER_URL), RESOURCE_ME_PATH)).Result.Content.ReadAsStringAsync().Result); Console.ReadKey(); } private static void InitializeWebServerClient() { Uri authorizationServerUri = new Uri(OAUTH_SERVER_URL); AuthorizationServerDescription authorizationServer = new AuthorizationServerDescription { TokenEndpoint = new Uri(authorizationServerUri, OAUTH_TOKEN_PATH) }; _WebServerClient = new WebServerClient(authorizationServer, ClientID, Secret); } private static void RequestToken() { IAuthorizationState state = _WebServerClient.GetClientAccessToken(); Console.WriteLine(state); _AccessToken = state.AccessToken; } private static void AccessProtectedResource() { Uri resourceServerUri = new Uri(RESOURCE_SERVER_URL); HttpClient client = new HttpClient(_WebServerClient.CreateAuthorizingHandler(_AccessToken)); string body = client.GetStringAsync(new Uri(resourceServerUri, RESOURCE_ME_PATH)).Result; Console.WriteLine(body); } } }
OK,Client环境搭好了,我们来运行下试试
DEMO地址: https://github.com/qiuxianhu/OAuth2.0ClientCredential
五、OWIN实现OAuth 2.0 之密码模式
1、原理
用户向客户端提供用户名和密码,客户端使用这些信息向认证服务进行认证,密码模式的流程图:
2、过程
如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式"(password)。
- A 网站要求用户提供 B 网站的用户名和密码。拿到以后,A 就直接向 B 请求令牌。
https://oauth.b.com/token?grant_type=password&username=USERNAME&password=PASSWORD&client_id=CLIENT_ID
上面 URL 中,grant_type
参数是授权方式,这里的password
表示"密码式",username
和password
是 B 的用户名和密码。
- B 网站验证身份通过后,直接给出令牌。注意,这时不需要跳转,而是把令牌放在 JSON 数据里面,作为 HTTP 回应,A 因此拿到令牌。这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。
3、示例
(1)新建客户端Client
using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Headers; using System.Text; namespace Client { class Program { static void Main(string[] args) { string server_url = "http://localhost:8005/"; string resource_url = "http://localhost:8006/"; string clientid = "123456"; string secret = "abcdef"; HttpClient _httpClient = new HttpClient(); Dictionary<string, string> parameters = new Dictionary<string, string>(); parameters.Add("grant_type", "password"); parameters.Add("UserName", "qiuxianhu"); parameters.Add("Password", "123456"); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes(clientid + ":" + secret)) ); var response = _httpClient.PostAsync(new Uri(new Uri(server_url), "/Token"), new FormUrlEncodedContent(parameters)).Result; var responseValue = response.Content.ReadAsStringAsync().Result; if (response.StatusCode == System.Net.HttpStatusCode.OK) { string token = JObject.Parse(responseValue)["access_token"].Value<string>(); Console.WriteLine(token); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); Console.WriteLine(_httpClient.GetAsync(new Uri(new Uri(resource_url), "/api/values")).Result.Content.ReadAsStringAsync().Result); } Console.ReadKey(); } } }
(2)认证服务
新建Startup.cs文件
using Microsoft.Owin; using Owin; [assembly: OwinStartup(typeof(OAuthService.Startup))] namespace OAuthService { public partial class Startup { public void Configuration(IAppBuilder app) { ConfigureAuth(app); } } }
新建Startup.Auth.cs文件
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
using Microsoft.Owin; using Microsoft.Owin.Security; using Microsoft.Owin.Security.OAuth; using Owin; using System; using System.Linq; using System.Security.Claims; using System.Security.Principal; using System.Threading.Tasks; namespace OAuthService { public partial class Startup { public void ConfigureAuth(IAppBuilder app) { //创建OAuth授权服务器 app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions { TokenEndpointPath = new PathString("/Token"),//获取 access_token 授权服务请求地址,即http://localhost:端口号/token; ApplicationCanDisplayErrors = true, AccessTokenExpireTimeSpan = TimeSpan.FromDays(10),//access_token 过期时间 #if DEBUG AllowInsecureHttp = true, #endif // Authorization server provider which controls the lifecycle of Authorization Server Provider = new OAuthAuthorizationServerProvider { OnValidateClientAuthentication = ValidateClientAuthentication, //这个方法就是后台处理密码模式认证关键的地方 OnGrantResourceOwnerCredentials = GrantResourceOwnerCredentials } }); } /// <summary> /// ValidateClientAuthentication方法用来对third party application 认证, /// 获取客户端的 client_id 与 client_secret 进行验证 /// context.Validated(); 表示允许此third party application请求。 /// </summary> /// <param name="context"></param> /// <returns></returns> private Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { string clientId = null; string clientSecret = null; if (context.TryGetBasicCredentials(out clientId, out clientSecret) || context.TryGetFormCredentials(out clientId, out clientSecret)) { if (clientId == "123456" && clientSecret == "abcdef") { context.Validated(); } } return Task.FromResult(0); } /// <summary> /// 这个方法就是后台处理密码模式认证关键的地方 /// 认证服务判断查询数据库判断该用户的用户名和密码是否正确。如果正确就会授权,产生token。 /// </summary> /// <param name="context"></param> /// <returns></returns> private async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) { if (context.UserName!="qiuxianhu"||context.Password!="123456") { context.SetError("invalid_grant","The user name or password is incorrect."); context.Rejected(); return; } else { ClaimsIdentity claimsIdentity = new ClaimsIdentity(context.Options.AuthenticationType); claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, context.UserName)); var ticket = new AuthenticationTicket(claimsIdentity, new AuthenticationProperties()); context.Validated(ticket); } } } }
(3)建立资源服务
新建Startup.cs文件
using Microsoft.Owin; using Owin; [assembly: OwinStartup(typeof(ResourceServer.Startup))] namespace ResourceServer { public partial class Startup { public void Configuration(IAppBuilder app) { ConfigureAuth(app); } } }
新建Startup.Auth.cs文件
using Owin; namespace ResourceServer { public partial class Startup { public void ConfigureAuth(IAppBuilder app) { // 这句是资源服务器认证token的关键,认证逻辑在里边封装好了,我们看不到 app.UseOAuthBearerAuthentication(new Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationOptions()); } } }
大功告成!
Demo下载:https://github.com/qiuxianhu/OAuth2.0PasswordCredential
六、OWIN实现OAuth 2.0 之授权码模式
1、原理
通过客户端的后台服务器,与“服务提供商”的认证服务器进行认证。
- 用户访问客户端,后者将前者导向认证服务器。
- 用户选择是否给予客户端授权。
- 假设用户给予授权,认证服务器首先生成一个授权码,并返回给用户,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
- 客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
- 认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
- Client拿着access token去访问Resource资源
2、示例
(1)新建客户端
using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OAuth2; using System; using System.Net.Http; using System.Web.Mvc; namespace AuthorizationCodeGrantClient.Controllers { public class HomeController : Controller { static string accessToken = null; static string refreshToken = null; public ActionResult Index() { Uri authorizationServerUri = new Uri("http://localhost:8011/"); AuthorizationServerDescription authorizationServerDescription = new AuthorizationServerDescription { AuthorizationEndpoint = new Uri(authorizationServerUri, "OAuth/Authorize"), TokenEndpoint = new Uri(authorizationServerUri, "OAuth/Token") }; WebServerClient webServerClient = new WebServerClient(authorizationServerDescription, "123456", "abcdef"); if (string.IsNullOrEmpty(accessToken)) { IAuthorizationState authorizationState = webServerClient.ProcessUserAuthorization(Request); if (authorizationState != null) { accessToken = authorizationState.AccessToken; refreshToken = authorizationState.RefreshToken; } } // 授权申请 if (!string.IsNullOrEmpty(Request.Form.Get("btnRequestAuthorize"))) { //这里 new[] { "scopes1", "scopes2" }为需要申请的scopes,或者说是Resource Server的接口标识,或者说是接口权限。然后Send(HttpContext)即重定向。 OutgoingWebResponse grantRequest = webServerClient.PrepareRequestUserAuthorization(new[] { "scopes1", "scopes2" }); grantRequest.Send(HttpContext); Response.End(); } // 申请资源 if (!string.IsNullOrEmpty(Request.Form.Get("btnRequestResource"))) { var resourceServerUri = new Uri("http://localhost:8012/"); var resourceRequest = new HttpClient(webServerClient.CreateAuthorizingHandler(accessToken)); ViewBag.ResourceResponse = resourceRequest.GetStringAsync(new Uri(resourceServerUri, "api/Values")).Result; } return View(); } } }
(2)新建认证服务
新建Startup.cs文件
using Microsoft.Owin; using Owin; [assembly: OwinStartup(typeof(AuthorizationCodeGrantServer.Startup))] namespace AuthorizationCodeGrantServer { public partial class Startup { public void Configuration(IAppBuilder app) { ConfigureAuth(app); } } }
新建OAuthController.cs文件
using Microsoft.AspNet.Identity; using Microsoft.Owin.Security; using System.Security.Claims; using System.Web; using System.Web.Mvc; namespace AuthorizationCodeGrantServer.Controllers { public class OAuthController : Controller { public ActionResult Authorize() { IAuthenticationManager authenticationManager = HttpContext.GetOwinContext().Authentication; AuthenticateResult authenticateResult = authenticationManager.AuthenticateAsync(DefaultAuthenticationTypes.ApplicationCookie).Result; ClaimsIdentity claimsIdentity = authenticateResult != null ? authenticateResult.Identity : null; if (claimsIdentity == null) { authenticationManager.Challenge(DefaultAuthenticationTypes.ApplicationCookie); //用户登录凭证失效就报401错误,并且跳转至AccountController中的Login中 return new HttpUnauthorizedResult(); } ViewBag.IdentityName = claimsIdentity.Name; ViewBag.Scopes = (Request.QueryString.Get("scope") ?? "").Split(' '); if (Request.HttpMethod == "POST") { // 点击btnGrant就确认授权,返回token等信息 if (!string.IsNullOrEmpty(Request.Form.Get("btnGrant"))) { claimsIdentity = new ClaimsIdentity(claimsIdentity.Claims, "Bearer", claimsIdentity.NameClaimType, claimsIdentity.RoleClaimType); foreach (var scope in ViewBag.Scopes) { claimsIdentity.AddClaim(new Claim("urn:oauth:scope", scope)); } authenticationManager.SignIn(claimsIdentity); } if (!string.IsNullOrEmpty(Request.Form.Get("btnOtherLogin"))) { authenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie); authenticationManager.Challenge(DefaultAuthenticationTypes.ApplicationCookie); return new HttpUnauthorizedResult(); } } return View(); } } }
(3)新建资源服务
新建Startup.cs文件
using Microsoft.Owin; using Owin; [assembly: OwinStartup(typeof(ResourceServer.Startup))] namespace ResourceServer { public partial class Startup { public void Configuration(IAppBuilder app) { ConfigureAuth(app); } } }
新建Startup.Auth.cs文件
using Owin; namespace ResourceServer { public partial class Startup { public void ConfigureAuth(IAppBuilder app) { // 这句是资源服务器认证token的关键,认证逻辑在里边封装好了,我们看不到 app.UseOAuthBearerAuthentication(new Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationOptions()); } } }
新建ValuesController,提供资源
using System.Security.Claims; using System.Threading; using System.Web.Http; namespace ResourceServer.Controllers { [Authorize] public class ValuesController : ApiController { public string Get() { ClaimsPrincipal principal = Thread.CurrentPrincipal as ClaimsPrincipal; var isInRole = principal.IsInRole("scopes1"); return "qiuxianhu"; } } }
Demo下载https://github.com/qiuxianhu/OAuth2.0CodeCredential
七、基于JWT的Token认证机制实现
1、什么是JWT
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
2、传统的session认证方式
http协议本身是一种无状态的协议,这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来。缺点主要体现在以下两个方面:
1、session:每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
2、扩展性:用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
3、CSRF:因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
3、基于token的鉴权
基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。
流程上是这样的:
- 用户使用用户名密码来请求服务器
- 服务器进行验证用户的信息
- 服务器通过验证发送给用户一个token
- 客户端存储token,并在每次请求时附送上这个token值
- 服务端验证token值,并返回数据
这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)
策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin:*
。
4、JWT组成
JWT是由三段信息构成的,将这三段信息文本连接起来就构成了Jwt字符串。第一部分我们称它为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature),下面是个JWT实例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOiJBZG1pbiIsIkV4cGlyZSI6IjIwMjAtMDctMTEgMTY6NDc6MTYifQ.9ev6IGc1K3xvYaEfmMYeyFz5oHCM57fRGOvSZ-jvArw
(1)header
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。jwt的头部承载两部分信息:
- 声明类型,这里是jwt
- 声明加密的算法 通常直接使用 HMAC SHA256
完整的头部就像下面这样的JSON:
{ 'typ': 'JWT', 'alg': 'HS256' }
在这里,我们说明了这是一个JWT,并且我们所用的签名算法是HS256算法。对它也要进行Base64编码,之后的字符串就成了JWT的Header(头部)。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
(2)playload
载荷就是存放有效信息的地方,这些有效信息包含三个部分
- 标准中注册的声明
- 公共的声明
- 私有的声明
标准中注册的声明
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 :公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 :私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
{
"iss": "John Wu JWT",
"iat": 1441593502,
"exp": 1441594722,
"aud": "www.example.com",
"sub": "jrocket@example.com",
"from_user": "B",
"target_user": "A"
}
将上面的JSON对象进行[base64编码]可以得到下面的字符串。这个字符串我们将它称作JWT的Payload(载荷)。
eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9
(3)signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret
这个部分需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串,然后通过header中声明的加密方式进行secret
组合加密,然后就构成了jwt的第三部分。
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload); var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.
连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
(5)JWT签名的目的
最后一步签名的过程,实际上是对头部以及载荷内容进行签名。一般而言,加密算法对于不同的输入产生的输出总是不一样的。所以,如果有人对头部以及载荷的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。而且,如果不知道服务器加密的时候用的密钥的话,得出来的签名也一定会是不一样的。
服务器应用在接受到JWT后,会首先对头部和载荷的内容用同一算法再次签名。那么服务器应用是怎么知道我们用的是哪一种算法呢?别忘了,我们在JWT的头部中已经用alg
字段指明了我们的加密算法了。如果服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个Token的内容被别人动过的,我们应该拒绝这个Token,返回一个HTTP 401 Unauthorized响应。
(6)JWT安全性
使用JWT会暴露信息吗?是的。所以,在JWT中,不应该在载荷里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第三方通过Base64解码就能很快地知道你的密码了。总结如下:
- 不应该在jwt的payload部分存放敏感信息,因为该部分是客户端可解密的部分。
- 保护好secret私钥,该私钥非常重要。
- 如果可以,请使用https协议
(7)JWT优点
- 因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。
- 因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
- 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。
- 它不需要在服务端保存会话信息, 所以它易于应用的扩展
(8)JWT的适用场景
我们可以看到,JWT适合用于向Web应用传递一些非敏感信息。其实JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录。
(9)如何应用
一般是在请求头里加入Authorization
,并加上Bearer
标注:
fetch('api/user/1', { headers: { 'Authorization': 'Bearer ' + token }
})
参考资料
https://www.cnblogs.com/lanxiaoke/category/941651.html
http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html