》》》
四个模式中,只有【客户端模式】不需要用户输入用户名和密码,因为 客户端模式,不是用户名义请求的,是客户端本身名义请求的,所以需要后台提供 client_id 和 client_secret,
根据这个两个去认证服务器【Authorization server】 获取access_token.
》》适用于没有前端的命令行应用,即在命令行下请求令牌。一般用来提供给我们完全信任的服务器端服务。
如果是第一方应用(自己开发的应用)一般我们都认为要安全一些,因为不会故意的去泄露访问resource的access token, 所以一般第一方应用我们可以使用简单的【密码模式】, 这种节省了通过code去交换access token这一步骤(实际上就是节省了一次网络请求来回),直接通过用户名,密码去获取access token。而第三方应用我们需要采用更加安全的 【授权码模式】和【简单模式】
【授权码模式】和【简单模式】、【密码模式】 都需要用户录入用户名和密码, 但【授权码模式】和【简单模式】 是认证服务器【authorization server】提供的界面录入的,【密码模式】是客户端提供的界面录入的 ,所以认证服务器提供的界面更加安全些
【简单模式】是没有授权码【code】和刷新token【refresh_code】
>>>如果有人很容易的拿到code 或 refresh token,那么就基本上可以随意随时的去访问你的resource了,因为他可以不断的通过refresh token 去刷新access token。 而为什么第三方的SPA使用的是implicit flow 而第三方的Native App却使用的是authorization code flow? 理论上第三方应用都应该使用【授权码模式】,但是如果你仔细看下,【简化模式】中是没有code 和 refresh token的,而SPA应用(本质上是web,需要通过浏览器的)更加容易去暴露code 和 refresh token, 所以才在第三方的SPA应用中使用了【简单模式】,而只给了access token,
》》》
客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行 授权。
适用于没有前端的命令行应用,即在命令行下请求令牌。一般用来提供给我们完全信任的服务器端服务。
》》》》 用webapi 做案例 ,新建项目 【webapi】
》》》删除自动的Global.asax, 这个文件是程序的入口,删除之后要创建一个 OWIN Startup 命名为 Startup。
using System;
using System.Threading.Tasks;
using Microsoft.Owin;
using Owin;
using System.Web.Http;
using WebApplication3.App_Start;
using Microsoft.Owin.Cors;
[assembly: OwinStartup(typeof(WebApplication3.Startup))]
namespace WebApplication3
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
HttpConfiguration configuration = new HttpConfiguration();
//注册Swagger
//SwaggerConfig.Register(configuration);
//注册WebAPI
WebApiConfig.Register(configuration);
//注册授权服务
AuthorizationConfig.Register(app);
//注册Json的数据展示格式
JsonFormatConfig.Register(configuration);
//跨域配置
app.UseCors(CorsOptions.AllowAll);
app.UseWebApi(configuration);
}
}
}
》》》 新建类 AuthorizationConfig
using Microsoft.Owin.Security.OAuth;
using Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using WebApplication3.Provider;
namespace WebApplication3.App_Start
{
public class AuthorizationConfig
{
public static void Register(Owin.IAppBuilder app)
{
OAuthAuthorizationServerOptions options = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,//允许http而非https访问
TokenEndpointPath = new Microsoft.Owin.PathString(value: "/access_token"),//Token 请求地址
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),//Token的过期时间
Provider = new OpenAuthorizationServerProvider(),//生成Token 配置
RefreshTokenProvider = new OpenRefreshTokenProvider(),//生成RefreshToken的配置
};
app.UseOAuthAuthorizationServer(options);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
}
》》》新建类 JsonFormatConfig
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
namespace WebApplication3.App_Start
{
public class JsonFormatConfig
{
public static void Register(HttpConfiguration configuration)
{
configuration.Formatters.JsonFormatter.SerializerSettings = new Newtonsoft.Json.JsonSerializerSettings()
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),//小驼峰命名
DateFormatString = "yyyy-MM-dd HH:mm:ss" //日期格式化
};
}
}
}
》》》新建文件夹Provider
》》》新建类 OpenAuthorizationServerProvider 生成 access_token
using Microsoft.Owin.Security.OAuth;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading.Tasks;
using System.Security.Claims;
using System.Security.Principal;
namespace WebApplication3.Provider
{
///
/// 授权服务器配置
///
public class OpenAuthorizationServerProvider:OAuthAuthorizationServerProvider
{
///
/// 验证客户端信息
///
///
///
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
//如果是刷新token,且不要验证。
if (context.Parameters.Get("refresh_token") == null)
{
//获取clientId,ClientSecret
string clientId, clientSecret;
if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
{
context.TryGetFormCredentials(out clientId, out clientSecret);
}
//对客户端Id和客户端密码进行校验 是与数据库进行比对
if (clientId == "zen" && clientSecret == "123456")
{
//通过客户端认证
context.Validated(clientId);
}else{
context.Rejected();
}
}
else
{
// 通过客户端认证
context.Validated();
}
return base.ValidateClientAuthentication(context);
}
///
/// 生成客户端模式Access_Token
/// 还需要将对应的客户端信息存储在web中
///
///
///
public override Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
{
//以下即为认证成功
//var identity = new ClaimsIdentity(context.Options.AuthenticationType);
ClaimsIdentity identity = new GenericIdentity( name: context.ClientId, type: OAuthDefaults.AuthenticationType);
//通过查数据库,得到一些用户的信息
int userid = 13;
string role = "管理员";
string scope = "权限1,权限2";
identity.AddClaim(new Claim("userid", userid.ToString()));
identity.AddClaim(new Claim("role", role));
identity.AddClaim(new Claim("scope", scope));
context.Validated(identity);
return base.GrantClientCredentials(context);
}
}
}
》》》新建类 OpenRefreshTokenProvider 刷新token
using Microsoft.Owin.Security.Infrastructure;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace WebApplication3.Provider
{
///
/// 刷新Token配置
///
public class OpenRefreshTokenProvider:AuthenticationTokenProvider
{
private static ConcurrentDictionary<string, string> _refreshTokens = new ConcurrentDictionary<string, string>();
///
/// 生成Refresh_token
///
///
public override void Create(AuthenticationTokenCreateContext context)
{
context.Ticket.Properties.IssuedUtc = DateTime.UtcNow;
context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddDays(30);
context.SetToken(tokenValue:Guid.NewGuid().ToString(format:"N")+Guid.NewGuid().ToString(format:"N"));
_refreshTokens[context.Token] = context.SerializeTicket();
//base.Create(context);
}
///
/// 使用Refresh_token 请求Access_Token
///
///
public override void Receive(AuthenticationTokenReceiveContext context)
{
string value;
if (_refreshTokens.TryRemove(context.Token,out value))
{
context.DeserializeTicket(value);
}
base.Receive(context);
}
}
}
》》》 新建控制器
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Web;
using System.Web.Http;
namespace WebApplication3.Controllers
{
public class HomeController : ApiController
{
// GET: Home
[Authorize]
public string Get()
{
var clientId = HttpContext.Current.User.Identity.Name;
Dictionary<string,string> lst = new Dictionary<string,string>();
foreach (Claim item in (this.User.Identity as ClaimsIdentity).Claims)
{
lst.Add(item.Type,item.Value);
}
return "我是Get方法";
}
// GET: Home
public string Get(int id)
{
return $"这是参数为{id}的Get方法";
}
}
}
》》》测试 用postman
》》客户端凭证 走 GrantClientCredentials方法
ValidateAuthorizeRequest 》》》授权码验证
ValidateClientAuthentication 》》》客户端模式验证
ValidateClientRedirectUri
ValidateTokenRequest 》》》验证令牌请求, 简化模式、隐藏式模式
用户将用户名和密码发送给第三方应用程序,第三方应用程序直接向授权服务器请求访问令牌。
如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式"(password)。
在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而授权服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。
适用场景:公司搭建的授权服务器
(A)用户向客户端提供用户名和密码。
(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。
(C)认证服务器确认无误后,向客户端提供访问令牌。
其它都一样,修改OpenAuthorizationServerProvider 即可
using Microsoft.Owin.Security.OAuth;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading.Tasks;
using System.Security.Claims;
using System.Security.Principal;
namespace WebApplication3.Provider
{
///
/// 授权服务器配置
///
public class OpenAuthorizationServerProvider:OAuthAuthorizationServerProvider
{
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
//获取用户传入的用户名和密码
string UserName = context.UserName;
string Password = context.Password;
//通过查数据库,判断用户名和密码是否正确
//以下只是一个示例,用户名必须以test开头
if (!UserName.StartsWith("test"))
{
context.SetError("invalid_grant", "用户名或密码不正确");
return;
}
//以下即为认证成功
//通过查数据库,得到一些用户的信息
int userid = 13;
string role = "管理员";
string scope = "权限1,权限2";
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim("userid", userid.ToString()));
identity.AddClaim(new Claim("role", role));
identity.AddClaim(new Claim("scope", scope));
context.Validated(identity);
}
///
/// 验证客户端信息
///
///
///
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
可以验证 ClientID、ClientSecret,或不验证
//从上下文中获取ClientID和ClientSecret
context.TryGetFormCredentials(out string clientId, out string clientSecret);
//非法客户端
if (clientId == null || !clientId.StartsWith("AAA"))
{
context.SetError("invalid_clientId", "客户端没有授权");
return Task.FromResult<object>(null);
}
//如果不验证可以直接执行下面的 验证通过
context.Validated();
}
}
}
有些 Web 应用是纯前端应用,没有后端。必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌,这种方式没有授权码这个中间步骤,所以称为(授权码)“隐藏式”(implicit)
简化模式不通过第三方应用程序的服务器,直接在浏览器中向授权服务器申请令牌,跳过了"授权码"这个步骤,所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。所以 不会触发 ValidateClientAuthentication
这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。
using Microsoft.Owin.Security.OAuth;
using Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using WebApplication3.Provider;
namespace WebApplication3.App_Start
{
public class AuthorizationConfig
{
public static void Register(Owin.IAppBuilder app)
{
OAuthAuthorizationServerOptions options = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,//允许http而非https访问
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active,//激活授权码模式
TokenEndpointPath = new Microsoft.Owin.PathString(value: "/token"),//访问host/token获取AccessToken
AuthorizeEndpointPath = new Microsoft.Owin.PathString("/auth"),//访问host/auth获取授权码
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),//AccessToken在30分钟后过期
Provider = new OpenAuthorizationServerProvider(),//AccessToken的提供类
// 简化模式 省略下面代码 简化模式又称为隐式授权码模式,它是授权码模式的一个简化版本
//AuthorizationCodeProvider = new OpenAuthorizationCodeProvider(),//授权码的提供类
RefreshTokenProvider = new OpenRefreshTokenProvider(),//生成RefreshToken的配置
};
app.UseOAuthAuthorizationServer(options);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
}
using Microsoft.Owin.Security.OAuth;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading.Tasks;
using System.Security.Claims;
using System.Security.Principal;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Infrastructure;
namespace WebApplication3.Provider
{
///
/// 授权服务器配置
///
public class OpenAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
///
/// 验证重定向URI是否合法
/// 授权码模式、简化模式 都会触发
///
///
///
public override async Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
string url = context.RedirectUri;
context.Validated(context.RedirectUri);
}
///
/// 验证请求信息是否合法
/// 授权码模式、简化模式 都会触发
///
///
///
public override async Task ValidateAuthorizeRequest(OAuthValidateAuthorizeRequestContext context)
{
if (context.AuthorizeRequest.ClientId.StartsWith("zen"))
{
context.Validated();
}
else
{
context.Rejected();
}
}
///
/// 完成认证,跳转到重定向URI
/// 授权码模式、简化模式 都会触发
///
///
///
public override async Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
{
var identity = new ClaimsIdentity("Bearer");
context.OwinContext.Authentication.SignIn(identity);
context.RequestCompleted();
}
}
}
》》访问
》》直接跳转 access_token 是通过锚链接的
一、是获取授权码,
二、是获取AccessToken
在获取授权码时,我们需要请求host/auth这个地址,输入的参数有以下要求:
(1)grant_type,必须为authorization_code。
(2)response_type,必须为code。
(3)client_id,客户端ID。
(4)redirect_uri,重定向地址,如为http://abc.com/,
则请求授权码完成后,将会重定向到:http://abc.com/code=[授权码]。
(5)scope,授权范围,可选。
(6)state,客户端状态,可选。
》》》Startup 同上
》》》JsonFormatConfig 同上
》》》api控制器同上
》》》OpenRefreshTokenProvider 刷新token 同上
》》》 AuthorizationConfig 类
using Microsoft.Owin.Security.OAuth;
using Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using WebApplication3.Provider;
namespace WebApplication3.App_Start
{
public class AuthorizationConfig
{
public static void Register(Owin.IAppBuilder app)
{
OAuthAuthorizationServerOptions options = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,//允许http而非https访问
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active,//激活授权码模式
TokenEndpointPath = new Microsoft.Owin.PathString(value: "/token"),//访问host/token获取AccessToken
AuthorizeEndpointPath = new Microsoft.Owin.PathString("/auth"),//访问host/auth获取授权码
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),//AccessToken在30分钟后过期
Provider = new OpenAuthorizationServerProvider(),//AccessToken的提供类
// 简化模式 省略下面代码 简化模式又称为隐式授权码模式,它是授权码模式的一个简化版本
AuthorizationCodeProvider = new OpenAuthorizationCodeProvider(),//授权码的提供类
RefreshTokenProvider = new OpenRefreshTokenProvider(),//生成RefreshToken的配置
};
app.UseOAuthAuthorizationServer(options);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
}
》》》OpenAuthorizationServerProvider
using Microsoft.Owin.Security.OAuth;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading.Tasks;
using System.Security.Claims;
using System.Security.Principal;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Infrastructure;
namespace WebApplication3.Provider
{
///
/// 授权服务器配置
///
public class OpenAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
///
/// 验证重定向URI是否合法
/// 授权码模式、简化模式 都会触发
///
///
///
public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
string url = context.RedirectUri;
context.Validated(context.RedirectUri);
return base.ValidateClientRedirectUri(context);
}
///
/// 验证请求信息是否合法
/// 授权码模式、简化模式 都会触发
///
///
///
public override async Task ValidateAuthorizeRequest(OAuthValidateAuthorizeRequestContext context)
{
if (context.AuthorizeRequest.ClientId.StartsWith("zen"))
{
context.Validated();
}
else
{
context.Rejected();
}
}
///
/// 完成认证,跳转到重定向URI
/// 授权码模式、简化模式 都会触发
///
///
///
public override async Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
{
//授权码模式
var redirectUri = context.Request.Query["redirect_uri"];
var clientId = context.Request.Query["client_id"];
var identity = new ClaimsIdentity(new GenericIdentity(
clientId, OAuthDefaults.AuthenticationType));
var authorizeCodeContext = new AuthenticationTokenCreateContext(
context.OwinContext,
context.Options.AuthorizationCodeFormat,
new AuthenticationTicket(
identity,
new AuthenticationProperties(new Dictionary<string, string>
{
{"client_id", clientId},
{"redirect_uri", redirectUri}
})
{
IssuedUtc = DateTimeOffset.UtcNow,
ExpiresUtc = DateTimeOffset.UtcNow.Add(context.Options.AccessTokenExpireTimeSpan)
}));
await context.Options.AuthorizationCodeProvider.CreateAsync(authorizeCodeContext);
context.Response.Write(Uri.EscapeDataString(authorizeCodeContext.Token));//为了测试方便,直接打印出code
//正常使用时是把code加在重定向网址后面
//context.Response.Redirect(redirectUri + "?code=" + Uri.EscapeDataString(authorizeCodeContext.Token));
context.RequestCompleted();
}
///
/// 验证客户端
//
///
///
///
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string cd = context.ClientId;
string clientId, clientSecret;
context.TryGetFormCredentials(out clientId, out clientSecret);
if (!clientId.StartsWith("zen"))
{
context.SetError("invalid_client", "未授权的客户端");
return Task.FromResult<object>(null); ;
}
context.Validated();
return Task.FromResult<object>(null);
}
///
/// 生成客户端模式Access_Token
/// 还需要将对应的客户端信息存储在web中
///
///
///
public override Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
{
//以下即为认证成功
return base.GrantClientCredentials(context);
}
public override Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
// 以下即为认证成功
//var identity = new ClaimsIdentity(context.Options.AuthenticationType);
ClaimsIdentity identity = new GenericIdentity(name: context.ClientId, type: OAuthDefaults.AuthenticationType);
//通过查数据库,得到一些用户的信息
int userid = 13;
string role = "管理员";
string scope = "权限1,权限2";
identity.AddClaim(new Claim("userid", userid.ToString()));
identity.AddClaim(new Claim("role", role));
identity.AddClaim(new Claim("scope", scope));
context.Validated(identity);
return base.GrantResourceOwnerCredentials(context);
}
public override async Task ValidateTokenRequest(OAuthValidateTokenRequestContext context)
{
if (context.TokenRequest.IsAuthorizationCodeGrantType)
{
context.Validated();
}
else
{
context.Rejected();
}
}
}
}
》》》 OpenAuthorizationCodeProvider
using Microsoft.Owin.Security.Infrastructure;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
namespace WebApplication3.Provider
{
public class OpenAuthorizationCodeProvider : IAuthenticationTokenProvider
{
private Dictionary<string, string> codes = new Dictionary<string, string>();
public void Create(AuthenticationTokenCreateContext context)
{
string new_code = Guid.NewGuid().ToString("n");
context.SetToken(new_code);
//context.SerializeTicket() 生成token
codes.Add(new_code, context.SerializeTicket());
}
public Task CreateAsync(AuthenticationTokenCreateContext context)
{
Create(context);
return Task.FromResult<object>(null);
}
public void Receive(AuthenticationTokenReceiveContext context)
{
string code = context.Token;
if (codes.ContainsKey(code))
{
string value = codes[code];
codes.Remove(code);
context.DeserializeTicket(value);
}
}
public Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
Receive(context);
return Task.FromResult<object>(null);
}
}
}
access_token不能暴露在浏览器那么该存放在哪?
重定向传回access_token会使安全保密性要求极高的访问令牌暴露在浏览器,增加访问令牌失窃风险。
在我看来,重定向携带的参数在URL上,http协议下重定向传回access_token的形式,是没有经过数据加密的,他会增加令牌失窃的风险。那么关于access_token存放在哪的问题,个人认为通过授权码以及客户端id和secret共同校验后获取的access_token,可以把access_token存放在localStorage中,localStorage虽然是永久存储,但是access_token会有一个有效期,有效期到了之后,即便access_token一直都存在但是有效期过后就无法访问到受保护资源。
》》》webstorage (sessionStorage和localStorage)
sessionStorage和localStorage区别
**注意: **不同浏览器无法共享localStorage或sessionStorage中的信息。
相同浏览器的不同页面间【相同域名和端口】可以共享相同的 localStorage,
但是不同页面或标签页间无法共享sessionStorage的信息。