-
[스프링 인 액션] Chapter 4 - 스프링 시큐리티 :: 사용자 인지하기개발서적읽기/Spring in Action 제 5판 2020. 7. 30. 15:55
사용자가 로그인되었음을 아는 정도로는 충분하지 않다.
사용자 경험에 맞추려면 그들이 누구인지 알아야 한다.
예를 들어, OrderController에서 주문 폼과 바인딩되는 Order 객체를 최초 생성할 때
해당 주문을 하는 사용자의 이름과 주소를 주문 폼에 미리 넣을 수 있다면 좋을 것이다.
그러면 사용자가 매번 주문을 할 때마다 다시 입력할 필요가 없기 때문이다.
또한 이보다 더 중요한 것으로, 사용자 주문 데이터를 DB에 저장할 때 주문이 생성되는
User와 Order를 연관시킬 수 있어야 한다.
DB에서 Order 개체와 User 개체를 연관시키기 위해서는
Order 클래스에 새로운 속성 User를 추가해야 한다.
package tacos;
/*
* @USER JungHyun
* @DATE 2020-07-04
* @DESCRIPTION
*/
import lombok.Data;
import org.hibernate.validator.constraints.CreditCardNumber;
import javax.persistence.*;
import javax.validation.constraints.Digits;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Data
@Entity
@Table(name = "Taco_Order")
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private Date placedAt;
@ManyToOne
private User user;
@NotBlank(message = "Name is required")
private String deliveryName;
@NotBlank(message = "Street is required")
private String deliveryStreet;
@NotBlank(message = "City is required")
private String deliveryCity;
@NotBlank(message = "State is required")
private String deliveryState;
@NotBlank(message = "Zip code is required")
private String deliveryZip;
@CreditCardNumber(message = "Not a valid credit card number")
private String ccNumber;
@Pattern(regexp = "^(0[1-9]|1[0-2])([\\/])([1-9][0-9])$", message = "Must be formatted MM/YY")
private String ccExpiration;
@Digits(integer = 3, fraction = 0, message = "Invalid CVV")
private String ccCVV;
@ManyToMany(targetEntity = Taco.class)
private List<Taco> tacos = new ArrayList<>();
public void addDesign(Taco design) {
this.tacos.add(design);
}
@PrePersist
void placedAt() {
this.placedAt = new Date();
}
}주문을 처리하는 OrderController에서는 processOrder()가 주문을 저장하는 일을 수행한다.
따라서 인증된 사용자가 누구인지 결정한 후, Order 객체의 setUser()를 호출하여
해당 주문을 사용자와 연결하도록 processOrder()를 수정해야 한다.
사용자가 누구인지 결정하는 방법은 여러 가지가 있으며
그중 가장 많이 사용되는 방법은 다음과 같다.
- Principal 객체를 컨트롤러 메서드에 주입한다.
- Authentication 객체를 컨트롤러 메서드에 주입한다.
- SecurityContextHolder를 사용해서 보안 컨텍스트를 얻는다.
- @AuthenticationPrincipal 애노테이션을 메서드에 지정한다.
예를 들어, processOrder() 에서 java.security.Principal 객체를 인자로 받도록 수정할 수 있다.
그다음에 이 객체의 name 속성을 사용해서 UserRepository의 사용자를 찾을 수 있다.
@PostMapping
public String processOrder(@Valid Order order, Errors errors, SessionStatus sessionStatus, Principal principal) {
if (errors.hasErrors()) {
return "orderForm";
}
User user = userRepository.findByUsername(principal.getName());
order.setUser(user);
orderRepo.save(order);
sessionStatus.setComplete();
return "redirect:/";
}하지만 이 코드는 보안과 관련 없는 코드가 혼재한다.
Principal 대신 Authentication 객체를 인자로 받도록 processOrder()를 변경할 수도 있다.
@PostMapping
public String processOrder(@Valid Order order, Errors errors, SessionStatus sessionStatus, Authentication authentication) {
if (errors.hasErrors()) {
return "orderForm";
}
User user = (User) authentication.getPrincipal();
order.setUser(user);
orderRepo.save(order);
sessionStatus.setComplete();
return "redirect:/";
}이 코드에서는 Authentication 객체를 얻은 다음에 getPrincipal()을 호출하여
Principal 객체를 얻는다.
하지만 아래와 같이 processOrder()의 인자로 User 객체를 전달하는 것이 가장
명쾌한 해결 방법일 것이다. 여러 방법 중 이 방법을 선택하자.
@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과 동일하게 보안 특정 코드만 갖는다.
(보안 특정 코드가 많아서 조금 어렵게 보이지만)
인증된 사용자가 누구인지 식별하는 방법이 하나 더 있다. 참고만 하자.
즉, 보안 컨텍스트로부터 Authentication 객체를 얻은 후
다음과 같이 Principal 객체를 요청하면 된다.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User) authentication.getPrincipal();이 코드에는 보안 특정 코드가 많다.
그러나 지금까지 얘기한 다른 방법에 비해서 한 가지 장점이 있다.
이 방법은 컨트롤러의 처리 메서드는 물론이고, 애플리케이션의 어디서든 사용할 수 있다.
사용자와 주문을 연관시키는 것에 추가하여
현재 주문을 하는 인증된 사용자의 이름과 주소를 주문 폼에 미리 채워서 보여줄 수 있다면
더욱 좋을 것이다.
그러면 사용자가 매번 주문을 할 때마다 이름과 주소를 다시 입력할 필요가 없기 때문이다.
OrderController의 orderForm()을 수정하자.
@GetMapping("/current")
public String orderForm(@AuthenticationPrincipal User user, @ModelAttribute Order order) {
if (order.getDeliveryName() == null) {
order.setDeliveryName(user.getFullname());
}
if (order.getDeliveryStreet() == null) {
order.setDeliveryStreet(user.getStreet());
}
if (order.getDeliveryCity() == null) {
order.setDeliveryCity(user.getState());
}
if (order.getDeliveryZip() == null) {
order.setDeliveryZip(user.getZip());
}
return "orderForm";
}주문 외에도 인증된 사용자 정보를 활용할 곳이 하나 더 있다.
즉, 사용자가 원하는 식자재를 선택하여 타코를 생성하는 디자인 폼에는
현재 사용자의 이름을 보여줄 것이다.
이때 UserRepository의 findByUsername()를 사용해서 현재 디자인 폼으로 작업중인 인증된
사용자를 찾아야 한다.
DesignTacoController의 생성자와 showDesignForm()을 변경하자.
package tacos.web;
/*
* @USER JungHyun
* @DATE 2020-07-04
* @DESCRIPTION
*/
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.*;
import tacos.Ingredient;
import tacos.Order;
import tacos.Taco;
import tacos.User;
import tacos.data.IngredientRepository;
import tacos.data.TacoRepository;
import tacos.data.UserRepository;
import javax.validation.Valid;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Controller
@RequestMapping("/design")
@SessionAttributes("order")
public class DesignTacoController {
private final IngredientRepository ingredientRepo;
private TacoRepository tacoRepo;
private UserRepository userRepo;
@Autowired
public DesignTacoController(IngredientRepository ingredientRepo, TacoRepository tacoRepo, UserRepository userRepo) {
this.ingredientRepo = ingredientRepo;
this.tacoRepo = tacoRepo;
this.userRepo = userRepo;
}
@ModelAttribute(name = "order")
public Order order() {
return new Order();
}
@ModelAttribute(name = "taco")
public Taco taco() {
return new Taco();
}
@GetMapping
public String showDesignForm(Model model, Principal principal) {
List<Ingredient> ingredients = new ArrayList<>();
ingredientRepo.findAll().forEach(i -> ingredients.add(i));
Ingredient.Type[] types = Ingredient.Type.values();
for (Ingredient.Type type : types) {
model.addAttribute(type.toString().toLowerCase(), filterByType(ingredients, type));
}
String username = principal.getName();
User user = userRepo.findByUsername(username);
model.addAttribute("user", user);
return "design";
}
@PostMapping
public String processDesign(@Valid Taco design, Errors errors, @ModelAttribute Order order) {
if (errors.hasErrors()) {
return "design";
}
Taco saved = tacoRepo.save(design);
order.addDesign(saved);
return "redirect:/orders/current";
}
private Object filterByType(List<Ingredient> ingredients, Ingredient.Type type) {
return ingredients.stream().filter(x -> x.getType().equals(type))
.collect(Collectors.toList());
}
}'개발서적읽기 > Spring in Action 제 5판' 카테고리의 다른 글
[스프링 인 액션] Chapter 5 - 구성 속성 사용하기 :: 자동-구성 세부 조정하기 (0) 2020.07.31 [스프링 인 액션] Chapter 4 - 스프링 시큐리티 :: 각 폼에 로그아웃 버튼 추가하고 사용자 정보 보여주기 (0) 2020.07.30 [스프링 인 액션] Chapter 4 - 스프링 시큐리티 :: 웹 요청 보안 처리하기 (0) 2020.07.30 [스프링 인 액션] Chapter 4 - 스프링 시큐리티 :: 스프링 시큐리티 구성하기 (0) 2020.07.30 [스프링 인 액션] Chapter 4 - 스프링 시큐리티 :: 스프링 시큐리티 활성화하기 (0) 2020.07.30 댓글