4.4 了解用户是谁

4.4 了解用户是谁

通常,仅仅知道用户已登录是不够的,我们一般还需要知道他们是谁,这样才能优化体验。

例如,在OrderController中,在最初创建Order的时候会绑定一个订单的表单,如果我们能够预先将用户的姓名和地址填充到Order中就好了,这样用户就不需要为每个订单都重新输入这些信息了。也许更重要的是,在保存订单的时候应该将Order实体与创建该订单的用户关联起来。

为了在Order实体和User实体之间实现所需的关联,我们需要为Order类添加一个新的属性:

1
2
3
4
5
6
7
8
9
@Data
@Entity
@Table(name="Taco_Order")
public class Order implements Serializable {
...
@ManyToOne
private User user;
...
}

这个属性上的@ManyToOne注解表明一个订单只能属于一个用户,但是,一个用户却可以有多个订单。因为我们使用了Lombok,所以不需要为该属性显式定义访问器方法。

在OrderController中,processOrder()方法负责保存订单。这个方法需要修改以便于确定当前的认证用户是谁,并且要调用Order对象的setUser()方法来建立订单和用户之间的关联。

我们有多种方式确定用户是谁,常用的方式如下:

  • 注入Principal对象到控制器方法中;
  • 注入Authentication对象到控制器方法中;
  • 使用SecurityContextHolder来获取安全上下文;
  • 使用@AuthenticationPrincipal注解来标注方法。

举例来说,我们可以修改processOrder()方法,让它接受一个java.security.Principal类型的参数。然后,我们就可以使用Principal的名称从UserRepository中查找用户了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

@Data
@Entity
@Table(name="Taco_Order")
public class Order implements Serializable {
...
@ManyToOne
private User user;
...
}
@PostMapping
public String processOrder(@Valid Order order, Errors errors,
SessionStatus sessionStatus,
Principal principal) {
...
User user = userRepository.findByUsername(
principal.getName());
order.setUser(user);
...
}

这种方式能够正常运行,但是它会在与安全无关的功能中掺杂安全性的代码。我们可以修改processOrder()方法,让它不再接受Principal参数,而是接受Authentication对象作为参数:

1
2
3
4
5
6
7
8
9
@PostMapping
public String processOrder(@Valid Order 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注解,这样它才会变成认证的principal:

1
2
3
4
5
6
7
8
9
10
11
12
@PostMapping
public String processOrder(@Valid Order 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对象之后,我们就可以使用它并赋值给Order了。

还有另外一种方式能够识别当前认证用户是谁,但是这种方式有点麻烦,它会包含大量安全性相关的代码。我们可以从安全上下文中获取一个Authentication对象,然后像下面这样获取它的principal:

1
2
3
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
User user = (User) authentication.getPrincipal();

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