NET WebApi OWIN 实现 OAuth 2.0
OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
OAuth 允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站(例如,视频编辑网站)在特定的时段(例如,接下来的 2 小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth 让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。
以上概念来自:https://zh.wikipedia.org/wiki/OAuth
OAuth 是什么?为什么要使用 OAuth?上面的概念已经很明确了,这里就不详细说明了。
阅读目录:
- 运行流程和授权模式
- 授权码模式(authorization code)
- 简化模式(implicit grant type)
- 密码模式(resource owner password credentials)
- 客户端模式(Client Credentials Grant)
开源地址:https://github.com/yuezhongxin/OAuth2.Demo
1. 运行流程和授权模式
关于 OAuth 2.0 的运行流程(来自 RFC 6749):
这里我们模拟一个场景:用户听落网,但需要登录才能收藏期刊,然后用快捷登录方式,使用微博的账号和密码登录后,落网就可以访问到微博的账号信息等,并且在落网也已登录,最后用户就可以收藏期刊了。
结合上面的场景,详细说下 OAuth 2.0 的运行流程:
- (A) 用户登录落网,落网询求用户的登录授权(真实操作是用户在落网登录)。
- (B) 用户同意登录授权(真实操作是用户打开了快捷登录,用户输入了微博的账号和密码)。
- (C) 由落网跳转到微博的授权页面,并请求授权(微博账号和密码在这里需要)。
- (D) 微博验证用户输入的账号和密码,如果成功,则将 access_token 返回给落网。
- (E) 落网拿到返回的 access_token,请求微博。
- (F) 微博验证落网提供的 access_token,如果成功,则将微博的账户信息返回给落网。
图中的名词解释:
- Client -> 落网
- Resource Owner -> 用户
- Authorization Server -> 微博授权服务
- Resource Server -> 微博资源服务
其实,我不是很理解 ABC 操作,我觉得 ABC 可以合成一个 C:落网打开微博的授权页面,用户输入微博的账号和密码,请求验证。
OAuth 2.0 四种授权模式:
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
下面我们使用 ASP.NET WebApi OWIN,分别实现上面的四种授权模式。
2. 授权码模式(authorization code)
简单解释:落网提供一些授权凭证,从微博授权服务获取到 authorization_code,然后根据 authorization_code,再获取到 access_token,落网需要请求微博授权服务两次。
第一次请求授权服务(获取 authorization_code),需要的参数:
- grant_type:必选,授权模式,值为 "authorization_code"。
- response_type:必选,授权类型,值固定为 "code"。
- client_id:必选,客户端 ID。
- redirect_uri:必选,重定向 URI,URL 中会包含 authorization_code。
- scope:可选,申请的权限范围,比如微博授权服务值为 follow_app_official_microblog。
- state:可选,客户端的当前状态,可以指定任意值,授权服务器会原封不动地返回这个值,比如微博授权服务值为 weibo。
第二次请求授权服务(获取 access_token),需要的参数:
- grant_type:必选,授权模式,值为 "authorization_code"。
- code:必选,授权码,值为上面请求返回的 authorization_code。
- redirect_uri:必选,重定向 URI,必须和上面请求的 redirect_uri 值一样。
- client_id:必选,客户端 ID。
第二次请求授权服务(获取 access_token),返回的参数:
- access_token:访问令牌.
- token_type:令牌类型,值一般为 "bearer"。
- expires_in:过期时间,单位为秒。
- refresh_token:更新令牌,用来获取下一次的访问令牌。
- scope:权限范围。
ASP.NET WebApi OWIN 需要安装的程序包:
- Owin
- Microsoft.Owin.Host.SystemWeb
- Microsoft.Owin.Security.OAuth
- Microsoft.Owin.Security.Cookies
- Microsoft.AspNet.Identity.Owin
在项目中创建 Startup.cs 文件,添加如下代码:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
var OAuthOptions = new OAuthAuthorizationServerOptions
{
AllowInsecureHttp = true,
AuthenticationMode = AuthenticationMode.Active,
TokenEndpointPath = new PathString("/token"), //获取 access_token 授权服务请求地址
AuthorizeEndpointPath=new PathString("/authorize"), //获取 authorization_code 授权服务请求地址
AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10), //access_token 过期时间
Provider = new OpenAuthorizationServerProvider(), //access_token 相关授权服务
AuthorizationCodeProvider = new OpenAuthorizationCodeProvider(), //authorization_code 授权服务
RefreshTokenProvider = new OpenRefreshTokenProvider() //refresh_token 授权服务
};
app.UseOAuthBearerTokens(OAuthOptions); //表示 token_type 使用 bearer 方式
}
}
OpenAuthorizationServerProvider 示例代码:
public class OpenAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
/// <summary>
/// 验证 client 信息
/// </summary>
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientId;
string clientSecret;
if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
{
context.TryGetFormCredentials(out clientId, out clientSecret);
}
if (clientId != "xishuai")
{
context.SetError("invalid_client", "client is not valid");
return;
}
context.Validated();
}
/// <summary>
/// 生成 authorization_code(authorization code 授权方式)、生成 access_token (implicit 授权模式)
/// </summary>
public override async Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
{
if (context.AuthorizeRequest.IsImplicitGrantType)
{
//implicit 授权方式
var identity = new ClaimsIdentity("Bearer");
context.OwinContext.Authentication.SignIn(identity);
context.RequestCompleted();
}
else if (context.AuthorizeRequest.IsAuthorizationCodeGrantType)
{
//authorization code 授权方式
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.AuthorizationCodeExpireTimeSpan)
}));
await context.Options.AuthorizationCodeProvider.CreateAsync(authorizeCodeContext);
context.Response.Redirect(redirectUri + "?code=" + Uri.EscapeDataString(authorizeCodeContext.Token));
context.RequestCompleted();
}
}
/// <summary>
/// 验证 authorization_code 的请求
/// </summary>
public override async Task ValidateAuthorizeRequest(OAuthValidateAuthorizeRequestContext context)
{
if (context.AuthorizeRequest.ClientId == "xishuai" &&
(context.AuthorizeRequest.IsAuthorizationCodeGrantType || context.AuthorizeRequest.IsImplicitGrantType))
{
context.Validated();
}
else
{
context.Rejected();
}
}
/// <summary>
/// 验证 redirect_uri
///