Spring JPA, SecurityContext사용시 ThreadLocal 사용범위 관련 #2
<기존>
- 비동기 병렬처리시 SecurityContext의 범위가 ThreadLocal이기 때문에 authentication 정보가 null 인 경우 발생
- DelegatingSecurityContextAsyncTaskExecutor 을 사용하여 SecurityContext를 전달하도록 처리함
https://icthuman.tistory.com/entry/HHH000346-Error-during-managed-flush-null?category=568674
<추가오류사항>
- API 호출 시 문제가 없었으나 다음날 아침작업에서 동일한 오류 발생
<원인>
- 해당 로직이 @Scheduled 처리 될 경우 인증을 통해서 들어온 Thread가 아니기 때문에 authentication 정보가 null 인 경우 발생
<해결방안>
- 다음과 같이 로직을 변경하고 SecurityContext를 생성하는 부분을 추가하여 해결함
@Scheduled(cron = "0 30 9 * * *", zone = "Asia/Seoul")
public void scheduledMethod(){
Authentication originalAuthentication = SecurityContextHolder.getContext().getAuthentication();
try {
String token = loginService.createToken("admin");
SecurityContextHolder.getContext().setAuthentication(jwtTokenProvider.getAuthentication(token));
...
bizlogic();
...
}catch(Throwable t){
log.error("scheduledMethod error {}", t.getMessage());
}finally{
SecurityContextHolder.getContext().setAuthentication(originalAuthentication);
}
}
- 해당 로직을 완료한 뒤 auth정보를 원복처리해주도록 한다. (중요!)
- bizlogic내 비동기호출이 있으며 auth정보를 활용한다면 CompletableFuture.get()등을 활용하여 끝날때까지 대기해야합니다.
그렇지 않을경우 비동거처리 특성상 해당로직이 종료되기 전에 originalAuthentication으로 원복처리되어 의도치않은 현상이 발생할 수 있습니다.
- 예상외의 오류가 발생할 수 있기 때문에 finally 절로 처리하는 것을 추천합니다.
<참고사항>
- 내부 @Scheduled 을 사용하지 않고 외부 스케쥴러를 사용하여 API Call하게 한다면 해당방법을 사용하지 않을 수 있습니다.
- AuditorAware의 경우 해당 메소드에서 접근하는 Entity에 @CreatedBy컬럼여부에 관계없이 항상 동작한다.
- 보다 안전한 처리를 위해서 AuditAware에 별도로직을 추가하는 것도 좋습니다.
예를 들면 NPE가 발생하여 DB Transaction에 문제가 발생할 수 있는데. (특히 @Transactionl로 묶여있다면 모두 rollback됨)
예외 userId로 처리한뒤 사후에 발견하도록 하면 서비스가 중단되지 않도록 할 수 있습니다.
e.g)
public class LoginUserAuditorAware implements AuditorAware<Integer> {
@Override
public Optional<Integer> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
log.debug("auth {}", authentication);
LoginUserDetails loginUserDetails = (LoginUserDetails)authentication.getPrincipal();
Integer userId = loginUserDetails.getUserId();
log.debug("id{}", userId);
if(userId == null){
log.error("id is null {}");
return Optional.of(0);
}else{
return Optional.of( userId );
}
}
}