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 /// </summary>