Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

이지선의 블로그

[프로젝트] [리팩토링] MSA 구조를 위한 인증/인가 처리 본문

Project/늘품

[프로젝트] [리팩토링] MSA 구조를 위한 인증/인가 처리

easyxun 2024. 7. 18. 17:43

문제 배경

원래 로그인 시 필자가 담당하는 유저 모듈에서 JWT 토큰을 발급하고 이를 통해 인증을 처리하였으나, 다른 모듈로 요청이 전달될 때 인증 및 인가 정보를 제대로 전달받지 못하는 문제가 발생했다!

이로 인해 다른 모듈에서 기능이 작동되지 않는 상황 ㅠ..


해결 방안

 

문제를 해결하기 위해 두 가지 방안을 고려하였다  :

  1. 모든 모듈에서 인증/인가를 구현하여 개별적으로 처리하는 방법
  2. api-gateway 모듈을 통해 인증/인가를 처리하는 방법

이 중 두 번째 방법을 채택하였는데 그 이유는,

api-gateway 모듈에서 인증/인가를 처리하게 된다면 코드 중복을 방지하고 다른 모듈을 추가하는 상황에서도 따로 인증/인가 로직을 추가할 필요가 없어 유지보수가 쉬워진다는 장점이 있기 때문이다.


즉! 해당 방법이 시스템 성능을 더 향상시킬 수 있음~~


구현 내용

user 모듈

  1. 로그인 성공 시 user 모듈에서 JWT 토큰을 발급
  2. 발급 된 JWT 토큰을 응답 헤더에 추가해서 반환

api-gateway 모듈

  1. api-gateway로 들어온 요청의 헤더에 저장되어있는 토큰을 해석하여 유저 정보를 추출
  2. 각 요청에 맞는 서비스 모듈로 요청을 재전송할 때 유저의 정보 'X-USER-ID' 를 헤더에 추가

구현 코드

✏️ api-gateway 모듈

1. FilterConfig : 라우트 구성
회원가입과 로그인 api를 제외한 모든 요청은 AuthorizationHeaderFilter를 거쳐간다.

@Configuration
public class FilterConfig {

    @Bean
    public AuthorizationHeaderFilter authorizationHeaderFilter(TokenValidator tokenValidator) {
        return new AuthorizationHeaderFilter(tokenValidator);
    }

    @Bean
    public RouteLocator gatewayRoutes(RouteLocatorBuilder builder, AuthorizationHeaderFilter authorizationHeaderFilter) {
        return builder.routes()
                .route(r -> r.path("/api/v1/users/**")
                        .filters(f -> f.filter(authorizationHeaderFilter.apply(
                                        new AuthorizationHeaderFilter.Config()
                                                .setWhiteList(Arrays.asList("/api/v1/users/signup", "/api/v1/users/login", "/api/v1/users/verification"))))
                                .rewritePath("/api/v1/users/(?<segment>.*)", "/api/v1/users/${segment}"))
                        .uri("http://localhost:8081"))
                .build();
    }
}

 

 

2. AuthorizationHeaderFilter :

- TokenValidator로 토큰 검증

-  토큰에서 유저 아이디 추출

- 추출 된 아이디는 'X-USER-ID'로 헤더에 저장

- 화이트리스트(로그인, 회원가입) 설정

@Component
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {

    private final TokenValidator tokenValidator;

    @Autowired
    public AuthorizationHeaderFilter(TokenValidator tokenValidator) {
        super(Config.class);
        this.tokenValidator = tokenValidator;
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            final ServerHttpRequest request = exchange.getRequest();
            String path = request.getURI().getPath();

            if (config.getWhiteList() != null && config.getWhiteList().stream().anyMatch(path::startsWith)) {
                return chain.filter(exchange);
            }

            if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
                return handleUnAuthorized(exchange);
            }

            String token = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);

            if (token != null && token.startsWith("Bearer ")) {
                token = token.substring(7);
            }

            try {
                if (!tokenValidator.validateToken(token)) {
                    return handleUnAuthorized(exchange);
                }
                String userId = tokenValidator.getUserId(token);

                ServerHttpRequest modifiedRequest = request.mutate()
                        .header("X-USER-ID", userId)
                        .build();

                return chain.filter(exchange.mutate()
                                .request(modifiedRequest)
                                .build());
            } catch (Exception e) {
                return handleUnAuthorized(exchange);
            }
        };
    }

    private Mono<Void> handleUnAuthorized(ServerWebExchange exchange) {

        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);

        return response.setComplete();
    }

    public static class Config {
        private List<String> whiteList;

        public List<String> getWhiteList() {
            return whiteList;
        }

        public Config setWhiteList(List<String> whiteList) {
            this.whiteList = whiteList;
            return this;
        }
    }
}

 

 

3. TokenValidator : 토큰 검증 로직

@Component
public class TokenValidator {

    @Value("${spring.jwt.secret}")
    private String secretKey;

    private SecretKey key; 

    @PostConstruct
    protected void init() {
        this.key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
    }

    public String getUserId(String token) {
        return parseClaims(token).getSubject();
    }

    public boolean validateToken(String token) {
        Claims claims = parseClaims(token);
        return claims.getExpiration().after(new Date());
    }

    private Claims parseClaims(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build()
                .parseClaimsJws(token)
                .getBody();
    }
}

 

✏️ user 모듈

1. 로그인 API 구현

로그인 시 JwtTokenProvider에서 JWT토큰을 발급받아 인증한다.

@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserLoginController {

    private final UserLoginService userLoginService;

    @PostMapping("/login")
    public ResponseEntity<JwtTokenDto> login(@RequestBody UserLoginRequestDto userLoginRequestDto) {
        JwtTokenDto jwtTokenDto = userLoginService.login(userLoginRequestDto);
        return ResponseEntity.ok(jwtTokenDto);
    }
}

@Service
@RequiredArgsConstructor
public class UserLoginService {

    private final UserRepository userRepository;
    private final JwtTokenProvider jwtTokenProvider;
    private final PasswordEncoder passwordEncoder;

    public JwtTokenDto login(UserLoginRequestDto userLoginRequestDto) {
        String email = userLoginRequestDto.email();
        String password = userLoginRequestDto.password();

        User user = userRepository.findByEmail(email)
                .orElseThrow(() -> new CustomException(ErrorCode.EMAIL_OR_PASSWORD_INVALID));

        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new CustomException(ErrorCode.EMAIL_OR_PASSWORD_INVALID);
        }

        String accessToken = jwtTokenProvider.generateAccessToken(email, user.getUserId());
        Long expiresTime = jwtTokenProvider.getExpiredTime(refreshToken);

        return new JwtTokenDto(accessToken, refreshToken, expiresTime);
    }
}

 

2.  각 모듈 컨트롤러 코드 수정

헤더에 저장된 'X-USER-ID'를 통해 회원 정보를 알 수 있다.

 

ex1) 회원 정보 수정

@PutMapping("/update")
    public ResponseEntity<ApiResponseDto<UserUpdateResponseDto>> update(
            @RequestHeader(value = "X-USER-ID") Long userId,
            @Valid @RequestBody UserUpdateRequestDto userUpdateRequestDto
    ) {
        log.info("UserController - 회원정보 수정 요청: {}", userUpdateRequestDto);
        UserUpdateResponseDto userUpdateResponseDto = userService.update(userId, userUpdateRequestDto);
        return ResponseEntity.ok(new ApiResponseDto<>(HttpStatus.OK, "회원 정보 수정 성공", userUpdateResponseDto));
    }

 

ex2) 일반 쿠폰 발급

@PostMapping("/general")
    public ResponseEntity<ApiResponseDto<CouponIssuedResponseDto>> issueCoupon(
            @RequestHeader(value = "X-USER-ID") Long userId, @RequestBody CouponIssuedRequestDto request) {
        CouponIssuedResponseDto issued = couponIssuedService.issueCoupon(userId, request);
        return ResponseEntity.ok(new ApiResponseDto<>(HttpStatus.OK, "쿠폰이 발급되었습니다.", issued));
    }

결과

이번 리팩토링은 MSA 프로젝트의 구조적 문제를 해결하는 과정이었다.

분리되어있는 모듈을 연결하는 과정!! 이 어려운 과제였다.

 

결론은 api-gateway 모듈로 모든 요청을 받는다는 점, 다른 모듈로 라우팅하는 방법, 라우팅 하기 전에 필터를 구현해서 로그인 유지를 해결하는 방법 등등 많은 배움이 있었다~~~~

 

조큼 오래 걸렸지만 다음 프로젝트에서는 조금 더 빠르게 구현 가능하지 아늘까 싶다..ㅠ

 

 

'Project > 늘품' 카테고리의 다른 글

[프로젝트] [기능구현] [Redis] 이메일 인증 서비스  (0) 2024.07.05