了解用户是谁

通常,仅仅知道用户是否已经登录和他们拥有的权限还是不够的,我们还需要知道他们是谁,从而优化他们的体验。

例如,在 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();

这个片段尽管充满了与安全有关的代码,但是与前文所述的其他方法相比有一个优势:它可以用于应用程序的任何地方,而不仅仅是在控制器的处理器方法中。这使得它非常适合在较低级别的代码中使用。

小结

  • Spring Security 的自动配置是实现基本安全功能的好办法,但是大多数的应用都需要显式的安全配置以满足特定的安全需求。

  • 用户详情可以通过用户存储进行管理,它的后端可以是关系型数据库、LDAP 或完全自定义实现。

  • Spring Security 会自动防范 CSRF 攻击。

  • 可以通过 SecurityContext 对象(该对象可由 SecurityContextHolder.getContext() 返回)来获取已认证用户的信息,也可以借助 @AuthenticationPrincipal 注解将其注入控制器。