在前面的文章中,我们讲述了ASP.NET实现OAuth2.0的四种模式。那在.NET Core中怎么实现呢?如果使用前面的方法,你会发现是行不通的,.NET Core的架构已经改变了。
在讲述认证之前,我们需要先了解一个新的协议:OpenID Connect。它基于OAuth2.0协议,增加了OpenID这个概念,使用起来会更加简便。本文不对这个协议展开分析,有兴趣的读者请参阅其他资料。在ASP.NET Core中,我们使用IdentityServer4这个开源库实现用户认证和授权的功能。
一、实现用户认证的四种模式
1.1 项目准备
在前面的文章中,我们讲到认证服务过程中有四种角色,其中有两个是认证服务器和资源服务器。我们先实现认证服务器。
1、新建一个ASP.NET Core Web应用程序,后面的页面选择“空”即可。
2、添加NuGet程序包:IdentityServer4。
3、打开launchSettings.json,修改URL为localhost:5001,如下所示:
{
"profiles": {
"SelfHost": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001"
}
}
}
4、新建一个名为Config的类,用来存放一些认证的资源(实际会通过查数据库的方法去做):
public static class Config
{
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId()
};
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
};
public static IEnumerable<Client> Clients =>
new Client[]
{
};
public static List<TestUser> Users =>
new List<TestUser>()
{
};
}
其中,IdentityResources存放认证资源,包括OpenID(必须)、Profile(用户名、邮箱、手机等)。ApiScopes相当于访问API的权限,例如可不可以获取照片,可不可以写入等。Clients是允许访问的客户端的定义。Users是允许访问的用户。
5、修改Startup.cs,如下:
public class Startup
{
public IWebHostEnvironment Environment { get; }
public Startup(IWebHostEnvironment environment)
{
Environment = environment;
}
public void ConfigureServices(IServiceCollection services)
{
var builder = services.AddIdentityServer()
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients)//在客户端模式中使用
.AddTestUsers(Config.Users);//在密码模式中使用
// not recommended for production - you need to store your key material somewhere secure
builder.AddDeveloperSigningCredential();
}
public void Configure(IApplicationBuilder app)
{
app.UseIdentityServer();
}
}
1.2 客户端模式
要实现客户端模式,要做的事情很简单,只需要修改Config.cs即可。
首先,我们定义一个权限,添加到ApiScopes中:
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
new ApiScope("scope1")
};
然后,我们增加一个允许以客户端模式访问的Client,如下所示:
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client
{
ClientId = "client1",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedScopes = { "scope1" }
}
};
这个Client有什么特点呢?它的ClientID为client1,ClientSecret为secret,它允许有scope1这个权限。
好了,客户端模式这样就实现好了。
在Postman中测试:
好,得到AccessToken之后,就可以去访问API了。
1.3 密码模式
同样的,只需要修改Config.cs。首先,再增加一个权限:
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
new ApiScope("scope1"),
new ApiScope("scope2")//新增的
};
然后我们增加一个Client,用于密码模式认证的:
new Client
{
ClientId = "client2",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
AllowedScopes = { "scope2" },
RequireClientSecret=false
}
认证类型修改为密码模式ResourceOwnerPassword,同时不需要验证ClientSecret。
最后,我们增加允许访问的用户:
public static List<TestUser> Users =>
new List<TestUser>()
{
new TestUser()
{
SubjectId="1",
Username="user1",
Password="password1",
Claims=new Claim[]{ new Claim("role","admin") }
},
new TestUser()
{
SubjectId="2",
Username="user2",
Password="password2",
Claims=new Claim[]{ new Claim("role","user") }
}
};
现在我们就可以在Postman中测试了:
1.4 授权码模式
授权码模式比较复杂,其流程如下:
1、客户端向认证服务器请求授权,假设其要求的权限为scope3,重定向链接为url。
2、认证服务器向用户提供登录页面,用户完成登录和确认授权。
3、认证服务器生成授权码code,跳转至url,把code附在其后。
4、url的后台程序获取到code之后,再向认证服务器请求token。
5、认证服务器返回token。
首先,我们增加一个权限:
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
new ApiScope("scope1"),
new ApiScope("scope2"),
new ApiScope("scope3")//新增
};
然后,我们增加一个Client:
new Client
{
ClientId = "client3",
AllowedGrantTypes = GrantTypes.Code,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "scope3" },
RedirectUris={ "https://localhost:5001/callback.html" },
RequirePkce=false
}
现在,我们尝试在浏览器中请求授权码,输入以下地址:
https://localhost:5001/connect/authorize?client_id=client3&response_type=code&scope=scope3&redirect_uri=https://localhost:5001/callback.html
我们会发现,页面将跳转到Account/Login这个地址,当然,因为没有这个页面,404了。这是Identity4默认的登录页地址,我们可以修改(在Startup.cs的ConfigureServices函数里):
var builder = services.AddIdentityServer(options =>
{
options.UserInteraction.LoginUrl = "/login.html";
})
这样,登录页我们改成了login.html。接下来,我们需要在这个页面完成用户名密码输入并验证这些工作。完成之后,我们需要通知Identity4,用户验证通过了。
为了实现通知,我们加入一个Controller,加入一个Post方法:
[HttpPost]
public async Task<IActionResult> Post()
{
string returnUrl = Request.Query["returnUrl"];
AuthenticationProperties props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(TimeSpan.FromDays(30))
};
var isuser = new IdentityServerUser("1");
await HttpContext.SignInAsync(isuser, props);
return Redirect(returnUrl);
}
可以看到,这个方法获取了传进来的returnUrl这个参数,完成用户验证登记之后,就跳转到该地址了。
接下来,我们在login.html里调用这个Controller方法即可。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<script src="axios.min.js"></script>
</head>
<body>
<div id="display_url"></div>
<script type="text/javascript">
let url = window.location.href;
let pos = url.indexOf("ReturnUrl=");
let returnUrl = url.substr(pos + 10);
axios({
method: 'post',
url: '/test',
params: {
returnUrl: returnUrl
}
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
</script>
</body>
</html>
到目前为此,假设我们用浏览器再次请求授权码,将会得到以下结果:
https://localhost:5001/callback.html?code=C22C07AD90C74140F8F46415D5FA8E150F2B28E5A30023D4F915AF878D71FB37&scope=scope3
那个code就是我们要的授权码。拿到之后,我们在Postman上获取AccessToken: