[MSA알아보기] 보안구성(OAuth2 + JWT)

차차의 개발일기·2024년 8월 8일
0

msa

목록 보기
7/7

1. 보안

1.1 중요성

마이크로서비스 아키텍처에서는 각 서비스가 독립적으로 배포되고 통신하기 때문에 데이터 보호, 인증 및 권한 부여, 통신 암호화 등에 관한 시스템 보안성을 확보해야 합니다.

1.2 사용할 기술

보안을 확보하기 위해 OAuth2 + JWT를 사용하여 권한 부여 및 토큰을 통한 정보 전달을 하곘습니다.

2. OAuth2

2.1 OAuth2란?

토큰 기반의 인증 및 권한 부여 프로토콜입니다. 클라이언트가 권한을 얻어 보호된 리소스에 접근할 수 있도록 도와줍니다. 이 방식에는 네가지 역할(리소스 소유자, 클라이언트, 리로스 서버, 인증 서버)을 정의합니다.

2.2 주요 개념

  • Authorization Code Grant: 인증 코드를 사용하여 액세스 토큰을 얻는 방식
  • Implicit Grant: 클라이언트 애플리케이션에서 직접 액세스 토큰을 얻는 방식
  • Resource Owner Password Credentials Grant: 사용자 이름과 비밀번호를 사용하여 액세스 토큰을 얻는 방식
  • Client Credentials Grant: 클라이언트 애플리케이션이 자신의 자격 증명을 사용하여 액세스 토큰을 얻는 방식

3. JWT

3.1 JWT란?

JWT(Json Web Token)는 JSON 형식의 자가 포함된 토큰으로, 클레임(claim)을 포함하여 사용자에 대한 정보를 전달합니다. 또한, 세 부분(헤더, 페이로드, 서명)으로 구성됩니다. 이렇게 구성된 JWT는 암호화를 통해 데이터의 무결성과 출처를 보장합니다.

3.2 특징

  • 자가 포함: 토큰 자체에 모든 정보를 포함하고 있어 별도의 상태 저장이 필요 없습니다.
  • 간결성: 짧고 간결한 문자열로, URL, 헤더 등에 쉽게 포함될 수 있습니다.
  • 서명 및 암호화: 데이터의 무결성과 인증을 보장합니다.

4. 구현

4.1 Auth Service

로그인을 담당하는 어플리케이션을 생성합니다. 로그인을 진행하면 토큰을 발급받고 이 토큰을 사용하여 Gateway를 호출합니다.

4.1.1 프로젝트 생성


위와 같은 디펜던시를 생성하고 프로젝트를 생성합니다.

4.1.2 build.gradle 수정

JWT를 사용하기 위해 JWT관련 디펜던시를 추가합니다.

implementation 'io.jsonwebtoken:jjwt:0.12.6'

4.1.3 yml 작성

spring:
  application:
    name: auth-service

eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka


service:
  jwt:
    access-expiration: 3600000
    secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"

4.1.4 config 작성

@Configuration
@EnableWebSecurity
public class AuthConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .authorizeRequests(
                        authorize -> authorize
                                .requestMatchers("/auth/signIn").permitAll()
                                .anyRequest().authenticated()
                )
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }
}

4.1.5 service 작성

@Service
public class AuthService {
    @Value("${spring.application.name}")
    private String issuer;

    @Value("${service.jwt.access-expiration}")
    private Long accessExpiration;

    private final SecretKey secretKey;

    public AuthService(@Value("${service.jwt.secret-key}") String secretKey) {
        this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
    }

    public String createAccessToken(String user_id) {
        return Jwts.builder()
                .claim("userId", user_id)
                .claim("role", "ADMIN")
                .issuer(issuer)
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + accessExpiration))
                .compact();
    }
}

4.1.6 controller 작성

@RestController
@RequiredArgsConstructor
public class AuthController {
    private final AuthService authService;

    @GetMapping("/auth/signIn")
    public ResponseEntity<?> createAuthenticationToken(@RequestParam String user_id) {
        return ResponseEntity.ok(new AuthResponse(authService.createAccessToken(user_id)));
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class AuthResponse {
        private String access_token;

    }
}

4.2 Cloud Gateway 구성

기존 게이트웨이에 auth-service 라우팅 정보를 추가합니다.

4.2.1 build.gradle

jwt 의존성을 추가합니다.

dependencies {
	implementation 'io.jsonwebtoken:jjwt:0.12.6'
	implementation 'org.springframework.boot:spring-boot-starter-actuator'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'io.projectreactor:reactor-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

4.2.2 yml 수정

server:
  port: 19091  # 게이트웨이 서비스가 실행될 포트 번호

spring:
  main:
    web-application-type: reactive
  application:
    name: gateway-service
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/order/**
        - id: product-service
          uri: lb://product-service 
          predicates:
            - Path=/product/** 
        - id: auth-service
          uri: lb://auth-service
          predicates:
            - Path=/auth/signIn
      discovery:
        locator:
          enabled: true 

eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/ 
      

service:
  jwt:
    secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"
    

auth-service, jwt secret-key를 추가해줍니다.

4.2.3 filter 추가

@Slf4j
@Component
public class LocalJwtAuthenticationFilter implements GlobalFilter {

    @Value("${service.jwt.secret-key}")
    private String secretKey;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();
        if (path.equals("/auth/signIn")) {
            return chain.filter(exchange);  // /signIn 경로는 필터를 적용하지 않음
        }

        String token = extractToken(exchange);

        if (token == null || !validateToken(token)) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        return chain.filter(exchange);
    }

    private String extractToken(ServerWebExchange exchange) {
        String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7);
        }
        return null;
    }

    private boolean validateToken(String token) {
        try {
            SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
            Jws<Claims> claimsJws = Jwts.parser()
                    .verifyWith(key)
                    .build().parseSignedClaims(token);
            log.info("#####payload :: " + claimsJws.getPayload().toString());

            // 추가적인 검증 로직 (예: 토큰 만료 여부 확인 등)을 여기에 추가할 수 있습니다.
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

4.3 실행

eureka server ⇒ gateway ⇒ auth ⇒ product 순으로 어플리케이션을 실행합니다.

4.3.1 인스턴스 확인

http://localhost:19090에 접속하여 각 인스턴스를 확인합니다.

4.3.2 게이트웨이에 상품을 요청


권한이 없기때문에 401에러가 발생하는 것을 볼 수 있습니다.

4.3.3 로그인 요청

4.3.4 상품 요청 With Token

이렇게 auth의 인증을 통해 보안을 알아봤습니다.
이상으로 보안구성 구현을 마무리 하겠습니다.


🔗Github Repository 링크

https://github.com/junghojune/MSA-Practice

profile
1년차 개발자 차차입니다.

0개의 댓글