Java

Spring JPA, SecurityContext사용시 ThreadLocal 사용범위 관련 #2

멋진그이름 2021. 9. 15. 15:55

<기존>

 - 비동기 병렬처리시 SecurityContext의 범위가 ThreadLocal이기 때문에 authentication 정보가 null 인 경우 발생

 - DelegatingSecurityContextAsyncTaskExecutor 을 사용하여 SecurityContext를 전달하도록 처리함

https://icthuman.tistory.com/entry/HHH000346-Error-during-managed-flush-null?category=568674 

 

Spring JPA, SecurityContext사용시 ThreadLocal 사용범위 관련 #1

<현상> - HHH000346: Error during managed flush [null] - Spring JPA 사용중 Transcation 처리가 정상적으로 되지 않는 현상 <원인> - 사용자 ID를 @CreatedBy 를 사용해서 관리중 (@EnableJpaAuditing) -..

icthuman.tistory.com

 

<추가오류사항>

 - 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 );
        }
    }
}