MSA에서 JWT 검증하기

dasd412·2023년 1월 7일
1

MSA 프로젝트

목록 보기
20/25

코드 분석

1. 인증 서비스에서 JWT 발급할 때 원하는 정보를 넣자.

package com.dasd412.api.writerservice.adapter.in.security.jwt;

@Component
public class JWTTokenProvider {
	...
    
    public String issueNewJwtAccessToken(String username, Long writerId,String requestURI, Collection<? extends GrantedAuthority> authorities) {
        Claims claims = Jwts.claims().setSubject(username);

        claims.put(USER_ID,writerId);

        List<String> roles = authorities.stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());

        claims.put(ROLE, roles);

        return Jwts.builder().addClaims(claims)
                .setExpiration(new Date(System.currentTimeMillis() + ACCESS_EXPIRED_TIME))
                .setIssuedAt(new Date())
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .setIssuer(requestURI)
                .compact();
    }

}

위 코드에서 claims.put()을 주목하자. 이 코드를 통해서 JWT에 원하는 정보를 넣을 수 있다. 나는 컨트롤러 코드들에 작성자 id가 파라미터가 필요했기 때문에 해당 정보를 넣어줬다.

package com.dasd412.api.writerservice.adapter.in.security.filter;

public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
 	@Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
            PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();
            
            String accessToken = jwtTokenProvider.issueNewJwtAccessToken(principalDetails.getUsername(), principalDetails.getWriter().getId(), request.getRequestURI(), principalDetails.getAuthorities());
            
            LocalDateTime expired = LocalDateTime.ofInstant(jwtTokenProvider.retrieveExpiredTime(accessToken).toInstant(), ZoneId.systemDefault());
            
            //중략
            
            Map<String, Object> responseBody = Map.of(
                    "accessToken", accessToken,
                    "expired", expired.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
            );

            new ObjectMapper().writeValue(response.getOutputStream(), responseBody);
    }
}

그리고 인증이 성공되었을 때 발동되는 successfulAuthentication()에서 JWT 정보를 response body에 넣어주었다.

2. 게이트 웨이에서 JWT를 검증하자.

필터 만들기

package com.dasd412.api.gatewayserver.filter.security;
import com.dasd412.api.gatewayserver.security.JWTTokenProvider;

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

    private final JWTTokenProvider jwtTokenProvider;

    public AuthorizationHeaderFilter(JWTTokenProvider jwtTokenProvider) {
        super(Config.class);
        this.jwtTokenProvider = jwtTokenProvider;
    }

    static class Config {

    }

    @Override
    public GatewayFilter apply(Config config) {
        return ((exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            
            HttpHeaders headers = request.getHeaders();

            //프론트에서 요청을 쏠 때, Authorization header에 토큰이 담겨 있어야 한다.
            if (!headers.containsKey(HttpHeaders.AUTHORIZATION)) {
                throw new NoAuthorizationHeaderException("authorization header not exist");
            }

            String authorizationHeader = headers.get(HttpHeaders.AUTHORIZATION).get(0);

            String token = authorizationHeader.replace("Bearer", "");

            jwtTokenProvider.validateJwtToken(token);

            String subject = jwtTokenProvider.retrieveSubject(token);
            String writerId = jwtTokenProvider.retrieveWriterId(token);

            ServerHttpRequest requestWithHeader = request.mutate()
                    .header("user-name", subject)
                    .header("writer-id", writerId)
                    .build();

            return chain.filter(exchange.mutate().request(requestWithHeader).build());
        });
    }

}

제일 먼저 인증 서비스에서 사용된 JWTTokenProvider를 게이트웨이에서도 작성해야한다.

여기서 중요한 부분은 두 가지이다. 나머지는 헤더에서 파싱하거나, 토큰에서 파싱하는 것뿐이다.

  1. claims.put(USER_ID,writerId);에서 넣은 정보를 토큰에서 해석해내는 코드이다.
String writerId = jwtTokenProvider.retrieveWriterId(token);
  1. 헤더에 정보를 담은 후, 다른 마이크로 서비스에 전파하는 코드이다.
            ServerHttpRequest requestWithHeader = request.mutate()
                    .header("user-name", subject)
                    .header("writer-id", writerId)
                    .build();

            return chain.filter(exchange.mutate().request(requestWithHeader).build());

yml 설정 추가하기

spring:
  cloud:
    gateway:
      routes:
        - id: diary-service
          uri: lb://diary-service
          predicates:
            - Path=/diary/**
          filters:
            - AuthorizationHeaderFilter
            - RewritePath=/diary/(?<segment>.*),/$\{segment}

        - id: writer-service
          uri: lb://writer-service
          predicates:
            - Path=/writer/login
            - Method=POST
          filters:
            - RewritePath=/writer/(?<segment>.*),/$\{segment}

        - id: writer-service
          uri: lb://writer-service
          predicates:
            - Path=/writer/signup
            - Method=POST
          filters:
            - RewritePath=/writer/(?<segment>.*),/$\{segment}

        - id: writer-service
          uri: lb://writer-service
          predicates:
            - Path=/writer/**
          filters:
            - AuthorizationHeaderFilter
            - RewritePath=/writer/(?<segment>.*),/$\{segment}

주목할 부분은 filters이다. AuthorizationHeaderFilter를 특정 경로에 매핑해주고 있는데, 해당 경로로 진입하면 매핑된 필터를 거치게 된다.

로그인과 회원가입은 당연히 JWT 검증을 할 수 없기 때문에 필터를 넣지 않았다.

3. 마이크로 서비스에서 정보를 사용하자.

2.에서 게이트웨이는 헤더에 writer-id를 담은 후, 다른 마이크로 서비스에 전파하고 있다.
그리고 각 서비스들의 컨트롤러는 @RequestHeader를 활용해서 해당 정보를 받아온다.

@RestController
public class DiaryDeleteRestController {

	 public ApiResult<?> deleteDiary(@PathVariable Long diaryId, @RequestHeader(value = "writer-id") String writerId){
     ...
     }     
}

4.테스트 코드는 이렇게

            mockMvc.perform(MockMvcRequestBuilders.post(URL)
                    .contentType(MediaType.APPLICATION_JSON)
                    .header("writer-id", "1")
                    .content(new ObjectMapper().writeValueAsString(postRequestDTO)))

모킹할 때 헤더 정보로 주입하면 된다.

5. 요청은 이렇게

Authroization 헤더에 Bearer (발급받은 JWT 토큰 값)을 넣고 요청하면 된다.


느낀 점

게이트웨이에서 전부 검증하기 때문에 다른 마이크로 서비스들은 인증과 관련한 코드를 집어 넣을 필요가 없어졌다.

이전 모놀리식 프로젝트에서는 각 도메인 컨트롤러마다@AuthenticationPrincipal이라는 어노테이션을 매번 붙여야 했다.

즉, MSA와 JWT를 같이 적용함으로써 각 도메인은 자기 할일에만 더 집중할 수 있게 됬다. (응집성)

물론, 게이트웨이가 뻗으면 SPOF가 되기 때문에 게이트웨이 가용성을 높여놔야 안정적일 것이다.


참고 링크

https://zayson.tistory.com/entry/Spring-Cloud-Gateway%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%9D%BC%EC%9A%B0%ED%8C%85-%EB%B0%8F-JWT-%ED%86%A0%ED%81%B0-%EA%B2%80%EC%A6%9D

https://llshl.tistory.com/32

소스 코드 출처

https://velog.io/@bum12ark/MSA-JWT-%EC%9D%B8%EC%A6%9D-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-1.-%EB%A1%9C%EA%B7%B8%EC%9D%B8


profile
아키텍쳐 설계와 테스트 코드에 관심이 많음.

0개의 댓글