오늘 한 일
Convention정리
Spring Security 환경 설정 및 필터 구현
S3 이미지 컴포넌트 개발
JWT 파싱 및 유효성 검사 기능 개발
JPA AuditAware 컴포넌트 개발
작업 내용
Security 환경 구현
Security의 인증 부분을 항상 구현해왔으나 이전 구현과정에서 항상 막혔던 것이 예외 발생시 처리 방법이였다.
기존의 GlobalExceptoinHandler로 예외내용에 대해 전파가 되지 않아 애를 먹었는데 드디어 해결방법을 찾아냈다.
이 또한 별도의 포스팅으로 남길것이나 여기에 간단히 이미지와 코드를 남겨두려고 한다.
이미지처럼 Filter는 DispatcherServlet의 이전에 요청을 감지하고 filterChain에 의해 순차적으로 수행된다.
이 과정에서 인증/인가 작업을 수행하거나 별도의 로직을 전처리할수도 있는데 나는 토큰의 유효성 검증을 수행하는 AuthenticatoinFilter를 작성하였다. 이 과정에서 Spring Context에 등록된 GlobalExceptionHandler를 사용하지 않고 예외를 Response 객체에 담아 보여주기 위해서는 아래와 같은 별도의 코드가 필요하다.
//인증Filter 연결
http.addFilterBefore(new AuthenticationFilter(jwtHelper), UsernamePasswordAuthenticationFilter.class);
//Filter 예외처리
http.exceptionHandling(exception -> exception.authenticationEntryPoint(new JwtAuthenticationEntryPoint()));
addFilterBefore
메서드를 통해 UsernamePasswordAuthentication의 유저 인증과정 전에 토큰에 대한 검증이 이루어지도록 순서를 지정한다.
그 과정에서 발생하는 예외는 exceptionHandling
메서드를 통해서 예외를 반환하도록 설정할 수 있다.
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
String message = request.getAttribute("message").toString();
int code = (int)request.getAttribute("code");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
ObjectMapper objectMapper = new ObjectMapper();
String result = objectMapper.writeValueAsString(ExceptionResponse.builder().httpStatus(HttpStatus.FORBIDDEN).status(message).code(code).build());
response.getWriter().write(result);
}
}
예외 Handler에서 지정한 JwtAuthenticationEntryPoint 클래스는 AuthenticationEntryPoint
을 상속받은 구현체로 이전 필터에서 전달받은 request의 속성들을 가져와 response에 예외에 대한 내용을 담고 반환할 수 있다.
원래라면 500 상태 코드와 예외 내용에 대해 보여줬겠지만 위와 같은 방식을 통해 원하는 메세지와 코드를 바디에 담거나 상태코드를 원하는 상태로 출력이 가능하다!
S3 이미지 파일 컴포넌트
이 부분에서 막혔던 부분은 ACL을 퍼블릭 상태로 처리하지 못한 부분인데 네트워크적인 개념은 나중에 VPC 개념과 함께 공부해보려고 한다.
S3에 대한 정책도 자세히 알아보고 싶어서 생명주기, 정책관리등과 함께 정리해보려고 한다.
JWT 구현
JWT를 이전에는 하나의 서비스로 보고 JWT로 생성한 RefreshToken은 무조건 저장해야 한다는 개념이 있었는데 튜터님의 강의 덕분에 개념이 확실히 정리되었다. 토큰 자체는 Stateless
한 상태로 통신에 사용된다는 장점이 있는데 이를 DB에 저장한다면 그 개념이 모호해지는 것이다. 또한 Jwt의 로직은 별도의 데이터를 조회하거나 삽입한는 부분이 없기에 정적인 서비스이다. 그러므로 JWTService라는 레이어로 생각하지 않고 다른 서비스들이 이용하는 하나의 컴포넌트 개념으로 JWTHelper를 하나 만들기로 했다. 지금와서 생각해보면 Util이라는 말이 더 좋은것 같기도 하다. Helper는 다른 서비스들이 공통적으로 사용하는 개념을 하나의 공동 메서드로 만들어 둔 것이기도 하기 때문이다.
지금은 JwtHelper에 Validate, Parsing하는 기능을 구현해뒀다.
JpaAuditAware
@CreatedBy, @LastModifiedBy는 @CreatedDate, @LastModifiedDate와는 다르게 별도의 설정이 필요하다.
필요한 설정은 아래 코드와 같다.
@RequiredArgsConstructor
public class AuditorAwareImpl implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(authentication == null || !authentication.isAuthenticated()){
return Optional.empty();
}
CustomUserDetail userDetail = (CustomUserDetail) authentication.getPrincipal();
return Optional.of(userDetail.getUsername()); // 또는 userDetail.getUuid()를 반환할 수도 있습니다.
}
}
다른사람과 로직이 다를 수도 있겠지만 내 로직은 SecurityContextHolder에서 인증된 객체를 가져와 유저의정보를 반환해주는 로직이다.
이를 AuditorAware인터페이스의 getCurrentAuditor에 구현하면 정상적으로 생성,수정자가 추가되는 것을 확인할 수 있다.
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public class BaseEntity {
@CreatedBy
@Column(name = "created_by", updatable = false)
private String createdBy;
@LastModifiedBy
@Column(name = "modified_by")
private String modifiedBy;
@Setter
@Column(name = "deleted_by")
private String deletedBy;
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "modified_at")
private LocalDateTime modifiedAt;
@Setter
@Column(name = "deleted_at")
private LocalDateTime deletedAt;
}
별도의 BaseEntity를 mappedSuperClass
로 만들었고 @EntityListeners(AuditingEntityListener.class)
이 설정을 함으로써 Auditing이 가능해진다.
마지막으로 DeletedBy와 DeletedAt은 직접 설정해야 하기에 각 엔티티에 @preRemove
를 적용해서 삭제하기 전에 전처리를 해주는 메서드를 만들었다.
@PreRemove
public void preRemove() {
this.setDeletedAt(LocalDateTime.now());
this.setDeletedBy(this.name);
}
나는 엔티티를 SoftDelete방식으로 처리할 것이여서 엔티티마다 이렇게 어노테이션을 적용하여 데이터가 삭제되지 않도록 하고, 조회시에도 is_delted가 true인 유저만 조회되도록 설정했다!
@SoftDelete
@SQLRestriction("where is_deleted = false")
내일 할일
- 팀원과 코드 리뷰
- CI/CD 파이프라인 개발
- User Controller 및 Dto 개발
'TIL' 카테고리의 다른 글
[TIL] 팀원간 코드 리뷰 진행 및 인덱스가 많아지면 생기는 문제, JPA의 flush 발생하는 조건 (0) | 2025.02.18 |
---|---|
[TIL] SoftDelete 개발 방식, 서브 모듈 설정, PR 및 Postman으로 API문서 공유하기 (0) | 2025.02.17 |
[TIL] 패키지 전략 및 토큰과 세션 개념 정리, 서비스와서비스끼리 의존관계를 갖게 하지 않는 이유 (1) | 2025.02.14 |
[TIL] API Document 피드백 수렴 및 기술, 컨벤션 회의 (0) | 2025.02.13 |
[16조 문서] API Document, Table Document, ERD (0) | 2025.02.13 |