티스토리 뷰

기록하게 된 이유

회사 프로젝트를 진행하면서 jwt를 처음 활용했는데 그 과정에서 배운 내용들을 정리해본다.

요구사항

정산과 관련된 서비스에 실명인증 프로세스가 필요하게 되어서 개발을 맡게 되었다.

인증 완료까지 한 화면에서 두 단계를 걸쳐서 이뤄져야 했다.

 

1) 인증하기 버튼을 누르면 기입된 정보로 실명인증을 해주는 외부기관의 API로 제대로된 정보임을 확인 받고

2) 인증 성공 후 아래 버튼이 활성화 되며 해당 버튼을 누르면 실명인증이 완료된 회원이라는 정보를 DB에 저장하는 API로 처리

간단하게 그려본 당시 기획안 컨셉

이렇게 한 화면에서 1) 2) 과정이 두 API로 분리되기 때문에 고민해야될 부분이 생겼다.

2단계 실명인증 과정에서 해당 요청을 보낸 유저가 1단계를 정상적으로 거쳤다는 것을 어떻게 보장하느냐가 문제였다.

추가로 혹시나 많이 들어올 트래픽에 대비하여 DB I/O 없이 처리하고자 했다.

 

요구사항을 정리하자면

1) 실명인증 -> 실명완료 두 단계로 API를 나눠서 처리하고

2) DB 등 서버에 인증상태를 저장하지 않고 1 -> 2 단계를 보장 

3) MSA 환경에서도 돌아갈 수 있도록

 

이 3가지를 모두 충족시키기 위해 JWT (Json Web Token)을 사용하기로 했다.

 

개발 내용

jwt를 실명인증을 정상적으로 마쳤다는 증서로 사용한다.

 

1) 실명인증이 완료되면 서버에서 jwt를 생성하고, 이를 클라이언트에게 응답에 포함

2) 클라이언트는 실명인증 완료 요청 시 1단계에서 받은 jwt를 포함해서 토큰 검증 과정을 거친 후 완료처리한다.

 

실제 회사 코드를 기재할 순 없기 때문에 위 내용을 담은 샘플코드를 적어본다.

@RestController
@RequestMapping("/api")
public class RealNameController {

    private final RealNameService realNameService;
    private final String SECRET_KEY = "my-secret"; //실제 운영에선 따로 관리

    public RealNameController(RealNameService realNameService) {
        this.realNameService = realNameService;
    }

    @PostMapping("/realname-verify")
    public ResponseEntity<?> verify(@RequestBody RealNameRequest request) {
        // 1단계: 실명인증
        String token = realNameService.verifyRealName(request.getName(), request.getBirth(), request.getPersonalId());
        // 토큰을 클라이언트에게 반환
        return ResponseEntity.ok(new TokenResponse(token));
    }

    @PostMapping("/realname-confirm")
    public ResponseEntity<?> confirm(@RequestHeader("Authorization") String token) {
        // 2단계: 토큰 검증
        try {
            Jws<Claims> claims = Jwts.parser()
                    .setSigningKey(SECRET_KEY.getBytes(StandardCharsets.UTF_8))
                    .parseClaimsJws(token.replace("Bearer ", ""));

            // 만료, 서명 등 검증
            // 인증된 사용자 정보 확인
            String name = (String) claims.getBody().get("name");
            String birth = (String) claims.getBody().get("birth");

            // 여기서 실명완료 로직 수행 (DB 저장, 가입 처리 등)
            // ...

            return ResponseEntity.ok("실명완료 처리 성공 for " + name);
        } catch (JwtException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid or expired token");
        }
    }
}
@Service
public class RealNameService {
    
    private final String SECRET_KEY = "my-secret";
    
    public String verifyRealName(String name, String birth, String personalId) {
        // 1) 실명인증 로직 수행(외부 API 호출 등)
        boolean isVerified = callExternalVerification(name, birth, personalId);
        
        if (isVerified) {
            // 2) JWT 생성
            return Jwts.builder()
                    .setSubject("realNameVerification")
                    .claim("name", name)
                    .claim("birth", birth)
                    .setIssuedAt(new Date(System.currentTimeMillis()))
                    .setExpiration(new Date(System.currentTimeMillis() + 30 * 60 * 1000)) // 30분 만료
                    .signWith(SignatureAlgorithm.HS256, SECRET_KEY.getBytes(StandardCharsets.UTF_8))
                    .compact();
        } else {
            throw new RuntimeException("실명인증 실패");
        }
    }
    
    private boolean callExternalVerification(String name, String birth, String personalId) {
        // TODO: 실제 실명인증 API 호출 로직
        return true; // 예시
    }
}

 

마지막으로 참고해야될 내용은 MSA 환경에서는 여러 마이크로서비스가 토큰을 검증 할 수 있어야하는 점이다.

 

  • HS256(대칭키)라면 모든 마이크로서비스가 같은 secret key를 가져야 한다.
  • RS256(비대칭키)라면 private key는 인증 발급 서비스(실명인증 전담 서비스)만 보관, 각 마이크로서비스들은 public key를 공유받아 토큰 서명을 검증

 

여기서 실제로 처음 개발할 때는 이를 고려하지 않아서 마이크로서비스 마다 다른 secret key를 생성한 채로 배포를 해서 검증 서버까지는 아무 문제 없다가 상용에서 계속 실패하는 이슈가 있었다...

//실제로 이렇게 저장하진 않지만 literal로 사용한다면 모든 서버들마다 secretkey를 공유한다는 점에서의 예시
private final String SECRET_KEY = "my-secret"; 

//이렇게 배포했어서 각 서버 마다 다른 키를 가지고 있었다...;
private final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);

 

성능 비교

개인적으로 궁금하여 테스트로 성능을 측정해봤다.

시니리오 (동시 사용자) 방식 평균 응답 시간 p95 응답시간
100 DB 접근 15ms 40ms
  JWT 5ms 15ms
1,000 DB 접근 70ms 200ms
  JWT 20ms 50ms
5,000 DB 접근 250ms 800ms
  JWT 80ms 250ms