在程序中使用身份
待办事项列表依然由所有用户共享,因为 待办事项条目 并未关联到特定的用户。现在,[Authorize]
属性确保了见到 待办事项视图 的人一定登录过,在查询数据库的时候,你就可以按照登录者的身份进行过滤了。
首先,在 TodoController
中注入一个 UserManager<ApplicationUser>
:
Controllers/TodoController.cs
[Authorize]
public class TodoController : Controller
{
private readonly ITodoItemService _todoItemService;
private readonly UserManager<ApplicationUser> _userManager;
public TodoController(ITodoItemService todoItemService,
UserManager<ApplicationUser> userManager)
{
_todoItemService = todoItemService;
_userManager = userManager;
}
// ...
}
还要在文件顶部加一个新的 using
语句:
using Microsoft.AspNetCore.Identity;
UserManager
包含在 ASP.NET Core Identity 里。你可以用它在 Index
action 里查找当前用户:
public async Task<IActionResult> Index()
{
var currentUser = await _userManager.GetUserAsync(User);
if (currentUser == null) return Challenge();
var items = await _todoItemService
.GetIncompleteItemsAsync(currentUser);
var model = new TodoViewModel()
{
Items = items
};
return View(model);
}
这个 action 方法的顶部添加了新代码,这行代码用 UserManager
从 User
属性中获取当前登录的用户——该属性在当前的 action 中有效:
var currentUser = await _userManager.GetUserAsync(User);
如果当前用户已经登录, User
属性就持有一个轻量级的对象,包括了用户的一些(并非全部)信息。UserManager
使用它,通过 GetUserAsync()
方法在数据库里查找该用户的详细信息。
因为控制器使用了 [Authorize]
属性,currentUser
的值绝不应该是 null。无论如何,做个明智的检查都没错,以防万一嘛。如果用户信息没找到,你可以用 Challenge()
方法强制用户再次登录:
if (currentUser == null) return Challenge();
既然你现在把一个 ApplicationUser
参数传给了 GetIncompleteItemsAsync()
,就该修改 ITodoItemService
接口了:
Services/ITodoItemService.cs
public interface ITodoItemService
{
Task<TodoItem[]> GetIncompleteItemsAsync(
ApplicationUser user);
// ...
}
而既然你修改了 ITodoItemService
接口,也就同样需要修改 TodoItemService
中 GetIncompleteItemsAsync()
方法的签名:
Services/TodoItemService
public async Task<TodoItem[]> GetIncompleteItemsAsync(
ApplicationUser user)
下一步是修改数据库查询,并添加一层过滤,仅显示当前用户创建的条目。但在做这些之前,你需要在数据库里添加一个新的字段。
修改数据库
你需要在 TodoItem
实体上添加一个新的属性,让每个条目都能够“记住”拥有它的用户:
Models/TodoItem.cs
public string UserId { get; set; }
既然你修改了数据库上下文里的实体模型,就应该同步修改数据库。在终端窗口里用 dotnet ef
指令创建一个新的变更:
dotnet ef migrations add AddItemUserId
这个命令新建了一个名为 AddItemUserId
的变更,它将给 Items
表新添一个列,以反映你在 TodoItem
实体模型上所做的修改:
再通过 dotnet ef
指令应用到数据库:
dotnet ef database update
修改服务类
修改了数据库和数据库上下文,你就可以修改 TodoItemService
里的 GetIncompleteItemsAsync()
方法和其中的 Where
查询子句了:
Services/TodoItemService.cs
public async Task<TodoItem[]> GetIncompleteItemsAsync(
ApplicationUser user)
{
return await _context.Items
.Where(x => x.IsDone == false && x.UserId == user.Id)
.ToArrayAsync();
}
如果你现在运行程序并注册或者登录,你将又一次见到一个空的 待办事项列表。糟糕的是,你尝试添加的任何条目也都会凭空消失,因为你还没修改 添加条目 的操作,并把用户信息存储到条目里:
修改 添加条目 和 完成事项 操作
你需要在 AddItem
和 MarkDone
这两个 action 的方法里,使用 UserManager
以获取当前用户,如同在 Index
里那样。
下面是 TodoController
控制器里对这两个方法的修改:
Controllers/TodoController.cs
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddItem(TodoItem newItem)
{
if (!ModelState.IsValid)
{
return RedirectToAction("Index");
}
var currentUser = await _userManager.GetUserAsync(User);
if (currentUser == null) return Challenge();
var successful = await _todoItemService
.AddItemAsync(newItem, currentUser);
if (!successful)
{
return BadRequest("Could not add item.");
}
return RedirectToAction("Index");
}
[ValidateAntiForgeryToken]
public async Task<IActionResult> MarkDone(Guid id)
{
if (id == Guid.Empty)
{
return RedirectToAction("Index");
}
var currentUser = await _userManager.GetUserAsync(User);
if (currentUser == null) return Challenge();
var successful = await _todoItemService
.MarkDoneAsync(id, currentUser);
if (!successful)
{
return BadRequest("Could not mark item as done.");
}
return RedirectToAction("Index");
}
这两个服务方法现在也必须接受 ApplicationUser
参数了,修改 ITodoItemService
里定义的接口:
Task<bool> AddItemAsync(NewTodoItem newItem, ApplicationUser user);
Task<bool> MarkDoneAsync(Guid id, ApplicationUser user);
最后,修改 TodoItemService
里面的实现方法。在 AddItemAsync
方法里,构造一个 new TodoItem
的时候,设置 UserId
属性:
public async Task<bool> AddItemAsync(
TodoItem newItem, ApplicationUser user)
{
newItem.Id = Guid.NewGuid();
newItem.IsDone = false;
newItem.DueAt = DateTimeOffset.Now.AddDays(3);
newItem.UserId = user.Id;
// ...
}
MarkDoneAsync
方法里的 Where
查询子句也需要检查用户的 ID,以防止恶意的用户通过猜测 ID 的方法把其他用户的事项标记为完成状态。
public async Task<bool> MarkDoneAsync(
Guid id, ApplicationUser user)
{
var item = await _context.Items
.Where(x => x.Id == id && x.UserId == user.Id)
.SingleOrDefaultAsync();
// ...
}
搞定!请用两个不同的账号尝试一下。待办事项条目现在是每个账户的私密信息了。
Using identity in the application
The to-do list items themselves are still shared between all users, because the stored to-do entities aren't tied to a particular user. Now that the [Authorize]
attribute ensures that you must be logged in to see the to-do view, you can filter the database query based on who is logged in.
First, inject a UserManager<ApplicationUser>
into the TodoController
:
Controllers/TodoController.cs
[Authorize]
public class TodoController : Controller
{
private readonly ITodoItemService _todoItemService;
private readonly UserManager<ApplicationUser> _userManager;
public TodoController(ITodoItemService todoItemService,
UserManager<ApplicationUser> userManager)
{
_todoItemService = todoItemService;
_userManager = userManager;
}
// ...
}
You'll need to add a new using
statement at the top:
using Microsoft.AspNetCore.Identity;
The UserManager
class is part of ASP.NET Core Identity. You can use it to get the current user in the Index
action:
public async Task<IActionResult> Index()
{
var currentUser = await _userManager.GetUserAsync(User);
if (currentUser == null) return Challenge();
var items = await _todoItemService
.GetIncompleteItemsAsync(currentUser);
var model = new TodoViewModel()
{
Items = items
};
return View(model);
}
The new code at the top of the action method uses the UserManager
to look up the current user from the User
property available in the action:
var currentUser = await _userManager.GetUserAsync(User);
If there is a logged-in user, the User
property contains a lightweight object with some (but not all) of the user's information. The UserManager
uses this to look up the full user details in the database via the GetUserAsync()
method.
The value of currentUser
should never be null, because the [Authorize]
attribute is present on the controller. However, it's a good idea to do a sanity check, just in case. You can use the Challenge()
method to force the user to log in again if their information is missing:
if (currentUser == null) return Challenge();
Since you're now passing an ApplicationUser
parameter to GetIncompleteItemsAsync()
, you'll need to update the ITodoItemService
interface:
Services/ITodoItemService.cs
public interface ITodoItemService
{
Task<TodoItem[]> GetIncompleteItemsAsync(
ApplicationUser user);
// ...
}
Since you changed the ITodoItemService
interface, you also need to update the signature of the GetIncompleteItemsAsync()
method in the TodoItemService
:
Services/TodoItemService
public async Task<TodoItem[]> GetIncompleteItemsAsync(
ApplicationUser user)
The next step is to update the database query and add a filter to show only the items created by the current user. Before you can do that, you need to add a new property to the database.
Update the database
You'll need to add a new property to the TodoItem
entity model so each item can "remember" the user that owns it:
Models/TodoItem.cs
public string UserId { get; set; }
Since you updated the entity model used by the database context, you also need to migrate the database. Create a new migration using dotnet ef
in the terminal:
dotnet ef migrations add AddItemUserId
This creates a new migration called AddItemUserId
which will add a new column to the Items
table, mirroring the change you made to the TodoItem
model.
Use dotnet ef
again to apply it to the database:
dotnet ef database update
Update the service class
With the database and the database context updated, you can now update the GetIncompleteItemsAsync()
method in the TodoItemService
and add another clause to the Where
statement:
Services/TodoItemService.cs
public async Task<TodoItem[]> GetIncompleteItemsAsync(
ApplicationUser user)
{
return await _context.Items
.Where(x => x.IsDone == false && x.UserId == user.Id)
.ToArrayAsync();
}
If you run the application and register or log in, you'll see an empty to-do list once again. Unfortunately, any items you try to add disappear into the ether, because you haven't updated the AddItem
action to be user-aware yet.
Update the AddItem and MarkDone actions
You'll need to use the UserManager
to get the current user in the AddItem
and MarkDone
action methods, just like you did in Index
.
Here are both updated methods:
Controllers/TodoController.cs
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddItem(TodoItem newItem)
{
if (!ModelState.IsValid)
{
return RedirectToAction("Index");
}
var currentUser = await _userManager.GetUserAsync(User);
if (currentUser == null) return Challenge();
var successful = await _todoItemService
.AddItemAsync(newItem, currentUser);
if (!successful)
{
return BadRequest("Could not add item.");
}
return RedirectToAction("Index");
}
[ValidateAntiForgeryToken]
public async Task<IActionResult> MarkDone(Guid id)
{
if (id == Guid.Empty)
{
return RedirectToAction("Index");
}
var currentUser = await _userManager.GetUserAsync(User);
if (currentUser == null) return Challenge();
var successful = await _todoItemService
.MarkDoneAsync(id, currentUser);
if (!successful)
{
return BadRequest("Could not mark item as done.");
}
return RedirectToAction("Index");
}
Both service methods must now accept an ApplicationUser
parameter. Update the interface definition in ITodoItemService
:
Task<bool> AddItemAsync(TodoItem newItem, ApplicationUser user);
Task<bool> MarkDoneAsync(Guid id, ApplicationUser user);
And finally, update the service method implementations in the TodoItemService
. In AddItemAsync
method, set the UserId
property when you construct a new TodoItem
:
public async Task<bool> AddItemAsync(
TodoItem newItem, ApplicationUser user)
{
newItem.Id = Guid.NewGuid();
newItem.IsDone = false;
newItem.DueAt = DateTimeOffset.Now.AddDays(3);
newItem.UserId = user.Id;
// ...
}
The Where
clause in the MarkDoneAsync
method also needs to check for the user's ID, so a rogue user can't complete someone else's items by guessing their IDs:
public async Task<bool> MarkDoneAsync(
Guid id, ApplicationUser user)
{
var item = await _context.Items
.Where(x => x.Id == id && x.UserId == user.Id)
.SingleOrDefaultAsync();
// ...
}
All done! Try using the application with two different user accounts. The to-do items stay private for each account.