Security Context의 정보와 DB의 정보가 다를 때?
Spring Security를 세션방식과 함께 이용한다면 로그인 시에 Security Context에 AuthenticationToken(UserDetails, Password, Granted)를 저장하고 세션이 만료되거나 로그아웃 될 때까지 전역적으로 사용할 수 있다. 하지만 로그인 중에 유저 정보가 변해도 Security Context가 들고있는 정보는 업데이트가 되지 않기 때문에 동기화를 시켜줘야한다.
코드 예시
유저 Entity와 Entity를 필드로 갖는 UserDetails를 정의한다.
@Entity
@Getter
@EqualsAndHashCode(of = "id")
@NoArgsConstructor
public class Account {
@Id
@GeneratedValue
private java.lang.Long id;
@Column(unique = true)
private String email;
@Column(unique = true)
private String nickname;
private String password;
}
@Getter
public class UserAccount extends User { // UserDetails
private Account account;
public UserAccount(Account account) {
super(account.getNickname(), account.getPassword(),
List.of(new SimpleGrantedAuthority("ROLE_USER")));
this.account = account;
}
}
커스텀 어노테이션 @UserAccount는 Security Context의 Authentication.Principal인 UserAccount의 필드인 Account를 넘겨준다. 여기서 Account Entity는 DB에 한번 저장되어 식별자가 있고 시큐리티 컨텍스트가 관리하지만 영속성 컨텍스트는 관리하지 않는 Detached Entity이다.
동기화 필요 없게 구현
@PostMapping(SETTINGS_PROFILE_URL)
public String updateProfile(@CurrentAccount Account account, @Valid @ModelAttribute Profile profile,
Errors errors, Model model, RedirectAttributes attributes) {
if (errors.hasErrors()) {
model.addAttribute(account);
return SETTINGS_PROFILE_VIEW_NAME;
}
accountService.updateProfile(account, profile);
attributes.addFlashAttribute("message", "프로필 수정했습니다.");
return "redirect:" + SETTINGS_PROFILE_URL;
}
public void updateProfile(Acount account, Profile profile) {
acount.updateProfile(profile);
accountRepository.save(account); // JPA MERGE
}
컨트롤러에서 서비스로 Detached Entity 자체를 넘겨주고 서비스에서는 수정하고 JPARepository를 통해 save 시에 식별자가 이미 있으므로 모든 값을 대체하는 Merge가 일어난다.
동기화 필요
// Controller
@PostMapping(SETTINGS_PROFILE_URL)
public String updateProfile(@CurrentAccount Account account, @Valid @ModelAttribute Profile profile,
Errors errors, Model model, RedirectAttributes attributes) {
if (errors.hasErrors()) {
model.addAttribute(account);
return SETTINGS_PROFILE_VIEW_NAME;
}
accountService.updateProfile(account.getId(), profile);
attributes.addFlashAttribute("message", "프로필 수정했습니다.");
return "redirect:" + SETTINGS_PROFILE_URL;
}
// Service
@Transactioanl
public void updateProfile(Long accountId, Profile profile) {
Account findAccount = accountRepository.findById(accountId)
.orElseThrow(EntityNotFoundException::new);
findAccount.updateProfile(profile);
syncAuthenticationAccount(findAccount);
}
private void syncAuthenticationAccount(Account account){
Authentication token = new UsernamePasswordAuthenticationToken(
new UserAccount(account),
account.getPassword(),
List.of(new SimpleGrantedAuthority("ROLE_USER")));
SecurityContextHolder.getContext().setAuthentication(token);
}
영속성 컨텍스트가 관리하는 객체는 트랜잭션이 커밋될 때 JPA가 Dirty Checking을 통해 수정된 내용만 업데이트를 하기위해 다음과 같은 코드를 구현할 수 있다. 하지만 컨트롤러에서 시큐리티 컨텍스트가 관리하는 객체를 넘긴게 아니라 엔티티 식별자(id)만 넘기고 조회를 통해 영속성 컨텍스트에 포함시키기 때문에 해당 과정은 Security Context의 업데이트를 위해 ContextHolder를 통해 Context에 업데이트 된 UserDetails를 set하는 과정을 거친다.
정리
Merge를 위해 Authentication에 Entity 객체를 전부 관리 필요
VSDirty Chekcing을 통해 업데이트 부분만 수정하지만 Security Context 관리가 필요