프로젝트에 로그인 기능을 구현하고 추후 권한이 필요한 리소스에 접근할 때 헤더에 입력된 토큰을 컨트롤러에서 @ReqeustHeader로 받아와서 서비스 단에 넘겨준 후 토큰에서 claims를 파싱해서 subject를 뽑아내고 DB에서 사용자를 찾아 검증하는 로직을 만들었었다.
근데 멘토님이 컨트롤러에서 Authentication을 컨트롤러에서 받아오면 서비스 단에 헤더에 입력된 토큰으로부터 파싱하는 작업이 필요 없을 것이라고 하셔서 적용해보기로 했다.
@AuthenticationPrincipal 로그인 한 정보 받아오기
로그인 사용자가 필요할 때 매번 서버에 요청을 보내 DB에 접근해서 데이터를 가져오는 것은 비효율 적이다.
한 번 인증된 사용자 정보를 세션에 담아놓고 세션이 유지되는 동안 사용자 객체를 DB로 접근하는 방법 없이 바로 사용할 수 있도록 한다.
Spring Security에서 로그인 한 User 정보를 필터에서 validation 검증을 해서 SecurityContextHolder 내부의 SecurityContext에 Authentication 객체로 저장해두고 있다. 이 Authentication 객체를 참조하는 방법은 3가지가 있다.
1. 컨트롤러에서 Principal 객체를 주입 받아 사용
- 가장 간단한 방법
Spring Security가 제공하는 SecurityContextHolder의 Principal 객체가 아닌 자바에 정의돼있는 Principal 객체를 바인딩 해주는 것이므로 사용할 수 있는 메소드가 getName() 뿐이다.
2. 컨트롤러에서 @AuthenticaionPrincipal 선언하여 엔티티 객체 받아오기
- 엔티티에 있는 모든 필드 참조 가능
3. 컨트롤러에서 @AuthenticaionPrincipal 선언하여 엔티티의 어댑터 객체 받아오기
- 가장 권장
엔티티 객체를 필드로 갖고 있는 어댑터 클래스 (DTO) 를 생성하여 회원 객체 (User) 상속
해당 어댑터 클래스의 엔티티 객체는 DB의 회원 객체의 정보를 담고 있어야 한다.
UserDetailsService의 loadByUsername()에서 어댑터 클래스를 반환하도록 수정.
스프링 시큐리티에서 세션에 존재하는 현재 사용자 정보를 아래와 같이 조회할 수 있다.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User user = (User)authentication.getPrincipal();
principal 객체는 자바 표준 객체로 받을 수 있는 정보는 name 뿐이다.
@AuthenticationPrincipal
세션 정보 UserDetails에 접근할 수 있는 어노테이션
@AuthenticaionPrincipal 은 UserDetails 타입을 가지고 있음.
-> UserDetails 타입을 구현한 PrincipalDetails 클래스를 받아 User 객체를 얻는다.
로그인 세션 정보가 필요한 컨트롤러에서 매번 @AuthenticationPrincipal로 세션 정보를 받아서 사용한다.
UserDetailsService에서 리턴하는 타입을 변경하면 Controller에서 @AuthenticaionPrincipal로 받아올 수 있는 객체가 변경된다.
이때 User 객체를 직접 반환하거나 User 객체를 감싸는 Adapter 클래스를 사용할 수 있다.
User 엔티티를 직접 사용하지 않기 위해서 Adapter 클래스를 사용해 개발하는 방법을 택했다.
-> 도메인 객체는 특정 기술에 종속되지 않도록 개발하는 것이 좋기 때문이다.
<UserAdapter>
@Getter
public class UserAdapter extends User {
private User user;
public UserAdapter(User user) {
super(user.getId(), user.getName(), user.getPhone(),
user.getEmail(), user.getPassword(), user.getAddress()
, user.getBirthDay(), user.getOAuthProvider(), user.getCreatedAt(),
user.getUpdatedAt());
this.user = user;
}
}
<UserController>
@GetMapping("/info")
public ResponseEntity<UserInfoResponse> userInfo(
@AuthenticationPrincipal UserAdapter userAdapter) {
log.info(userAdapter.getAddress());
return ResponseEntity.ok(userService.userInfo(userAdapter));
}
---------
수정
- UserAdapter가 엔티티 User를 상속하는 것은 엔티티를 계속 끌고 다니는 것이기 때문에 관계를 끊어줌.
- 엔티티 User -> Member로 이름 변경
- UserAdapter Dto 객체를 생성하여 UserDetails를 구현하게 하고 UserDetailsService의 loadByUsername() 메소드에서 입력한 토큰으로부터 DB에서 일치하는 사용자 정보를 찾아 UserAdapter로 반환하게 함.
<MemberSercieImpl>
@Override
public UserDetails loadUserByUsername(String mail)
throws UsernameNotFoundException {
Member member = memberRepository.findByEmail(mail)
.orElseThrow(() -> new MemberException(USER_NOT_FOUND));
return new UserAdapter(
member, member.getEmail(), member.getPassword());
}
<UserAdapter>
public class UserAdapter implements UserDetails {
private final Member member; //인증 완료된 Member 객체
private final String username; //인증완료된 Member ID
private final String password; //인증완료된 Member 패스워드
public UserAdapter(Member member, String username, String password) {
this.member = member;
this.username = username;
this.password = password;
}
public Member getMember() {
return this.member;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
- 컨트롤러에서 @AuthenticationPrincipal로 인증된 UserDetails객체인 UserAdapter를 받아와서 서비스 단에 넘겨주고 동작하게 함으로써 토큰을 직접받아 파싱, 검증하는 과정을 없앰.
@GetMapping("/info")
public ResponseEntity<UserInfoResponse> userInfo(
@AuthenticationPrincipal UserAdapter userAdapter) {
return ResponseEntity.ok(memberService.userInfo(userAdapter));
}
@PatchMapping("/update")
public ResponseEntity<UpdateInfo.Response> update(
@RequestBody UpdateInfo.Request request,
@AuthenticationPrincipal UserAdapter userAdapter) {
return ResponseEntity.ok(memberService.update(request, userAdapter));
}
참고
'프로젝트' 카테고리의 다른 글
GiftFunding) Docker Compose로 es와 kibana 띄우기 + nori 까지 설치 (1) | 2023.12.09 |
---|---|
GiftFunding) 도커에 Elasticsearch, Kibana 컨테이너 다운 및 실행 (2) | 2023.12.08 |
GiftFunding)필터 내 예외 처리 (1) | 2023.11.30 |
GiftFunding) Dirth Checking (feat.회원정보 입력된 부분 일부만 수정할 때) (0) | 2023.11.30 |
GiftFunding) Spring Boot와 Redis를 활용한 Resfresh Token 구현 (1) | 2023.11.26 |