首页 > 编程语言 >ASP.NET WebApi OWIN 实现 OAuth 2.0

ASP.NET WebApi OWIN 实现 OAuth 2.0

时间:2024-09-30 11:04:38浏览次数:10  
标签:WebApi ASP access token code context 授权 OAuth new

ASP.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>
    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

参考资料:

标签:WebApi,ASP,access,token,code,context,授权,OAuth,new
From: https://www.cnblogs.com/sexintercourse/p/18441440

相关文章

  • ASP.NET WebApi OWIN 实现 OAuth 2.0(自定义获取 Token)
    ASP.NETWebApiOWIN实现OAuth2.0(自定义获取Token) 相关文章:ASP.NETWebApiOWIN实现OAuth2.0之前的项目实现,Token放在请求头的Headers里面,类似于这样:Accept:application/jsonContent-Type:application/jsonAuthorization:BearerpADKsjwMv927u...虽然这是......
  • 爱与恨的抉择:ASP.NET 5+EntityFramework 7
    爱与恨的抉择:ASP.NET5+EntityFramework7  EF7的纠缠ASP.NET5的无助忘不了你的好一开始列出的这个博文大纲,让我想到了很久之前的一篇博文:恋爱虽易,相处不易:当EntityFramework爱上AutoMapper,只不过这次的剧情换主角了,而且与EF和AutoMapper爱情故事不同的是,这次是......
  • 解决 ASP.NET Core MySql varchar 字符串截取(长度 255)
    解决ASP.NETCoreMySqlvarchar字符串截取(长度255) ASP.NETCore中使用MySql,如果字段类型为varchar,不管设置多少长度,插入或更新数据的时候,会自动截断(截取255长度的字符)。出现问题的原因,就是使用了MySql.Data.EntityFrameworkCore程序包(我使用的版本是7.0.7-m6),可能是......
  • [.NET Blog] .NET Aspire 测试入门
    https://devblogs.microsoft.com/dotnet/getting-started-with-testing-and-dotnet-aspire/自动化测试是软件开发的重要一环。它可以帮助我们尽早确认软件中的缺陷和防止回归问题。在本文中,我们将探讨如何在.NETAspire中开始测试,支持我们进行跨分布式应用的测试场景。测试分......
  • .Net 6 WebApi 项目部署到 Linux 系统上的 Docker 容器
    .Net6WebApi项目部署到Linux系统上的Docker容器 1.创建一个基础的WebApi项目  注意:因为发布时候,Dockerfile文件必须和解决方案.cspro文件放在同级,所以建议勾上这个,当时遇到这个问题,导致打包镜像时找不到.cspro文件,搞了好久  点击创建,项目基础框架是这样......
  • 在 ASP.NET Core Web API 中使用操作筛选器统一处理通用操作
    前言:什么是操作筛选器操作筛选器是ASP.NETCoreWebAPI中的一种过滤器,用于在执行控制器操作(Action)之前或之后执行一些代码,完成特定的功能,比如执行日志记录、身份验证、授权、异常处理等通用的处理逻辑。每次ASP.NETCoreWebAPI中控制器的操作方法被执行的时候,操作筛选器......
  • C# ASP.NET Core Web API 框架 实现向手机发送验证码短信
    本文章主要是在C#ASP.NETCoreWebAPI框架实现向手机发送验证码短信功能。这里我选择是一个互亿无线短信验证码平台,其实像阿里云,腾讯云上面也可以。首先我们先去互亿无线https://www.ihuyi.com/api/sms.html去注册一个账号注册完成账号后,它会送10条免费短信以及通话验证......
  • C# Linq.FirstOrDefault、Linq.Where、Linq.AsParallel、List.Exists、List.Find、Dic
    C#Linq.FirstOrDefault、Linq.Where、Linq.AsParallel、List.Exists、List.Find、Dictionar.TryGetValue、HashSet.Contains性能的比较 今天我们来比较一下集合检索方法性能更优问题,测试代码publicclassEntity{publicintId{get;set;}publicintNo{......
  • ASP.NET MVC Autofac依赖注入的一点小心得(包含特性注入)
    ASP.NETMVCAutofac依赖注入的一点小心得(包含特性注入) 前言IOC的重要性大家都清楚..便利也都知道..新的ASP.NETCore也大量使用了这种手法..一直憋着没写ASP.NETCore的文章..还是怕误导大家..今天这篇也不是讲Core的 前面写了C#开发移动应用系列 就第一篇和最后......
  • DataSphere Studio & Linkis 单机部署
    一、环境要求参考文档:DataSphereStudio&Linkis单机一键部署文档相关软件包:百度网盘1.系统要求CentOS为6或者72.安装依赖命令命令依赖:telnet;tar;sed;dos2unix;mysql;yum;java;unzip;zip;expectyumprovides*/telnet#如果命令不存在通过此命令查看命令所属安装包......