了解用户是谁
通常,仅仅知道用户是否已经登录和他们拥有的权限还是不够的,我们还需要知道他们是谁,从而优化他们的体验。
例如,在 OrderController 中,在最初创建绑定一个订单表单的 TacoOrder 时,如果能够预先将用户的姓名和地址填充到 TacoOrder 中就好了,这样一来,用户就不需要在每个订单中重复输入这些信息了。也许更重要的是,保存订单时,应该将 TacoOrder 实体与创建该订单的用户关联起来。
为了在 TacoOrder 实体和 User 实体之间实现所需的关联,需要为 TacoOrder 类添加一个新的属性:
@Data
@Entity
@Table(name = "Taco_Order")
public class TacoOrder implements Serializable {
...
@ManyToOne
private User user;
...
}
这个属性上的 @ManyToOne 注解表明一个订单只能属于一个用户。但是,对应地,一个用户可以有多个订单。(因为我们使用了 Lombok,所以不需要为该属性显式定义访问器方法。)在 OrderController 中,processOrder() 方法负责保存订单。需要修改这个方法以确定当前的认证用户是谁,并且要调用 Order 对象的 setUser() 方法来建立订单和用户之间的关联。
有多种方式确定用户是谁,常用的方式如下:
-
将 java.security.Principal 对象注入控制器方法;
-
将 org.springframework.security.core.Authentication 对象注入控制器方法;
-
使用 org.springframework.security.core.context.SecurityContextHolder 获取安全上下文;
-
注入 @AuthenticationPrincipal 注解标注的方法参数(@AuthenticationPrincipal 来自 Spring Security 的 org.springframework.security.core.annotation 包)。
举例来说,我们可以修改 processOrder() 方法,让它接受一个 java.security.Principal 类型的参数。然后,就可以使用 Principal 的名称从 UserRepository 中查找用户了:
@PostMapping
public String processOrder(@Valid TacoOrder order, Errors errors,
SessionStatus sessionStatus,
Principal principal) {
...
User user = userRepository.findByUsername(
principal.getName());
order.setUser(user);
...
}
这种方法能够正常运行,但是它在与安全无关的功能中掺杂了与安全有关的代码。我们可以修改 processOrder() 方法,让它不再接收 Principal 参数,转而接收 Authentication 对象作为参数,从而消除与安全有关的代码:
@PostMapping
public String processOrder(@Valid TacoOrder order, Errors errors,
SessionStatus sessionStatus,
Authentication authentication) {
...
User user = (User) authentication.getPrincipal();
order.setUser(user);
...
}
有了 Authentication 对象之后,我们就可以调用 getPrincipal() 来获取 principal 对象。在本例中,它是一个 User 对象。需要注意,getPrincipal() 返回的是 java.util.Object,所以我们需要将其转换成 User。
最整洁的方案可能是在 processOrder() 中直接接收一个 User 对象,不过,我们需要为其添加 @AuthenticationPrincipal 注解,使其变成 authentication 的 principal:
@PostMapping
public String processOrder(@Valid TacoOrder order, Errors errors,
SessionStatus sessionStatus,
@AuthenticationPrincipal User user) {
if (errors.hasErrors()) {
return "orderForm";
}
order.setUser(user);
orderRepo.save(order);
sessionStatus.setComplete();
return "redirect:/";
}
@AuthenticationPrincipal 的一个突出优势在于,它不需要类型转换(前文中的 Authentication 则需要进行类型转换),同时能够将与安全有关的代码局限于注解本身。processOrder() 得到 User 对象后,我们就可以使用该对象,并将其赋值给 TacoOrder 了。还有另一种方法能够识别当前认证用户,但是这种方法有点麻烦,包含大量与安全有关的代码。我们可以从安全上下文中获取一个 Authentication 对象,然后像下面这样获取它的 principal:
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
User user = (User) authentication.getPrincipal();
这个片段尽管充满了与安全有关的代码,但是与前文所述的其他方法相比有一个优势:它可以用于应用程序的任何地方,而不仅仅是在控制器的处理器方法中。这使得它非常适合在较低级别的代码中使用。