IdentityServer4: 集成 AspNetCore Identity 框架
目录
IdentityServer4 集成 AspNetCore Identity 框架
本节基于 IdentityServer4: 配置项持久化 一节的代码基础上进行。
新增依赖包
在 IdentityServer4 项目:Dotnet.WebApi.Ids4.AuthService 中新增集成 AspNetCore Identity 所需的依赖包:
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="4.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.14" />
最终需要的依赖包为:
<PackageReference Include="IdentityServer4" Version="4.1.2" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="4.1.2" />
<PackageReference Include="IdentityServer4.EntityFramework" Version="4.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.3">
集成 AspNetIdentity 代码
//获取数据库连接字符串,从appsettings.json中读取。
var connetionString = builder.Configuration.GetConnectionString("Ids4Connection");
//注册AppDbContext服务。
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(connetionString);
});
//注册Asp.Net Core Identity服务。
builder.Services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
var ids4Builder = builder.Services.AddIdentityServer().
.AddAspNetIdentity<IdentityUser>(); //使用持久化用户
迁移 AspNetIdentity 数据库
控制台上输入如下命令生成迁移代码:
Add-Migration InitIdentity -Context AppDbContext -o Data\Migrations\Ids4\AspNetIdentity
再使用命令执行生成的迁移代码,在 SQL Server 数据库中创建表结构:
Update-Database -Context AppDbContext
迁移成功后,数据库表会新增几张以 AspNet 开头的表,如下图所示:
注意:ASP.NET Core Identity 框架中的表很多,从上图中,可以看到只是将与用户相关的表创建出来了。
生成用户信息
编写一个类,用于初始化用户信息,在项目的根目录添加 SyncUsers.cs 类,然后编写如下代码:
public class SyncUsers
{
public static async void InitDbUser(IApplicationBuilder app)
{
using (var scope = app.ApplicationServices.CreateScope())
{
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
context.Database.Migrate();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<IdentityUser>>();
var username = "zhangsan";
var userToAdd = await userManager.FindByNameAsync(username);
if (userToAdd == null)
{
IdentityUser user = new IdentityUser()
{
Id = Guid.NewGuid().ToString(),
UserName = username,
PhoneNumber = "12345678911"
};
var result = userManager.CreateAsync(user, "1q2w3E*").Result;
if(result.Succeeded)
{
var claims = new List<Claim>
{
new Claim(JwtClaimTypes.Name, "Zhangsan"),
new Claim(JwtClaimTypes.GivenName, "Zs"),
new Claim(JwtClaimTypes.FamilyName, "Zhang"),
new Claim(JwtClaimTypes.Email, "zhangsan@dotnet.com"),
new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean)
};
result = await userManager.AddClaimsAsync(user, claims);
}
if (result.Succeeded)
{
Console.WriteLine("初始化用户成功。");
}else
{
Console.WriteLine("初始化用户失败!!!");
}
}
}
}
}
将 SyncUsers.InitDbUser()方法放在 Program 类中去执行,实现用户信息的添加:
var app = builder.Build();
SyncUsers.InitDbUser(app);
修改 IdentityServer.QuickstartUI 代码
登录
找到 Quickstart/Account/AccountController.cs,添加如下代码:
public class AccountController : Controller
{
......
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
public AccountController(
......
UserManager<IdentityUser> userManager,
SignInManager<IdentityUser> signInManager,
)
{
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model, string button)
{
// check if we are in the context of an authorization request
var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);
// the user clicked the "cancel" button
if (button != "login")
{
if (context != null)
{
// if the user cancels, send a result back into IdentityServer as if they
// denied the consent (even if this client does not require consent).
// this will send back an access denied OIDC error response to the client.
await _interaction.DenyAuthorizationAsync(context, AuthorizationError.AccessDenied);
// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage("Redirect", model.ReturnUrl);
}
return Redirect(model.ReturnUrl);
}
else
{
// since we don't have a valid context, then we just go back to the home page
return Redirect("~/");
}
}
if (ModelState.IsValid)
{
//获取用户对象
var user = await _userManager.FindByNameAsync(model.Username);
if (user != null)
{
//登录
var r = await _signInManager.PasswordSignInAsync(model.Username, model.Password, model.RememberLogin, lockoutOnFailure: true);
if (r.Succeeded)
{
await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id.ToString(), user.UserName));
if (_interaction.IsValidReturnUrl(model.ReturnUrl) || Url.IsLocalUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}
return Redirect("~/");
}
}
await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId: context?.Client.ClientId));
ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);
}
// something went wrong, show form with error
var vm = await BuildLoginViewModelAsync(model);
return View(vm);
}
退出
找到 Quickstart/Account/AccountController.cs,找到Logout()
方法, 替换如下代码
public async Task<IActionResult> Logout(LogoutInputModel model)
{
......
- await HttpContext.SignOutAsync();
+ await _signInManager.SignOutAsync();
......
}
使用 IdentityUser 用户登录
使用初始化用户 zhangsan
,密码:1q2w3e* 登录,得到 AccessToken:
eyJhbGciOiJSUzI1NiIsImtpZCI6IjQzMjE3QzNGNzY1MEVGRUIyNTU4RTNERjc2REY4QzJDRTVEQ0ZDNkFSUzI1NiIsInR5cCI6ImF0K2p3dCIsIng1dCI6IlF5RjhQM1pRNy1zbFdPUGZkdC1NTE9YY19HbyJ9.eyJuYmYiOjE2NzgzMzQ5OTksImV4cCI6MTY3ODMzODU5OSwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NjAwMSIsImNsaWVudF9pZCI6Ik12Y0FwcCIsInN1YiI6ImNlYzYyMWJlLWJkOWYtNDdjNi05NjQ2LWQxOGE5ZjQ5ZmJlMCIsImF1dGhfdGltZSI6MTY3ODMzNDk5NCwiaWRwIjoibG9jYWwiLCJqdGkiOiJBNDJBRkQ4QUY1QzBDMTNEMzEzQUI0QTdDMDJGQzdFOCIsInNpZCI6Ijc2MTIxOUNERDIwMkJDNDI5NzZDMEZCNDEzRUNENTBEIiwiaWF0IjoxNjc4MzM0OTk5LCJzY29wZSI6WyJvcGVuaWQiLCJwcm9maWxlIiwiT0FBUEkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsicHdkIl19.prQrI-B_QrZy6js3bg32HzFPCZZy2u8WWjqRLCZw24QmzeksCaY4-cPGo_zeCl4TNKkSFoLMUIRGO4OkMd2agDhQm_xyzw8RajrE8WA0nBHDZSsHXtwXPQDSpy8i2kVxEZQ31U_PjEulBIhzyrluQwMjXFsziRBUrTS6ZFMReduExwlMZZHu_bzVAyQOFu2oARjfGO3-6IYfHbsVXrbNVjCw47FQazgQMJU190OL3SoHhY9BsKnAHZ2RgQ-RnfFOtcfOxMvZz9dgXjLsSwi-bzmfE54PsCWcwne2Fau9WsWQNVOz3u1TVeToW5cF0ytN1k_qsx1TQAvRVzmTvWPaOA
打开 https://jwt.io, 可以解析到 AccessToken 的 PAYLOAD:DATA 信息:
{
"nbf": 1678334999,
"exp": 1678338599,
"iss": "https://localhost:6001",
"client_id": "MvcApp",
"sub": "cec621be-bd9f-47c6-9646-d18a9f49fbe0",
"auth_time": 1678334994,
"idp": "local",
"jti": "A42AFD8AF5C0C13D313AB4A7C02FC7E8",
"sid": "761219CDD202BC42976C0FB413ECD50D",
"iat": 1678334999,
"scope": [
"openid",
"profile",
"OAAPI",
"offline_access"
],
"amr": [
"pwd"
]
}