1. 程式人生 > >ASP.NET WebApi OWIN 實現 OAuth 2.0

ASP.NET WebApi OWIN 實現 OAuth 2.0

OAuth(開放授權)是一個開放標準,允許使用者讓第三方應用訪問該使用者在某一網站上儲存的私密的資源(如照片,視訊,聯絡人列表),而無需將使用者名稱和密碼提供給第三方應用。

OAuth 允許使用者提供一個令牌,而不是使用者名稱和密碼來訪問他們存放在特定服務提供者的資料。每一個令牌授權一個特定的網站(例如,視訊編輯網站)在特定的時段(例如,接下來的 2 小時內)內訪問特定的資源(例如僅僅是某一相簿中的視訊)。這樣,OAuth 讓使用者可以授權第三方網站訪問他們儲存在另外服務提供者的某些特定資訊,而非所有內容。

OAuth 是什麼?為什麼要使用 OAuth?上面的概念已經很明確了,這裡就不詳細說明了。

閱讀目錄:

  • 執行流程和授權模式
  • 授權碼模式(authorization code)
  • 簡化模式(implicit grant type)
  • 密碼模式(resource owner password credentials)
  • 客戶端模式(Client Credentials Grant)

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,分別實現上面的四種授權模式。

簡單解釋:落網提供一些授權憑證,從微博授權服務獲取到 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,供第三方開發者進行呼叫資料等。

參考資料:

相關推薦

ASP.NET WebApi OWIN 實現 OAuth 2.0(自定義獲取 Token)

href timespan 獲取 edi prot cep b- med 2-0 相關文章:ASP.NET WebApi OWIN 實現 OAuth 2.0 之前的項目實現,Token 放在請求頭的 Headers 裏面,類似於這樣: Accept: application

ASP.NET WebApi OWIN 實現 OAuth 2.0

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

基於ASP.NET WebAPI OWIN實現Self-Host項目實戰

hosting 知識 工作 develop plist 簡單 eba 直接 sock 引用 寄宿ASP.NET Web API 不一定需要IIS 的支持,我們可以采用Self Host 的方式使用任意類型的應用程序(控制臺、Windows Forms 應用、WPF 應

Spring Boot實現OAuth 2.0(二)-- 自定義許可權驗證

自定義攔截器進行許可權驗證 涉及到的姿勢: 自定義註解 攔截器 Spring Boot新增攔截器 文章目錄: 自定義註解 @Target(ElementType.METHOD)//作用在方法 @Retention(RetentionP

Spring Boot實現OAuth 2.0

忘記po原始碼了,點這裡[github 原始碼] 開篇當然是包結構啦。 @EnableAuthorizationServer @SpringBootApplication 【更正】 ( scanBasePackages = {“co

WEBAPI基於Owin中介軟體實現身份驗證例項(OAUTH 2.0方式)附原始碼

1,在Webapi專案下新增如下引用: Microsoft.AspNet.WebApi.Owin Owin Microsoft.Owin.Host.SystemWeb Microsoft.Owin.Security.OAuth Microsoft.Owin.Secu

IdentityServer4 ASP.NET Core的OpenID Connect OAuth 2.0框架學習保護API

IdentityServer4 ASP.NET Core的OpenID Connect OAuth 2.0框架學習之保護API。 使用IdentityServer4 來實現使用客戶端憑據保護ASP.NET Core Web API 訪問。 IdentityServer4 GitHub: https://g

ASP.NET WebApi 基於OAuth2.0實現Token簽名認證

oauth2 oauth 如何 nsh class 應用 post請求 實現 最重要的 一、課程介紹 明人不說暗話,跟著阿笨一起玩WebApi!開發提供數據的WebApi服務,最重要的是數據的安全性。那麽對於我們來說,如何確保數據的安全將是我們需要思考的問題。為了保護

Host ASP.NET WebApi in Owin

public define nuget get log 文檔 getname hang null 什麽是OWIN                                             Owin其實是微軟為了解耦.Net Web app對IIS的依賴而制定

OWIN OAuth 2.0 Authorization Server

href auth docs alt tar 參考 服務器 ges tps 參考:https://docs.microsoft.com/en-us/aspnet/aspnet/overview/owin-and-katana/owin-oauth-20-authorizat

asp.net webapi+swagger+OAuth2.0

== status direct lap .get path all innertext stat 文檔繼續完善整理中。。。。。。 c.DocumentFilter<SwaggerDocTag>(); /// <summary>

.Net Identity OAuth 2.0 SecurityStamp 使用

起源: 近期幫別人做專案,涉及到OAuth認證,服務端主動使token失效,要使對應使用者不能再繼續訪問,只能重新登陸,或者重新授權。 場景: 這種場景OAuth2.0是支援的,比如使用者修改了密碼,那所有之前保留在各個客戶端的token均失效,要求使用者重新提供憑證。 原因: 在之前的專案中,一旦

ASP.NET WebAPI 雙向token實現對接小程式登入邏輯

最近在學習用asp.net webapi搭建小程式的後臺服務,因為基於小程式端和後臺二者的通訊,不像OAuth(開放授權),存在第三方應用。所以這個token是雙向的,一個是對使用者的,一個是對介面的。本來做了一份是用Oauth的,用的是第三種密碼策略模式。但是因為不存在第三方應用,所以不用Oauth這種授權

WebApi 增加身份驗證 (OAuth 2.0方式)

1,在Webapi專案下新增如下引用:Microsoft.AspNet.WebApi.OwinOwinMicrosoft.Owin.Host.SystemWebMicrosoft.Owin.Security.OAuthMicrosoft.Owin.Security.CookiesMicrosoft.AspNe

ASP.NET WebApi 基於分散式Session方式實現Token簽名認證(釋出版)

一、課程介紹 明人不說暗話,跟著阿笨一起學玩WebApi!開發提供資料的WebApi服務,最重要的是資料的安全性。那麼對於我們來說,如何確保資料的安全將會是需要思考的問題。在ASP.NETWebService服務中可以通過SoapHead驗證機制來實現,那麼在ASP.NET WebApi中我們應該如何保

ASP.NET WebApi服務介面如何防止重複請求實現HTTP冪等性(八)

一、背景描述與課程介紹 明人不說暗話,跟著阿笨一起玩WebApi。在我們平時開發專案中可能會出現下面這些情況; 1)、由於使用者誤操作,多次點選網頁表單提交按鈕。由於網速等原因造成頁面卡頓,使用者重複重新整理提交頁面。黑客或惡意使用者使用postman等工具重複惡意提交表單(攻擊網站)。這些情況都會導

ASP.NET WebApi 基於JWT實現Token簽名認證(釋出版)

一、前言 明人不說暗話,跟著阿笨一起玩WebApi!開發提供資料的WebApi服務,最重要的是資料的安全性。那麼對於我們來說,如何確保資料的安全將會是需要思考的問題。在ASP.NET WebService服務中可以通過SoapHead驗證機制來實現,那麼在ASP.NET WebApi中我們應該如何保證我

ASP.NET Web API實現快取的2種方式

在ASP.NET Web API中實現快取大致有2種思路。一種是通過ETag, 一種是通過類似ASP.NET MVC中的OutputCache。 通過ETag實現快取 首先安裝cachecow.ser

IdentityServer4 實現 OpenID Connect 和 OAuth 2.0

http key 嚴格 簡單 alt 三種 pan 清除 推送 原文:IdentityServer4 實現 OpenID Connect 和 OAuth 2.0OAuth 2.0 。順便說一下個人理解授權與認證的區別,授權可以理解為房間的主人允許你進入他的房間,但是房間的主

Asp.net基於session實現購物車的方法

lai 程序 clas contain ext info border mode man 本文實例講述了asp.net基於session實現購物車的方法。分享給大家供大家參考,具體如下: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 1