simple-auth : 토큰 인증 시나리오를 대신해주는 라이브러리 본문

simple-auth : 토큰 인증 시나리오를 대신해주는 라이브러리

JinHwan Kim 2023. 11. 26. 00:31

Simple-auth 

이번에 만든 simple-auth는 Spring 환경에서 사용자 토큰 인증, 재발급 처리를 돕는 라이브러리이다.

 

Spring에서 제작하여 사용자 측의 간단한 빈 설정만으로 관련 빈 들이 자동 등록되어 바로 사용할 수 있도록 하고, Jitpack 으로 배포하여 gradle, maven에서 쉽게 의존성으로 추가 할 수 있도록 하였다.

 

- 핸들러에 어노테이션을 붙이는 것으로 인증이 필요한 api 를 간단히 구분할 수 있다.

- JWT 토큰을 인증하고 필요시 refresh 토큰으로 재발급한다. 

- 토큰의 Payload를 직접 정의하고, 핸들러에서 argument로 받을 수 있다.

- Refresh 토큰을 만료시킬 수 있다.

 

이 글에선 왜 이 라이브러리 제작이 필요했는지, 사용 예시와 개발에서 고민해야 했던 부분, 사용 결과를 소개하려 한다.

 

저장소 : https://github.com/ecsimsw/simple-auth

 

첫 번째 니즈 : Core 다이어트

Picup 프로젝트에서 MSA를 도전해 보았다. 아직 어렵지만 각 모듈 별로 배포 리소스를 다르게 할 수 있는 점, 모듈별로 각자 설정에 따라 스케일 아웃 설정이 가능한 점, 모듈별로 필요한 기술 스택, 의존성을 따로 가질 수 있다는 점이 장점이었던 거 같다.

 

'모듈 별로 필요한 설정 / 코드만을 갖는다', '모듈 별로 비슷한 것들끼리 묶고, 다른 것끼리는 떨어트린다.' 

근데 모듈별로 공통되는 것은 어쩌지...?

모듈 간 공통으로 사용되는 코드라는 이름으로 Core 모듈을 만들었고 이제는 그 모듈이 뚱뚱해지고 있다.

예를 들면 A,B,C 중에 A/B에서 사용하는 것도 Core에, B/C에서 사용하는 것도 Core에, 그렇다고 A-B core, B-C core 모듈을 또 따로 만들기에는 과하고 그냥 다 Core에 때려 박는 느낌이다.

 

그래서 코어의 것을 라이브러리로 뺄 생각을 했다. 뚱뚱했던 코어 코드를 라이브러리로 걷어내고 필요한 모듈에만 (위 에시로는 A/B에만) 해당 라이브러리를 의존하는 것이다.

 

두 번째 니즈 : 프로젝트마다 반복되던 코드들 

프로젝트를 하면 항상 똑같이 하는 코드들이 있다. 당장 토큰 인증 / 재발행 로직, 암호화 처리가 생각난다. 매 프로젝트마다 똑같은 로직이 필요한데도 매번 다시 짜고 있다. 

 

더 이유는 변화였다. A 프로젝트에서 짰던 인증 로직인데, B 프로젝트에서 다시 짜다보니 조금씩 변화가 생기는 것이다. 더 문제는 새로 짠 로직이 더 좋아 보이는데 그렇다고 옛날에 했었던 프로젝트들을 뒤져가면서까지 옛 코드를 수정하기엔 번거로웠다.

 

그래서 그런 중복되는 로직들을 라이브러리로 만들 생각이었다. 프로젝트마다 해당 라이브러리의 버전들만 상이하고 사용 방식은 유사하도록 하다가, 변화를 결심했을 때는 코드 전체를 보는 게 아니라 라이브러리 버전 정보를 바꿔주고 그것이 쓰이는 꼴만 수정하면 되도록.

 

사용 예시

아래는 라이브러리를 적용했을 때의 사용 모습이다. Properties 값 몇개와 토큰 사용 설정 몇 개만 추가하면 아래처럼 인증이 필요한 API, 원하는 토큰 Payload 를 표시하는 것만으로 토큰 로직이 끝난다.

 

(구체적인 설정 방법은 위 깃헙 레포지토리에 추가하였다.)

 

@RestController
public class SampleController {

    private final AuthTokenService authTokenService;

    public SampleController(AuthTokenService authTokenService) {
        this.authTokenService = authTokenService;
    }

    // 1. 토큰 생성, 쿠키 응답
    @PostMapping("/sample/auth")
    public ResponseEntity<String> login(String username, HttpServletResponse response) {
        var userInfo = new MyLoginPayload(username, "ecsimsw@gmail.com");
        authTokenService.issue(response, userInfo);     
        return ResponseEntity.ok().build();
    }

    // 2. 인증 필요 API 표시, 커스텀 Payload resolve
    @GetMapping("/sample/auth")
    public ResponseEntity<String> auth(@JwtPayload MyCustomPayload info) {
        return ResponseEntity.ok(info.getName() + " " + info.getEmail());
    }

    // 3. Refresh token 무효화
    @DeleteMapping("/sample/auth")
    public ResponseEntity<Void> revoke(String username) {
        authTokenService.revoke(username);
        return ResponseEntity.ok().build();
    }
}

 

Component scan 경로를 어떻게 알려줄까

라이브러리 사용처 측에 "라이브러리 측 Component scan 위치를 어떻게 알려줘야 할지"를 가장 많이 고민했다.

 

디렉터리 구조가 동일하면 문제되지 않지만 라이브러리 사용 측과 라이브러리 프로젝트의 디렉터리 구조는 다른 게 일반적이고, 라이브러리 사용 측의 Component scan 에는 라이브러리의 디렉터리를 포함하지 않아 라이브러리에서 정의한 Bean이 생성 / 주입되지 않는다는 문제가 있었다. 또 그렇다고 사용처에서 구체적인 라이브러리의 빈 주입 root path를 설정하게 하고 싶진 않았다.

 

Component scan root path 을 갖는 설정 파일을 정의하고 어노테이션으로 이 설정 파일을 import 하는 것으로 rootPath를 숨기면서 라이브러리 쪽 ComponentScan 위치를 사용 측에서 확인할 수 있도록 하였다.

 

// root path 정보를 포함하는 설정 파일
@Configuration
@ComponentScan(basePackages = "ecsimsw.auth")
@EnableRedisRepositories(basePackages = "ecsimsw.auth")
public class SharedConfigurationReference {

// 그 설정 파일을 import 하는 어노테이션
@Import(SharedConfigurationReference.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface EnableSimpleAuth {

 

그리고 사용 측에선 이 어노테이션을 추가하는 것으로 앞서 정의한 설정 파일을 읽어 라이브러리 측 Component scan root path를 읽고 빈들을 정상 주입이 가능해진다.

 

@EnableSimpleAuth
@Configuration
public class SimpleAuthConfig implements WebMvcConfigurer {

 

이 빈 스캔 경로 문제 정도만 평소에 고민해 적이 없어 나만의 답을 찾는데 시간을 썼던 것 같고, 나머지 코드는 사실 프로젝트에서 사용한 그대로 그 자체라 특별히 개발에 어려움은 없었다.

 

적용 결과와 다음 스텝

아래는 picup 에 적용한 이후 코드 파일의 변화이다. 회색 파일 표시가 모두 프로젝트에서 제거된 파일이고, 라이브러리 설정을 위해 두 개의 파일이 추가되었다. 해당 로직은 모든 모듈에서 다 사용되고 있어 필요 모듈별로 주입되진 않았지만 그래도 picup 프로젝트에서 관리해야 할 코드는 확 줄은 것을 볼 수 있다.

 

이제는 토큰 처리 로직에 변화가 있으면 이는 picup 측이 아니라 simple-auth 의 문제로 보고 작업 후 최신 버전을 공지해야 할 것이다. 그 변화를 picup에도 적용하고 싶을 때는 최신 버전으로 라이브러리 의존성을 업데이트하고, 그렇지 않고 지금으로도 만족하다면 simple-auth의 변화에 상관없이 지금 구버전으로 남기면 되는 문제이다.

 

 

다음 스텝으로 필터를 사용자 선택지로 제공하고 싶다. 지금은 인증이 필요한 api를 구체적이고 명확하게 표시하고 싶어 핸들러의 어노테이션을 읽고 jwt 확인 여부를 결정한다. 따라서 핸들러 정보를 알고 인증 여부를 결정하기 위해 필터가 아닌 인터셉터에서 처리하고 있다.

 

요청은 필터를 거친 이후에 인터셉터를 만나게 되므로 만약 라이브러리 사용자가 인증 후에 처리되어야 하는 필터가 있는 상황이라면 문제가 될 것이다. 지금 picup 에선 그런 꼴이 없어 우선은 깔끔하고 설정이 쉬운 인터셉터로 구현했는데, 다음 버전에선 인터셉터와 필터를 선택지로 줄 수 있도록 기능을 추가할 생각이다.

 

 

Comments