一、授权码模式认证流程
授权码模式是最为安全,同时也最为复杂的一种模式。其认证流程如下图所示:
可能有的人会问,为什么要多出授权码这个东西呢,没有它不行吗?(我自己一开始也有这个疑问)确实是不行,我们来看一下。
我们拿“同城交友”APP微信登录这个例子来解释。首先,用户名、密码就是我们的微信号和密码,我们是不能交给“同城交友”这个APP的。所以,我们在微信官方的登录页上完成了微信号和密码的输入(当然,也可能是手机扫一扫)。完成之后,我们能不能直接把AccessToken交给APP去访问资源呢?还不能,因为任何APP都可以打开微信登录页,如果这时APP就拿到了AccessToken,安全性就难以保证了。这时,如果我们提出授权码这个概念,这个问题就能解决。APP在拿到授权码之后,它并不能直接访问资源。它需要用自己的AppID和AppSecret去换AccessToken。因为APP都要在微信平台上注册之后,才会有AppSecret,所以这个APP就被认定为安全的。这样一来,安全性就能够得到保障了。
二、服务器配置
首先,我们需要修改网站入口类Startup。其完整代码如下所示:
[assembly: OwinStartup(typeof(AuthorizationCodeMode.Startup))]//让整个网站的入口为Startup这个类
namespace AuthorizationCodeMode
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
//配置OAuth
ConfigureOAuth(app);
//配置网站路由等信息
HttpConfiguration config = new HttpConfiguration();
Register(config);
//允许跨域访问
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
}
private void ConfigureOAuth(IAppBuilder app)
{
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,//允许http而非https访问
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active,//激活授权码模式
TokenEndpointPath = new PathString("/token"),//访问host/token获取AccessToken
AuthorizeEndpointPath = new PathString("/auth"),//访问host/auth获取授权码
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),//AccessToken在30分钟后过期
Provider = new AuthorizationServerProvider(),//AccessToken的提供类
AuthorizationCodeProvider = new AuthorizationCodeProvider()//授权码的提供类
};
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
private static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First();
jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
}
}
}
跟密码模式相比,改动的就是服务器的配置项。添加了AuthenticationMode、AuthorizeEndpointPath和AuthorizationCodeProvider三个属性。
认证流程可分为两步,一是获取授权码,二是获取AccessToken。
三、获取授权码
在获取授权码时,我们需要请求host/auth这个地址,输入的参数有以下要求:
(1)grant_type,必须为authorization_code。
(2)response_type,必须为code。
(3)client_id,客户端ID。
(4)redirect_uri,重定向地址,如为http://abc.com/,则请求授权码完成后,将会重定向到:http://abc.com/code=[授权码]。
(5)scope,授权范围,可选。
(6)state,客户端状态,可选。
首先,我们创建一个授权码的提供类,如下所示:
public class AuthorizationCodeProvider : IAuthenticationTokenProvider
{
private Dictionary<string, string> codes = new Dictionary<string, string>();
public void Create(AuthenticationTokenCreateContext context)
{
string new_code = Guid.NewGuid().ToString("n");
context.SetToken(new_code);
codes.Add(new_code, context.SerializeTicket());
}
public Task CreateAsync(AuthenticationTokenCreateContext context)
{
Create(context);
return Task.FromResult<object>(null);
}
public void Receive(AuthenticationTokenReceiveContext context)
{
string code = context.Token;
if (codes.ContainsKey(code))
{
string value = codes[code];
codes.Remove(code);
context.DeserializeTicket(value);
}
}
public Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
Receive(context);
return Task.FromResult<object>(null);
}
}
其实现就是用一个GUID作为code,调用Create的时候放进字典中,调用Receive的时候从字典中移除。
接着,我们需要修改AuthorizationServerProvider类。在获取授权码的过程中,这个类有以下函数被调用:
(1)ValidateClientRedirectUri,验证重定向URI是否合法。
(2)ValidateAuthorizeRequest,验证请求的参数是否合法。
(3)AuthorizeEndpoint,生成Code(调用AuthorizationCodeProvider.Create),将结果添加到重定向URI中。
完整示例代码如下:
public override async Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
context.Validated(context.RedirectUri);
}
public override async Task ValidateAuthorizeRequest(OAuthValidateAuthorizeRequestContext context)
{
if (context.AuthorizeRequest.ClientId.StartsWith("AAA"))
{
context.Validated();
}
else
{
context.Rejected();
}
}
public override async Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
{
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.Write(Uri.EscapeDataString(authorizeCodeContext.Token));//为了测试方便,直接打印出code
//context.Response.Redirect(redirectUri + "?code=" + Uri.EscapeDataString(authorizeCodeContext.Token));//正常使用时是把code加在重定向网址后面
context.RequestCompleted();
}
使用Postman测试结果:
四、获取AccessToken
在获取AccessToken时,我们需要请求host/token这个地址,输入的参数有以下要求:
(1)grant_type,必须为authorization_code。
(2)code,上面所获取到的授权码。
(3)client_id,客户端ID。
(4)redirect_uri,重定向地址,必须跟上面的一致。
同样,在获取AccessToken过程中,AuthorizationServerProvider类中有几个函数会被调用:
(1)ValidateClientAuthentication,验证客户端是否合法。
(2)AuthorizationCodeProvider.Receive,调出授权码背后的信息。
(3)ValidateTokenRequest,验证请求是否合法。
完整测试代码如下:
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
context.TryGetFormCredentials(out string clientId, out string clientSecret);
if (!clientId.StartsWith("AAA"))
{
context.SetError("invalid_client", "未授权的客户端");
return;
}
context.Validated();
}
public override async Task ValidateTokenRequest(OAuthValidateTokenRequestContext context)
{
if (context.TokenRequest.IsAuthorizationCodeGrantType)
{
context.Validated();
}
else
{
context.Rejected();
}
}
使用Postman测试的结果如下图所示: