오늘은 jwt 토큰을 이용해서 로그인/로그아웃을 구현하던 중 만난 어려움에 대해서 포스팅 해보려고 합니다.
어려움을 만날 때까지의 과정
- 구글링을 통해서 로그인 기능 구현
- 토큰 값 없이 API요청을 할 경우 401에러와 권한이 없다는 메세지 출력
이후 로그아웃 기능을 구현하기 위해 controller로 넘어온 데이터를 확인하던 중 내가 커스텀한 데이터로 Authentication이 왔으면 좋겠다는 생각이 들었습니다.
수 많은 구글링을 해본 결과
방법은 3가지가 있습니다.
1. Authentication의 .getName()을 해서 데이터베이스에서 데이터를 조회하는 것입니다.
@GetMapping("test")
public ResponseEntity<String> test(
Authentication authentication
) {
String name = authentication.getName();
return ResponseEntity.ok("test");
}
위의 코드를 사용하면 처음 Token의 넣어졌던 Username을 가져올 수 있습니다.
저는 Username에 제가 만든 Users엔티티의 이메일 값을 넣었습니다.
따라서, String name = authentication.getName()으로 Users의 이메일을 알 수 있고 그 값으로 데이터를 조회할 수 있습니다.
2. UserDetails를 상속 받는 CustomDetails를 만들고 @AuthenticationPrincipal을 이용하는 것입니다.
@GetMapping("test")
public ResponseEntity<String> test(
@AuthenticationPrincipal CustomDetails customDetails
) {
return ResponseEntity.ok("test");
}
------------------------------------------------------------
@Getter
@NoArgsConstructor
public class CustomDetails implements UserDetails {
private Users users;
public static CustomDetails of(Users users) {
CustomDetails instance = new CustomDetails();
instance.users = users;
return instance;
}
위에가 @AuthenticationPrincipal을 이용한 controller 부분입니다.
밑에는 UserDetails를 상속받은 custom객체입니다.
저는 이 두번째 방법이면 충분히 만족스러웠고 두번째 방법으로 진행했습니다.
하지만 이 방법을 진행하면서 너무 많은 시행착오를 겪게 되었습니다... ㅠㅠ
천천히 알아보겠습니다.
1. 우선 정말 말 그래도 상속받은 객체를 만들고 controller에 @AuthenticationPrincipal을 붙여서 사용했습니다.. 당연히 결과는 실패...
2. loadUserByUsername의 return 값을 수정해봤습니다.
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UsersRepository usersRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return usersRepository.findByEmail(username)
.map(CustomDetails::of)
.orElseThrow(() -> new UsernameNotFoundException(username + " -> 데이터베이스에서 찾을 수 없습니다."));
}
}
기존에는 userDetails을 반환하던 메소드를 내부 로직에서 CustomDetails를 리턴할 수 있도록 수정했습니다.
이젠 확실히 정답에 가까워진거 같은데... 이번에도 제가 원하는 결과에는 도달하지 못했습니다...
이후로 무한 디버깅... 진짜 별에 별거를 다 찍어봤는데요.,.. ㅠㅠ
디버깅을 찍던 중 Authentication을 리턴해주는 로직을 발견했습니다. 이 로직에서는 여전히 스프링 시큐리티가 제공하는 User를 파라미터로 넘겨주고 있었습니다.
저는 여기서 User를 만들어서 파라미터로 넘겨주는 대신
3. loadUserByUsername 메소드를 호출해서 customDetails를 만들어 낸 뒤 파라미터로 넘겨줬습니다.
결과는 성공!!
public Authentication getAuthentication(String accessToken) {
// 토큰 복호화
Claims claims = parseClaims(accessToken);
if(claims.get(AUTHORITIES_KEY) == null) {
throw new RuntimeException("권한 정보가 없는 토근값입니다.");
}
// 클레임에서 권한 정보 가져오기
Collection<? extends GrantedAuthority> authorities = Arrays.stream(
claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// 기존 코드
//UserDetails principal = new User(claims.getSubject(), "", authorities);
//return new UsernamePasswordAuthenticationToken(principal, "", authorities);
//수정한 코드
UserDetails userDetails = customUserDetailsService.loadUserByUsername(claims.getSubject());
return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
}
정말 수많은 삽질 끝에
제가 원하는 커스텀 데이터가 찍히는 것을 확인할 수 있었습니다.
3. UserAdapter 방식입니다.
제가 직접 해보지 않아서 보여드릴 코드는 존재하지 않습니다만...
Users를 직접 받는 것이 아닌 어댑터 패턴을 이용해서 받는다고 합니다...
나중에 제가 직접 만들어보고 이어서 포스팅 해보겠습니다~
'스프링' 카테고리의 다른 글
[스프링 시큐리티]JWT 토큰안에 내가 원하는 값 넣기 (0) | 2023.04.10 |
---|---|
[스프링 시큐리티] redis를 이용한 jwt 로그아웃 만들기 (2) | 2023.01.31 |
스프링 이메일 인증 (0) | 2021.11.12 |
스프링 USER권한 페이지 (0) | 2021.10.08 |
스프링 권한에 따라 다른 기능 추가 - 등록, 찾기 (0) | 2021.10.07 |
댓글