💡 발단
현재 프로젝트에선 팀원이 Security 로직을 잘 구현해놨다.
*카카오 연동 소셜 로그인(OAuth2.0)과 JWT 인증 방식을 사용한다.
그래서 난 내가 맡은 기능만 구현하면 되겠다고 생각했는데, Security의 구조를 모르니 문제가 생겼다.
로그인된 유저 객체를 ... 어떻게 가져오지? 분명 DB에서 조회해올텐데?
찾아보니 편의 메서드를 만들어서 쓰고 있었고, 난 이 함수를 불러와서 다른 로직을 구현할 수 있었다.
public class UserService {
// ...
public User getCurrentUser() {
// DB에서 유저 엔티티 조회
return userRepository.findBySocialEmail(getCurrentUserSocialEmail()).orElseThrow(() -> new UserException(StatusCode.NOT_FOUND_USER));
}
public class SecurityUtils {
// 프로젝트에서 쓰기로 한 유저 식별값을 받아오는 편의 메서드
public static String getCurrentUserSocialEmail() {
// 유저 관련 정보를 불러오는 부분..?!!
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// ...
// 유저 식별값 반환
return authentication.getName();
}
}
코드를 자세히 보다보니 궁금증이 생겼다.
저 한 줄의 동작 원리가 너무 궁금했다.
SecurityContextHolder.getContext().getAuthentication();
Authentication은 대체 어디서 오는거지?
Authentication은 SpringContext의 필드였다.
SpringContext는 정말 단순한 인터페이스다.
Authentication 필드 딱 하나가 있고 그에 대한 getter, setter밖에 없다.
🔍 흐름 파악하기
결론적으로 유저의 정보가 SecurityContext에 담기는 순서는 다음과 같다.
1. 사용자가 JWT 토큰을 담아서 요청 (Http Request)
2. SecurityConfig에 설정된 AuthenticationFilter가 요청을 가로챔
3. 요청으로부터 토큰 추출
4. 토큰 유효성 검사
5. 토큰 파싱
6. 토큰에 있던 식별값으로 DB에서 유저 엔티티를 조회
7. 유저 엔티티를 담은 UserDetails 객체 생성
8. UserDetails 등을 담은 Authentication 객체를 생성
9. Authenticaton 객체를 SecurityContext에 저장
10. Spring 전역에서 SecurityContext 사용, 유저 객체 불러옴
SecurityConfig
우선 SecurityConfig에서 필터를 추가했기 때문에 매 요청마다 어떤 로직이 실행된다.
// Filter 추가
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
AuthenticationFilter (JWT 방식)
요청에서 토큰값 추출 -> Authentication 객체 불러옴 -> SecurityContext에 Authentication 객체 주입
@RequiredArgsConstructor
@Slf4j
// 커스텀 필터. JWT 인증 방식을 구현한 핵심 로직
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtProvider jwtProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 요청에서 토큰값 추출
String accessToken = jwtProvider.resolveToken(request);
// 토큰 유효할 경우
if (StringUtils.hasText(accessToken) && jwtProvider.validate(accessToken)) {
// Authentication 객체 불러옴
Authentication authentication = jwtProvider.getAuthentication(accessToken);
// SecurityContext에 Authentication 객체 주입
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request,response);
}
}
근데 Authentication 객체는 어떻게 생성되는거지?
JwtProvider.getAuthentication
JWT파싱 -> 유저 식별값으로 유저 엔티티 조회 -> 유저 엔티티 정보를 담아서 Authentication 객체 생성
@Component
@Slf4j
@Transactional
public class JwtProvider {
// ...
public Authentication getAuthentication(String accessToken) {
// JWT 파싱
Claims body = Jwts.parserBuilder()
.setSigningKey(privatekey)
.build()
.parseClaimsJws(accessToken)
.getBody();
// 프로젝트에서 쓰기로 한 유저 식별값(카카오측에서 받은 소셜 이메일을 조합한 정보)
String socialEmail = customEncryptUtil.decrypt(body.getSubject());
// PrincipalDetailsService : 유저 엔티티 조회
UserDetails userDetails = principalDetailService.loadUserByUsername(socialEmail);
// Authoentication 구현체 반환
return new UsernamePasswordAuthenticationToken(userDetails,"",userDetails.getAuthorities());
}
// ...
}
UserDetails는 어떻게 생성되는거지?
PrincipalDetailsService : 유저 엔티티 조회
DB에서 User 엔티티 조회 -> User 엔티티를 인자로 한 UserDetails 생성 및 반환
@Service
@RequiredArgsConstructor
public class PrincipalDetailService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String socialEmail) throws UsernameNotFoundException {
// DB에서 User 엔티티 조회
User principal = userRepository.findBySocialEmail(socialEmail)
.orElseThrow(()-> new UsernameNotFoundException("해당 사용자를 찾을 수 없습니다. :" + socialEmail));
// User 엔티티를 인자로 한 UserDetails 생성 및 반환
return new PrincipalDetails(principal);
}
}
PrincipalDetails는 미리 정의해놓은 커스텀 객체다. UserDetails의 구현체이고, User 엔티티를 필드로 갖는다. 그래서 Spring 전역의 메서드들에서 유저 엔티티에 접근할 수 있게 된다!
public class PrincipalDetails implements UserDetails {
private User user;
// ...
이렇게 만든 UserDetails를 UsernamePasswordAuthenticationToken 생성하는데 쓴다.
UsernamePasswordAuthenticationToken과 Principal
UsernamePasswordAuthenticationToken은 Authentication의 구현체이다.
그리고 Principal은... 결국 엔티티를 표현하는 인터페이스였다.
represent any entity, such as an individual, a corporation, and a login id.
그리고 우리가 아까 생성한 UsernamePasswordAuthenticationToken은 첫번째 인자인 UserDetails를 Principal로써 넣는다!
즉, Authentication 객체 안에 UserDetails가 들어가게 되고
우린 언제든지 SpringContext에 담겨있는 Authenticatoin을 불러올 수 있게 된다.
또한 거기에 담긴 Principal(==User 엔티티 정보)를 불러올 수 있게 된다.
SecurityContextHolder.getContext().getAuthentication();
'Back-end > Spring Boot' 카테고리의 다른 글
[Spring Boot] 연관관계 생성 메서드 삽질 (0) | 2023.08.23 |
---|---|
[Spring JPA] 실시간으로 적재되는 데이터와 부모 엔티티 묶어서 가져오기 (0) | 2023.08.10 |
[Spring Boot] 스웨거 springdoc-openapi 적용 (webmvc-ui) (0) | 2023.08.09 |
[Spring Boot] 예외처리 야무지게 하기 (0) | 2023.08.04 |
[Spring Boot] 서비스에서 로그인된 유저 정보에 대한 의존성 최소화하기 (@AuthenticationPrincipal) + 스프링 계층 구조 지키기 (0) | 2023.07.30 |