ASP.NET WebApi OWIN 實現 OAuth 2.0

来源:http://www.cnblogs.com/xishuai/archive/2016/12/21/aspnet-webapi-owin-oauth2.html
-Advertisement-
Play Games

OAuth(開放授權)是一個開放標準,允許用戶讓第三方應用訪問該用戶在某一網站上存儲的私密的資源(如照片,視頻,聯繫人列表),而無需將用戶名和密碼提供給第三方應用。 OAuth 允許用戶提供一個令牌,而不是用戶名和密碼來訪問他們存放在特定服務提供者的數據.每一個令牌授權一個特定的網站(例如,視頻編輯 ...


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>
    public override async Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
    {
        context.Validated(context.RedirectUri);
    }

    /// <summary>
    /// 驗證 access_token 的請求
    /// </summary>
    public override async Task ValidateTokenRequest(OAuthValidateTokenRequestContext context)
    {
        if (context.TokenRequest.IsAuthorizationCodeGrantType || context.TokenRequest.IsRefreshTokenGrantType)
        {
            context.Validated();
        }
        else
        {
            context.Rejected();
        }
    }
}

需要註意的是,ValidateClientAuthentication 並不需要對 clientSecret 進行驗證,另外,AuthorizeEndpoint 只是生成 authorization_code,並沒有生成 access_token,生成操作在 OpenAuthorizationCodeProvider 中的 Receive 方法。

OpenAuthorizationCodeProvider 示例代碼:

public class OpenAuthorizationCodeProvider : AuthenticationTokenProvider
{
    private readonly ConcurrentDictionary<string, string> _authenticationCodes = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);

    /// <summary>
    /// 生成 authorization_code
    /// </summary>
    public override void Create(AuthenticationTokenCreateContext context)
    {
        context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
        _authenticationCodes[context.Token] = context.SerializeTicket();
    }

    /// <summary>
    /// 由 authorization_code 解析成 access_token
    /// </summary>
    public override void Receive(AuthenticationTokenReceiveContext context)
    {
        string value;
        if (_authenticationCodes.TryRemove(context.Token, out value))
        {
            context.DeserializeTicket(value);
        }
    }
}

上面 Create 方法是 await context.Options.AuthorizationCodeProvider.CreateAsync(authorizeCodeContext); 的重載方法。

OpenRefreshTokenProvider 示例代碼:

public class OpenRefreshTokenProvider : AuthenticationTokenProvider
{
    private static ConcurrentDictionary<string, string> _refreshTokens = new ConcurrentDictionary<string, string>();

    /// <summary>
    /// 生成 refresh_token
    /// </summary>
    public override void Create(AuthenticationTokenCreateContext context)
    {
        context.Ticket.Properties.IssuedUtc = DateTime.UtcNow;
        context.Ticket.Properties.ExpiresUtc = DateTime.UtcNow.AddDays(60);

        context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
        _refreshTokens[context.Token] = context.SerializeTicket();
    }

    /// <summary>
    /// 由 refresh_token 解析成 access_token
    /// </summary>
    public override void Receive(AuthenticationTokenReceiveContext context)
    {
        string value;
        if (_refreshTokens.TryRemove(context.Token, out value))
        {
            context.DeserializeTicket(value);
        }
    }
}

refresh_token 的作用就是,在 access_token 過期的時候,不需要再通過一些憑證申請 access_token,而是直接通過 refresh_token 就可以重新申請 access_token。

另外,需要一個 api 來接受 authorization_code(來自 redirect_uri 的回調跳轉),實現代碼如下:

public class CodesController : ApiController
{
    [HttpGet]
    [Route("api/authorization_code")]
    public HttpResponseMessage Get(string code)
    {
        return new HttpResponseMessage()
        {
            Content = new StringContent(code, Encoding.UTF8, "text/plain")
        };
    }
}

基本上面代碼已經實現了,單元測試代碼如下:

public class OAuthClientTest
{
    private const string HOST_ADDRESS = "http://localhost:8001";
    private IDisposable _webApp;
    private static HttpClient _httpClient;

    public OAuthClientTest()
    {
        _webApp = WebApp.Start<Startup>(HOST_ADDRESS);
        Console.WriteLine("Web API started!");
        _httpClient = new HttpClient();
        _httpClient.BaseAddress = new Uri(HOST_ADDRESS);
        Console.WriteLine("HttpClient started!");
    }

    private static async Task<TokenResponse> GetToken(string grantType, string refreshToken = null, string userName = null, string password = null, string authorizationCode = null)
    {
        var clientId = "xishuai";
        var clientSecret = "123";
        var parameters = new Dictionary<string, string>();
        parameters.Add("grant_type", grantType);

        if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(password))
        {
            parameters.Add("username", userName);
            parameters.Add("password", password);
        }
        if (!string.IsNullOrEmpty(authorizationCode))
        {
            parameters.Add("code", authorizationCode);
            parameters.Add("redirect_uri", "http://localhost:8001/api/authorization_code"); //和獲取 authorization_code 的 redirect_uri 必須一致,不然會報錯
        }
        if (!string.IsNullOrEmpty(refreshToken))
        {
            parameters.Add("refresh_token", refreshToken);
        }

        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
            "Basic",
            Convert.ToBase64String(Encoding.ASCII.GetBytes(clientId + ":" + clientSecret)));

        var response = await _httpClient.PostAsync("/token", new FormUrlEncodedContent(parameters));
        var responseValue = await response.Content.ReadAsStringAsync();
        if (response.StatusCode != HttpStatusCode.OK)
        {
            Console.WriteLine(response.StatusCode);
            Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
            return null;
        }
        return await response.Content.ReadAsAsync<TokenResponse>();
    }

    private static async Task<string> GetAuthorizationCode()
    {
        var clientId = "xishuai";

        var response = await _httpClient.GetAsync($"/authorize?grant_type=authorization_code&response_type=code&client_id={clientId}&redirect_uri={HttpUtility.UrlEncode("http://localhost:8001/api/authorization_code")}");
        var authorizationCode = await response.Content.ReadAsStringAsync();
        if (response.StatusCode != HttpStatusCode.OK)
        {
            Console.WriteLine(response.StatusCode);
            Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
            return null;
        }
        return authorizationCode;
    }

    [Fact]
    public async Task OAuth_AuthorizationCode_Test()
    {
        var authorizationCode = GetAuthorizationCode().Result; //獲取 authorization_code
        var tokenResponse = GetToken("authorization_code", null, null, null, authorizationCode).Result; //根據 authorization_code 獲取 access_token
        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);

        var response = await _httpClient.GetAsync($"/api/values");
        if (response.StatusCode != HttpStatusCode.OK)
        {
            Console.WriteLine(response.StatusCode);
            Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
        }
        Console.WriteLine(await response.Content.ReadAsStringAsync());
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);

        Thread.Sleep(10000);

        var tokenResponseTwo = GetToken("refresh_token", tokenResponse.RefreshToken).Result; //根據 refresh_token 獲取 access_token
        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponseTwo.AccessToken);
        var responseTwo = await _httpClient.GetAsync($"/api/values");
        Assert.Equal(HttpStatusCode.OK, responseTwo.StatusCode);
    }
}

Startup 配置的 access_token 過期時間是 10s,線程休眠 10s,是為了測試 refresh_token。

上面單元測試代碼,執行成功,當然也可以用 Postman 模擬請求測試。

3. 簡化模式(implicit grant type)

簡單解釋:授權碼模式的簡化版,省略 authorization_code,並且 access_token 以 URL 參數返回(比如 #token=xxxx)。

請求授權服務(只有一次),需要的參數:

  • response_type:必選,授權類型,值固定為 "token"。
  • client_id:必選,客戶端 ID。
  • redirect_uri:必選,重定向 URI,URL 中會包含 access_token。
  • scope:可選,申請的許可權範圍,比如微博授權服務值為 follow_app_official_microblog。
  • state:可選,客戶端的當前狀態,可以指定任意值,授權伺服器會原封不動地返回這個值,比如微博授權服務值為 weibo。

需要註意的是,簡化模式請求參數並不需要 grant_type,並且可以用 http get 直接請求。

Startup 代碼:

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 相關授權服務
            RefreshTokenProvider = new OpenRefreshTokenProvider() //refresh_token 授權服務
        };
        app.UseOAuthBearerTokens(OAuthOptions); //表示 token_type 使用 bearer 方式
    }
}

OpenRefreshTokenProvider、OpenAuthorizationServerProvider 的代碼就不貼了,和上面授權碼模式一樣,只不過在 OpenAuthorizationServerProvider 的 AuthorizeEndpoint 方法中有 IsImplicitGrantType 判斷,示例代碼:

var identity = new ClaimsIdentity("Bearer");
context.OwinContext.Authentication.SignIn(identity);
context.RequestCompleted();

這段代碼執行會直接回調 redirect_uri,並附上 access_token,接受示例代碼:

[HttpGet]
[Route("api/access_token")]
public HttpResponseMessage GetToken()
{
    var url = Request.RequestUri;
    return new HttpResponseMessage()
    {
        Content = new StringContent("", Encoding.UTF8, "text/plain")
    };
}

單元測試代碼:

[Fact]
public async Task OAuth_Implicit_Test()
{
    var clientId = "xishuai";

    var tokenResponse = await _httpClient.GetAsync($"/authorize?response_type=token&client_id={clientId}&redirect_uri={HttpUtility.UrlEncode("http://localhost:8001/api/access_token")}");
    //redirect_uri: http://localhost:8001/api/access_token#access_token=AQAAANCMnd8BFdERjHoAwE_Cl-sBAAAAfoPB4HZ0PUe-X6h0UUs2q42&token_type=bearer&expires_in=10
    var accessToken = "";//get form redirect_uri
    _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

    var response = await _httpClient.GetAsync($"/api/values");
    if (response.StatusCode != HttpStatusCode.OK)
    {
        Console.WriteLine(response.StatusCode);
        Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
    }
    Console.WriteLine(await response.Content.ReadAsStringAsync());
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

回調 redirect_uri 中的 access_token 參數值,因為在 URL 的 # 後,後端不好獲取到,所以這裡的單元測試只是示例,並不能執行成功,建議使用 Poastman 進行測試。

4. 密碼模式(resource owner password credentials)

簡單解釋:在一開始敘述的 OAuth 授權流程的時候,其實就是密碼模式,落網發起授權請求,用戶在微博的授權頁面填寫賬號和密碼,驗證成功則返回 access_token,所以,在此過程中,用戶填寫的賬號和密碼,和落網沒有半毛錢關係,不會存在賬戶信息被第三方竊取問題。

請求授權服務(只有一次),需要的參數:

  • grant_type:必選,授權模式,值固定為 "password"。
  • username:必選,用戶名。
  • password:必選,用戶密碼。
  • scope:可選,申請的許可權範圍,比如微博授權服務值為 follow_app_official_microblog。

Startup 代碼:

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 相關授權服務
            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>
    /// 生成 access_token(resource owner password credentials 授權方式)
    /// </summary>
    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        if (string.IsNullOrEmpty(context.UserName))
        {
            context.SetError("invalid_username", "username is not valid");
            return;
        }
        if (string.IsNullOrEmpty(context.Password))
        {
            context.SetError("invalid_password", "password is not valid");
            return;
        }

        if (context.UserName != "xishuai" || context.Password != "123")
        {
            context.SetError("invalid_identity", "username or password is not valid");
            return;
        }

        var OAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
        OAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
        context.Validated(OAuthIdentity);
    }
}

GrantResourceOwnerCredentials 內部可以調用外部服務,以進行對用戶賬戶信息的驗證。

單元測試代碼:

[Fact]
public async Task OAuth_Password_Test()
{
    var tokenResponse = GetToken("password", null, "xishuai", "123").Result; //獲取 access_token
    _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);

    var response = await _httpClient.GetAsync($"/api/values");
    if (response.StatusCode != HttpStatusCode.OK)
    {
        Console.WriteLine(response.StatusCode);
        Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
    }
    Console.WriteLine(await response.Content.ReadAsStringAsync());
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);

    Thread.Sleep(10000);

    var tokenResponseTwo = GetToken("refresh_token", tokenResponse.RefreshToken).Result;
    _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponseTwo.AccessToken);
    var responseTwo = await _httpClient.GetAsync($"/api/values");
    Assert.Equal(HttpStatusCode.OK, responseTwo.StatusCode);
}

5. 客戶端模式(Client Credentials Grant)

簡單解釋:顧名思義,客戶端模式就是客戶端直接向授權服務發起請求,和用戶沒什麼關係,也就是說落網直接向微博提交授權請求,此類的請求不包含用戶信息,一般用作應用程式直接的交互等。

請求授權服務(只有一次),需要的參數:

  • grant_type:必選,授權模式,值固定為 "client_credentials"。
  • client_id:必選,客戶端 ID。
  • client_secret:必選,客戶端密碼。
  • scope:可選,申請的許可權範圍,比如微博授權服務值為 follow_app_official_microblog。

Startup 代碼:

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 相關授權服務
            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" || clientSecret != "123")
        {
            context.SetError("invalid_client", "client or clientSecret is not valid");
            return;
        }
        context.Validated();
    }

    /// <summary>
    /// 生成 access_token(client credentials 授權方式)
    /// </summary>
    public override async Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
    {
        var identity = new ClaimsIdentity(new GenericIdentity(
            context.ClientId, OAuthDefaults.AuthenticationType),
            context.Scope.Select(x => new Claim("urn:oauth:scope", x)));

        context.Validated(identity);
    }
}

和其他授權模式不同,客戶端授權模式需要對 client_secret 進行驗證(ValidateClientAuthentication)。

單元測試代碼:

[Fact]
public async Task OAuth_ClientCredentials_Test()
{
    var tokenResponse = GetToken("client_credentials").Result; //獲取 access_token
    _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);

    var response = await _httpClient.GetAsync($"/api/values");
    if (response.StatusCode != HttpStatusCode.OK)
    {
        Console.WriteLine(response.StatusCode);
        Console.WriteLine((await response.Content.ReadAsAsync<HttpError>()).ExceptionMessage);
    }
    Console.WriteLine(await response.Content.ReadAsStringAsync());
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);

    Thread.Sleep(10000);

    var tokenResponseTwo = GetToken("refresh_token", tokenResponse.RefreshToken).Result;
    _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponseTwo.AccessToken);
    var responseTwo = await _httpClient.GetAsync($"/api/values");
    Assert.Equal(HttpStatusCode.OK, responseTwo.StatusCode);
}

除了上面四種授權模式之外,還有一種就是更新令牌(refresh token),單元測試代碼中已經體現了,需要額外的兩個參數:

  • grant_type:必選,授權模式,值固定為 "refresh_token"。
  • refresh_token:必選,授權返回的 refresh_token。

最後,總結下四種授權模式的應用場景:

  • 授權碼模式(authorization code):引入 authorization_code,可以增加系統的安全性,和客戶端應用場景差不多,但一般用於 Server 端。
  • 簡化模式(implicit):無需 Server 端的介入,前端可以直接完成,一般用於前端操作。
  • 密碼模式(resource owner password credentials):和用戶賬戶相關,一般用於第三方登錄。
  • 客戶端模式(client credentials):和用戶無關,一般用於應用程式和 api 之間的交互場景,比如落網開放出 api,供第三方開發者進行調用數據等。

開源地址:https://github.com/yuezhongxin/OAuth2.Demo

參考資料:


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文是介紹MySQL資料庫InnoDB存儲引擎重做日誌漫游 00 – Undo LogUndo Log 是為了實現事務的原子性,在MySQL資料庫InnoDB存儲引擎中,還用Undo Log來實現多版本併發控制(簡稱:MVCC)。 - 事務的原子性(Atomicity) 事務中的所有操作,要麼全部完 ...
  • 1.環境準備 手動添加資料庫依賴: 在package.json的dependencies中新增, “mysql” : “latest”, 使用命令安裝mysql並添加依賴: 2.官方例子: 運行node ...
  • 一:在Dos里切換盤符 a:在電腦左下角右擊顯示圖片;(我用的是win10系統,其他系統類似) b:點擊運行,輸入cmd; c:點擊確定: d:輸入盤符:(如f:) 或F: 只寫字母,不寫分號是不行的!如下圖: cd f:(F:)是不行的如下圖: ...
  • 我在 "Linux字元設備驅動框架" 一文中已經簡單的介紹了字元設備驅動的基本的編程框架,這裡我們來探討一下Linux內核(以4.8.5內核為例)是怎麼管理字元設備的,即當我們獲得了設備號,分配了 cdev 結構,註冊了驅動的操作方法集,最後進行 cdev_add() 的時候,究竟是將哪些內容告訴了 ...
  • snull是《Linux Device Drivers》中的一個網路驅動的例子。這裡引用這個例子學習Linux網路驅動。 因為snull的源碼,網上已經更新到適合最新內核,而我自己用的還是2.6.22.6比較舊的內核。而網上好像找不到舊版的snull。因此結合《Linux Device Driver ...
  • <! 知識的力量是無限的(當然肯定還有更簡單的方法) !> 當我考慮將省市區三級聯動數據從mysql轉入mongodb時遇到了網上無直接插入mongodb的示例(基本均是mysql插入示例)。於是想到利用json文件直接導入mongodb會比較easy(SQLyog如何導出json?) 在SQLyo ...
  • Linux內核大量使用面向對象的設計思想,通過追蹤源碼,我們甚至可以使用面向對象語言常用的UML類圖來分析Linux設備管理的"類"之間的關係。這裡以4.8.5內核為例從kobject,kset,kobj_type的分析入手,進而一探內核對於設備的管理方式 container_of巨集 這個巨集幾乎是l ...
  • 首先設定loop數量(ubuntu系統) 更改以下內容: 然後更新grub 重啟電腦 之後就可以看到loop設備數量變為64個 設定loop數量有此系統不一樣,archlinux可參考我的另一隨筆http://www.cnblogs.com/LTSperl/p/6195493.html。 然後就是今 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...