세션 인증 방식에서 토큰 인증 방식으로의 전환하며, 로그인 유저를 UserUtils에서 찾아오기와 중복된 쿼리 제거를 해보았습니다
•
서론)
◦
세 개의 서비스 간 통합 로그인을 구축하고자 했다.
◦
이때, 셋 중 한 서비스만 세션 방식을 취하고 있었다.
◦
따라서 해당 서비스를 세션 방식에서 토큰 방식으로 전환하기로 했다.
◦
후엔 멀티모듈을 도입해 세 서비스의 인증 서버를 하나로 빼줄 예정이다.
◦
일단 이번 글에선 전환까지만 이야기할 것이고 그 중에서도 인가 부분에 대해 주로 이야기하겠다
◦
인증 부분까지 쓰려면 특별할 게 없어서 아예 spring security 처음인 분들을 위해 포스트로 쓰는 게 나을 거 같아서다.
◦
인가 부분은 조금 독특하고 쉽게 구현했다. 이 부분을 쓰겠다.
•
본론)
◦
AuthorizationFilter
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
private final JwtTokenProvider jwtTokenProvider;
public JwtAuthorizationFilter(AuthenticationManager authenticationManager, JwtTokenProvider jwtTokenProvider) {
super(authenticationManager);
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader(JwtProperties.HEADER_STRING);
if (header == null || !header.startsWith(JwtProperties.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
String token = request.getHeader(JwtProperties.HEADER_STRING).replace(JwtProperties.TOKEN_PREFIX, ""); // Bearer 제거
Authentication authentication = jwtTokenProvider.getAuthentication(token); // 토큰 검증 -> 인증 완료 (AuthenticationManager 대체)
SecurityContextHolder.getContext().setAuthentication(authentication); // 권한 구분 위해 Authentication 객체를 생성해 세션에 저장
chain.doFilter(request, response); // 다음 필터로 진행
}
}
Java
복사
◦
JwtTokenProvider
private String getUsername(String token) {
return getClaims(token).getSubject();
}
private UserRole getUserRole(String token) {
String userRole = getClaims(token).get(JwtProperties.TOKEN_ROLE, String.class);
return Optional.ofNullable(UserRole.valueOf(userRole))
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ENUM_VALUE));
}
public Authentication getAuthentication(String token) {
String loginId = getUsername(token);
UserRole userRole = getUserRole(token);
UserDetails userDetails = principalDetailsService.loadUserByUsernameAndUserRole(loginId, userRole);
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
Java
복사
◦
PrincipalDeatilsService
public PrincipalDetails loadUserByUsernameAndUserRole(String username, UserRole userRole) throws UsernameNotFoundException {
if (teacherRepository.existsByAuthInfoLoginIdAndUserRole(username, userRole)) {
return new PrincipalDetails(username, userRole);
} else {
throw new UsernameNotFoundException("해당 유저가 존재하지 않습니다.");
}
Java
복사
◦
SecurityUtils ← UserUtils를 한 번 더 분리한 것. UserUtils로 합쳐도 무관함
public class SecurityUtils {
private static PrincipalDetails getPrincipalDetails() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !(authentication.getPrincipal() instanceof PrincipalDetails)) {
throw new IllegalStateException(
"Authentication not found or the principal is not an instance of PrincipalDetails");
}
return ((PrincipalDetails)authentication.getPrincipal());
}
public static String getCurrentUsername() {
return getPrincipalDetails().getUsername();
}
}
Java
복사
◦
UserUtils ← 현재 로그인한 유저 반환
@Component
@RequiredArgsConstructor
@Transactional
public class UserUtils {
private final UserRepository userRepository;
public String getCurrentUsername() {
return SecurityUtils.getCurrentUsername();
}
public User getCurrentUser() {
String nickname = getCurrentUsername();
return userRepository.findByNickname(nickname)
.stream()
.findFirst()
.orElseThrow(() -> new AccessDeniedException("User is not authorized to access this resource"));
}
}
Java
복사
•
결론)
◦
핵심은 로그인한 유저 정보가 필요할 때 SecurityContext에서 빼온 userId로 User를 찾아오는 부분을 UserUtils로 분리했단 점이다.
◦
이를 통해 기존의 다른 서비스들이 UserRepository를 쓰는 부분을 없애 도메인 간 종속성을 줄여줄 수 있었다.
◦
한 가지 더 주의할 점을 적어보자면, Security에 익숙치 않은 분들이 주로 하는 실수가 하나 있다.
▪
AuthorizationFilter에서 토큰 정보를 토대로 SecurityContext에 로그인한 유저 정보를 저장할 때 User를 찾아오고, 실제 서비스 단에서 로그인 한 유저가 필요할 때 SecurityContext에서 빼온 User 정보(authentication.getPrinciapal().getUser())를 토대로 User를 한 번 더 찾아오는 것이다.
▪
즉, 로그인한 유저 정보를 알아야 하는 api들에서 중복된 쿼리(findById)를 날리는 것이다.
▪
결론만 말하자면, 둘 중 필수로 필요한 건 후자다.
▪
SecurityContext에 User 객체 자체를 저장하고 그걸 그대로 사용하려하면 트랜잭션 문제로 이를 그대로 사용하면 join된 컬럼은 못찾아오기 때문이다.
▪
따라서 결국엔 SecurityContext에 저장된 User 객채든 User id든 뭐든 간에 User와 관련된 정보를 이용해서 User 객체를 찾아오는 로직을 서비스단에서 날려줘야 한다.
▪
그래서 내가 추천하는 바는 SecurityContxt에 User 객체를 굳이 저장하지 않는 것이다.
▪
SecurityContext엔 id 혹은 loginId 같은 unique filed만 저장해두고, 이를 이용해 실제 서비스 단에서 로그인 한 유저를 알아야 할 때 findById를 해주자. 이를 Utils로 빼주면 더욱 좋고 말이다.
▪
그렇다면 SecurityContext에 userid를 저장해줄 때 이제 아예 User를 찾아올 필요가 없는 것일까?
▪
그건 아니다. 토큰에 담긴 user정보를 무한정 신뢰할 순 없기 때문이다. 물론 이에 대해선 의견이 나뉠 수도 있을 거 같다
▪
어쩃든, 나는 그래서 토큰 정보를 토대로 SecurityContext에 user 정보를 저장할 땐 findUserById까진 필요 없고 existsById로 검증하는 정도를 추천한다. 토큰을 신뢰해 아예 exists 쿼리도 안 날리는 분들도 있긴 한데 exists 쿼리는 jpa가 알아서 최적화를 해주기에 큰 리소스가 들지 않는다 생각해 난 요 정도는 해주는 편이다
AWS S3 CORS 에러 해결
•
이미지 링크를 프론트에서 띄우려는데 cors가 뜬다 요청 받았다.
•
s3 버킷에 가서 간단한 설정만 해주면 해결할 수 있다.
•
s3 버킷 > 권한 > cors 편집 으로 들어가서 아래 내용을 입력해준다.
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"HEAD"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": [
"x-amz-server-side-encryption",
"x-amz-request-id",
"x-amz-id-2"
],
"MaxAgeSeconds": 3000
}
]
JSON
복사