Nuget引用:
Install-Package Microsoft.AspNet.WebApi.OwinSelfHost
或者引用以下三个
Install-Package Microsoft.AspNet.WebApi.Owin (让WebApi作为中间件)
Install-Package Microsoft.Owin.Hosting (Hosting接口默认使用HttpListener作为Server)
Install-Package Microsoft.Owin.Host.HttpListener (默认的Server实现)
在App_Start文件夹下新增ApplicationDbInitializer,代码如下:
public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext> { protected override void Seed(ApplicationDbContext context) { InitializeIdentityForEF(context); base.Seed(context); } //创建用户名为admin@123.com,密码为“Admin@123456” public static void InitializeIdentityForEF(ApplicationDbContext dbContext) { var userManager = HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>(); const string name = "admin@123.com";//用户名 const string email = "admin@123.com";//邮箱 const string password = "Admin@123456";//密码 //如果没有admin@123.com用户则创建该用户 var user = userManager.FindByName(name); if (user == null) { user = new ApplicationUser { UserName = name, Email = email }; var result = userManager.Create(user, password); result = userManager.SetLockoutEnabled(user.Id, false); } } }
修改Model文件夹下的IdentityModels.cs,添加斜体部分代码,需添加命名空间:using System.Data.Entity;
public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false) { // 在第一次启动网站时初始化数据库添加管理员用户凭据到数据库 Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer()); }
我把WebApi的Controller放到一个新建的文件夹APIControllers中,TestController的View的js的测试代码
打开Startup.Auth.cs,以下代码是Oauth相关的配置代码
public partial class Startup { public void ConfigureAuth(IAppBuilder app) { var OAuthOptions = new OAuthAuthorizationServerOptions { //获取Token的路径 TokenEndpointPath = new PathString("/Token"), Provider = new ApplicationOAuthProvider(PublicClientId), AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"), //Token 过期时间,默认20分钟 AccessTokenExpireTimeSpan = TimeSpan.FromDays(1), //在生产模式下设 AllowInsecureHttp = false AllowInsecureHttp = true }; app.UseOAuthBearerTokens(OAuthOptions); } }
使用Client Credentials Grant的授权方式( grant_type= client_credentials)获取 Access Token,并以这个 Token 调用与用户相关的 Web API。
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider { public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { string clientId, clientSecret; context.TryGetBasicCredentials(out clientId, out clientSecret); if (clientId == "Mobile" && clientSecret == "Xiaomi") { context.Validated(); } return Task.FromResult<object>(null); } public override Task GrantClientCredentials(OAuthGrantClientCredentialsContext context) { var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType); oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, "Xiaomi")); var ticket = new AuthenticationTicket(oAuthIdentity, new AuthenticationProperties()); context.Validated(ticket); return base.GrantClientCredentials(context); } }
在 ValidateClientAuthentication() 方法中获取客户端的 client_id 与 client_secret 进行验证。在 GrantClientCredentials() 方法中对客户端进行授权,授了权就能发 access token 。这样,OAuth的ClientCredentials授权服务端代码就完成了。在ASP.NET Web API中启用OAuth的Access Token验证非常简单,只需在相应的Controller或Action加上[Authorize]标记,VS已生成部分代码,详细查看APIController文件夹下的ValuesController
下面我们在客户端调用一下,添加TestController,生成Index的View,然后在View中添加如下
$(function () { $("#clientCredentials").on("click", function () { GetClientCredentialsAccessToken(); }); }); function GetClientCredentialsAccessToken() { $("#clientResult").html("Requesting"); var clientId = "Mobile"; var clientSecret = "Xiaomi"; $.ajax({ url: "/Token", type: "post", data: { "grant_type": "client_credentials" }, dataType: "json", headers: { "Authorization": "Basic " + Base64_Encode(clientId + ":" + clientSecret) }, success: function (data) { var accessToken = data.access_token; GetValues(accessToken); } }); } function GetValues(accessToken) { var html = "Token:" + accessToken + "<br/><br/>"; $.ajax({ url: "/api/Values", type: "get", dataType: "json", headers: { "Authorization": "Bearer " + accessToken }, success: function (values) { for (var i = 0; i < values.length; i++) { html += "values[" + i + "] :" + values[i] + "<br/>"; } $("#clientResult").html(html); } }); } function Base64_Encode(str) { var c1, c2, c3; var base64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; var i = 0, len = str.length, string = ''; while (i < len) { c1 = str.charCodeAt(i++) & 0xff; if (i === len) { string += base64EncodeChars.charAt(c1 >> 2); string += base64EncodeChars.charAt((c1 & 0x3) << 4); string += "=="; break; } c2 = str.charCodeAt(i++); if (i === len) { string += base64EncodeChars.charAt(c1 >> 2); string += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)); string += base64EncodeChars.charAt((c2 & 0xF) << 2); string += "="; break; } c3 = str.charCodeAt(i++); string += base64EncodeChars.charAt(c1 >> 2); string += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)); string += base64EncodeChars.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6)); string += base64EncodeChars.charAt(c3 & 0x3F); } return string; }
Ps:
传递clientId与clientSecret有两种方式,上例使用BasicAuthentication,服务端使用TryGetBasicCredentials();另外一种方式是普通From的,把参数放到Ajax的data中,如:
{“clientId”: id ,” clientSecret”:”secret”, "grant_type":"client_credentials"}
对应服务端使用TryGetFormCredentials()获取clientId和clientSecret;
推荐使用Basic Authentication方式;使用Resource Owner Password Credentials Grant 的授权方式( grant_type=password )获取 Access Token,并以这个 Token 调用与用户相关的 Web API。
Resource Owner Password Credentials Grant 授权方式(需要验证登录用户)
因为我们刚开始时已经初始化EF,添加了一个用户信息。ApplicationOAuthProvider.cs 的GrantResourceOwnerCredentials()方法(VS帮我们自动生成了),已经实现了先关的代码
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) { //调用后台的登录服务验证用户名与密码 var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>(); ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password); if (user == null) { context.SetError("invalid_grant", "用户名或密码不正确。"); return; } ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager, OAuthDefaults.AuthenticationType); ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager, CookieAuthenticationDefaults.AuthenticationType); AuthenticationProperties properties = CreateProperties(user.UserName); AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties); context.Validated(ticket); context.Request.Context.Authentication.SignIn(cookiesIdentity); }
在Test的index.cshtml 中新增测试的代码,如下
function GetResourceOwnerCredentialsAccessToken() { $("#resourceOwnerresult").html("Requesting"); var clientId = "Mobile"; var clientSecret = "Xiaomi"; $.ajax({ url: "/Token", type: "post", data: { "grant_type": "password", "username": "admin@123.com", "password": "Admin@123456" }, dataType: "json", headers: { "Authorization": "Basic " + Base64_Encode(clientId + ":" + clientSecret) }, success: function (data) { var accessToken = data.access_token; GetCurrentUserName(accessToken); } }); } function GetCurrentUserName(accessToken) { var html = "Token:" + accessToken + "<br/><br/>"; $.ajax({ url: "/api/User", type: "get", dataType: "text", headers: { "Authorization": "Bearer " + accessToken }, success: function (userName) { html += "CurrentUserName:" + userName + "<br/>"; $("#resourceOwnerresult").html(html); } }); }
至此,使用WebApi 的两种授权方式发放Token和两种授权方式区别,以及使用Token调用受保护的api已经介绍完了,Oauth其实还有一个refresh token,refresh token 是专用于刷新 access token 的 token。一是因为 access token 是有过期时间的,到了过期时间这个 access token 就失效,需要刷新;二是因为一个 access token 会关联一定的用户权限,如果用户授权更改了,这个 access token 需要被刷新以关联新的权限。
这个就不单独介绍了,有兴趣的可以自己研究下。转:https://www.cnblogs.com/YamatAmain/p/5029466.html
api测试:
/// <summary> ///客户端模式【Client Credentials Grant】 ///http://www.asp.net/web-api/overview/security/individual-accounts-in-web-api /// </summary> [RoutePrefix("api/v1/oauth2")] public class OAuth2Controller : ApiController { /// <summary> /// 获得资讯 /// </summary> /// <returns></returns> [Authorize] [Route("news")] public async Task<IHttpActionResult> GetNewsAsync() { var authentication = HttpContext.Current.GetOwinContext().Authentication; var ticket = authentication.AuthenticateAsync("Bearer").Result; var claimsIdentity = User.Identity as ClaimsIdentity; var data = claimsIdentity.Claims.Where(c => c.Type == "urn:oauth:scope").ToList(); var claims = ((ClaimsIdentity)Thread.CurrentPrincipal.Identity).Claims; return Ok(new { IsError = true, Msg = string.Empty, Data = Thread.CurrentPrincipal.Identity.Name + " It's about news !!! token expires: " + ticket.Properties.Dictionary.ToJson() }); } }
/// <summary> ///客户端模式【Client Credentials Grant】 ///http://www.asp.net/web-api/overview/security/individual-accounts-in-web-api /// </summary> [RoutePrefix("api/v1/oauth2")] public class OAuth2Controller : ApiController { /// <summary> /// 获取token /// </summary> /// <returns></returns> [Route("token")] public async Task<IHttpActionResult> GetTokenAsync() { //获得token var dict = new SortedDictionary<string, string>(); dict.Add("client_id", "irving"); dict.Add("client_secret", "123456"); dict.Add("grant_type", "client_credentials"); var data = await (@"http://" + Request.RequestUri.Authority + @"/token").PostUrlEncodedAsync(dict).ReceiveJson<Token>(); //根据token获得咨询信息 [Authorization: Bearer {THE TOKEN}] //var news = await (@"http://" + Request.RequestUri.Authority + @"/api/v1/oauth2/news").WithHeader("Authorization", "Bearer " + data.access_token).GetAsync().ReceiveString(); var news = await (@"http://" + Request.RequestUri.Authority + @"/api/v1/oauth2/news").WithOAuthBearerToken(data.access_token).GetAsync().ReceiveString(); return Ok(new { IsError = true, Msg = data, Data = news }); } } public class Token { public string access_token { get; set; } public string token_type { get; set; } public string expires_in { get; set; } }
全:https://www.cnblogs.com/xsj1989/p/5557251.html
https://www.cnblogs.com/Irving/p/4607104.html
/************ 自己测试的 ****************/
using Microsoft.Owin.Security.OAuth; using System; using System.Collections.Generic; using System.Linq; using System.Web.Http; namespace WebApplication1 { public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API 配置和服务 //添加的配置 //匿名身份验证 //config.SuppressDefaultHostAuthentication(); //config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType)); // Web API 路由 config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } } }
var
jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First();
jsonFormatter.SerializerSettings.ContractResolver =
new
CamelCasePropertyNamesContractResolver();
using System; using System.Threading.Tasks; using Microsoft.Owin; using Owin; using System.Web.Http; [assembly: OwinStartup(typeof(WebApplication1.Startup))] namespace WebApplication1 { public partial class Startup { public void Configuration(IAppBuilder app) { // 有关如何配置应用程序的详细信息,请访问 http://go.microsoft.com/fwlink/?LinkID=316888 var config = new HttpConfiguration(); //去掉? WebApiConfig.Register(config); // 去掉?? ConfigureAuth(app);//开启OAuth服务 app.UseWebApi(config);//这一行代码必须放在ConfiureOAuth(app)之后 } } }
using Microsoft.Owin; using Microsoft.Owin.Security.OAuth; using Owin; using System; using System.Collections.Generic; using System.Linq; using System.Web; using WebApplication1.Providers; namespace WebApplication1 { public partial class Startup { public static OAuthBearerAuthenticationOptions OAuthBearerOptions { get; private set; } public void ConfigureAuth(IAppBuilder app) { //OAuthBearerOptions = new OAuthBearerAuthenticationOptions(); //匿名身份验证 //Token 生成配置 var oAuthOptions = new OAuthAuthorizationServerOptions { AllowInsecureHttp = true, //允许客户端使用Http协议请求 AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active, TokenEndpointPath = new PathString("/token"), AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(5), //提供认证策略 Provider = new OpenAuthorizationServerProvider() //RefreshTokenProvider = new RefreshAuthenticationTokenProvider() }; //app.UseOAuthAuthorizationServer(oAuthOptions); //app.UseOAuthBearerAuthentication(OAuthBearerOptions); //匿名身份验证 app.UseOAuthBearerTokens(oAuthOptions); } } }
using Microsoft.Owin.Security.OAuth; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Web; namespace WebApplication1.Providers { public class OpenAuthorizationServerProvider : OAuthAuthorizationServerProvider { public OpenAuthorizationServerProvider() { } /// <summary> /// 客户端授权[生成access token] /// </summary> /// <param name="context"></param> /// <returns></returns> public override Task GrantClientCredentials(OAuthGrantClientCredentialsContext context) { var oAuthIdentity = new System.Security.Claims.ClaimsIdentity(context.Options.AuthenticationType); oAuthIdentity.AddClaim(new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, context.OwinContext.Get<string>("as:client_id"))); var ticket = new Microsoft.Owin.Security.AuthenticationTicket(oAuthIdentity, new Microsoft.Owin.Security.AuthenticationProperties { AllowRefresh = true }); context.Validated(ticket); return base.GrantClientCredentials(context); } /// <summary> /// 验证客户端 /// </summary> /// <param name="context"></param> public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { string clientId; string clientSecret; context.TryGetFormCredentials(out clientId, out clientSecret);//clienti认证 //context.TryGetBasicCredentials(out clientId, out clientSecret); //Basic认证 //TODO:读库,验证 if (clientId != "malfy" && clientSecret != "111111") { context.SetError("invalid_client", "client is not valid"); return Task.FromResult<object>(null); } context.OwinContext.Set("as:client_id", clientId); //ILifetimeScope scope = context.OwinContext.GetAutofacLifetimeScope(); //var authService = scope.Resolve<IAuthService>(); //var client = authService.GetClient(clientId); //context.OwinContext.Set<string>("as:clientRefreshTokenLifeTime", client.RefreshTokenLifeTime.ToString()); context.Validated(clientId); return Task.FromResult<object>(null); } } }
Controller : [Authorize]
//********** end ***********/
Provider :提供具体的认证策略;
///添加新的RefreshTokenProvider
public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider { public async Task CreateAsync(AuthenticationTokenCreateContext context) { var refreshTokenId = Guid.NewGuid().ToString("n"); using (AuthRepository _repo = new AuthRepository()) { var token = new RefreshToken() { Id = refreshTokenId.GetHash(), Subject = context.Ticket.Identity.Name, IssuedUtc = DateTime.UtcNow, ExpiresUtc = DateTime.UtcNow.AddMinutes(30) }; context.Ticket.Properties.IssuedUtc = token.IssuedUtc; context.Ticket.Properties.ExpiresUtc = token.ExpiresUtc; token.ProtectedTicket = context.SerializeTicket(); var result = await _repo.AddRefreshToken(token); if (result) { context.SetToken(refreshTokenId); } } } public async Task ReceiveAsync(AuthenticationTokenReceiveContext context) { string hashedTokenId = context.Token.GetHash(); using (AuthRepository _repo = new AuthRepository()) { var refreshToken = await _repo.FindRefreshToken(hashedTokenId); if (refreshToken != null) { //Get protectedTicket from refreshToken class context.DeserializeTicket(refreshToken.ProtectedTicket); var result = await _repo.RemoveRefreshToken(hashedTokenId); } } } public void Create(AuthenticationTokenCreateContext context) { throw new NotImplementedException(); } public void Receive(AuthenticationTokenReceiveContext context) { throw new NotImplementedException(); } }
我们实现了其中两个异步方法,对两个同步方法不做实现。其中CreateAsync用来生成RefreshToken值,生成后需要持久化在数据库中,客户端需要拿RefreshToken来请求刷新token,此时ReceiveAsync方法将拿客户的RefreshToken和数据库中RefreshToken做对比,验证成功后删除此refreshToken。
2、重新请求token
可以看到这次请求不但得到了token,还得到了refresh_token
3、当token过期后,凭借上次得到的refresh_token重新获取token
此次请求又得到了新的refresh_token,每次refresh_token只能用一次,因为在方法ReceiveAsync中我们一旦拿到refresh_token就删除了记录。
七、总结
此文重点介绍了OAuth2.0中resource owner password credentials模式的使用,此模式可以实现资源服务为自己的客户端授权。另外文章中也提到模式4-client credentials也可以实现这种场景,但用来给有服务端的客户端使用-区别于纯html+js客户端。原因在于模式4-client credentials使用appKey+appSecrect来验证客户端,如果没有服务端的话appSecrect将暴露在js中。
同样的道理:模式1-授权码模式(authorization code)和模式2-简化模式(implicit)的区别也在于模式2-简化模式(implicit)用在无服务端的场景下,请求头中不用带appSecrect。
在webApi中使用owin来实现OAuth2.0是最简单的解决方案,另外一个方案是使用DotNetOpenOauth,这个方案的实现稍显复杂,可用的文档也较少,源码中带有几个例子我也没有直接跑起来,最后无奈之下几乎读完了整个源码才理解。
八、客户端的实现
我们将采用jquery和angular两种js框架来调用本文实现的服务端。下一篇将实现此功能,另外还要给我们的服务端加上CORS(同源策略)支持。
代码都同步更新在 https://git.oschina.net/richieyangs/OAuthPractice.git
转:https://www.cnblogs.com/Leo_wl/p/4919783.html
数据并行:http://www.cnblogs.com/Leo_wl/p/4919814.html