전체 보기
🍀

로그인 유저 UserUtils에서 찾아오기와 인가 필터의 중복된 쿼리 제거(feat 세션 방식에서 토큰 방식으로의 전환), AWS S3 CORS 에러 해결

작성일자
2023/10/03
태그
DIARY_DEVELOP
프로젝트
FIS
책 종류
1 more property

세션 인증 방식에서 토큰 인증 방식으로의 전환하며, 로그인 유저를 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
복사