1. OAuth2란 무엇인가?
- 토큰 기반의 인증 및 권한 부여 프로토콜
- 클라이언트 애플리케이션이 리소스 소유자의 권한을 얻어 보호된 리소스에 접근할 수 있도록 한다.
- 역할: 리소스 소유자, 클라이언트, 리소스 서버, 인증 서버
1) Authorization Code Grant: 인증 코드를 사용하여 엑세스 토큰을 얻는 방식
2) Implicit Grant: 클라이언트 애플리케이션에서 직접 액세스 토큰을 얻는 방식
3) Resource Owner Password Credentials Grant: 사용자 이름과 비밀번호를 사용하여 액세스 토큰을 얻는 방식
4) Client Credentials Grant: 클라이언트 애플리케이션이 자신의 자격 증명을 사용하여 액세스 토큰을 얻는 방식
2. JWT(JSON Web Token)란 무엇인가?
- JSON 형식의 자가 포함된 토큰으로, 클레임(claim)을 포함하여 사용자에 대한 정보를 전달합니다.
- JWT는 세 부분으로 구성됩니다: 헤더, 페이로드, 서명
- JWT는 암호화를 통해 데이터의 무결성, 인증과 출처를 보장합니다.
3. 구조

4. 구현
Auth 생성
build.gradle
dependencies {
implementation 'io.jsonwebtoken:jjwt:0.12.6'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
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 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation 'io.projectreactor:reactor-test'
}
application.yml
spring:
application:
name: auth-service
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/ # Eureka 서버의 URL을 지정
service:
jwt:
access-expiration: 3600000 # 1시간
secret-key: "key"
// 구현하기 위한 키 이므로 미제공
server:
port: 19095
AuthConfig.java
package com.springcloud.eureka.client.auth;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class AuthConfig {
// SecurityFilterChain 빈을 정의합니다. 이 메서드는 Spring Security의 보안 필터 체인을 구성합니다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// CSRF 보호를 비활성화합니다. CSRF 보호는 주로 브라우저 클라이언트를 대상으로 하는 공격을 방지하기 위해 사용됩니다.
.csrf(csrf -> csrf.disable())
// 요청에 대한 접근 권한을 설정합니다.
.authorizeRequests(authorize -> authorize
// /auth/signIn 경로에 대한 접근을 허용합니다. 이 경로는 인증 없이 접근할 수 있습니다.
.requestMatchers("/auth/signIn").permitAll()
// 그 외의 모든 요청은 인증이 필요합니다.
.anyRequest().authenticated()
)
// 세션 관리 정책을 정의합니다. 여기서는 세션을 사용하지 않도록 STATELESS로 설정합니다.
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
// 설정된 보안 필터 체인을 반환합니다.
return http.build();
}
}
AuthController.java
package com.springcloud.eureka.client.auth;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
@GetMapping("/auth/signIn")
public ResponseEntity<?> createAuthToken(@RequestParam String user_id){
// GetMapping 파라미터를 가져올때는 @RequestParam 사용)
return ResponseEntity.ok(new AuthResponse(authService.createAcessToken(user_id)));
}
@Data
@AllArgsConstructor // 모든 필드를 매개변수로 받는 생성자 자동 생성
@NoArgsConstructor // 매개변수가 없는 기본 생성자 자동 생성
static class AuthResponse{
private String access_token;
}
}
AuthService.java
package com.springcloud.eureka.client.auth;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.util.Date;
@Service
public class AuthService {
@Value("${spring.application.name}") // 토큰 발급한 APP
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 createAcessToken(String user_id){
return Jwts.builder()
.claim("user_id", user_id)
.claim("role", "ADMIN")
.issuer(issuer)
.issuedAt(new Date((System.currentTimeMillis())))
.expiration(new Date(System.currentTimeMillis()+ accessExpiration))
.signWith(secretKey, io.jsonwebtoken.SignatureAlgorithm.HS512)
.compact();
}
}
이후 중간 매개체 역할을 하는 API Gateway를 통해 인증, 권한부여를 해야하기 때문에 Gateway의 해당 부분을 추가
gateway/LocalJwtAuthenticationFilter.java
package com.springcloud.client.gateway;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.crypto.SecretKey;
@Component
@Slf4j
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;
}
}
}
먼저 Auth로 직접 요청(19095 포트)

access_token이 잘 나오는 것을 볼 수 있음.
다음으로, Gateway를 통해서 Auth로 요청할 수 있는지 확인

Gateway(19091)을 통해서 Auth 인증을 보내 access_token을 정상적으로 받아옴을 확인했다.
이번 프로젝트를 진행하면서 AuthConfig.java를 작성할 때 가장 생소했던 부분이 바로 아래의 Spring Security 설정 코드였습니다
@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();
}
기존 자바 코드처럼 한 줄씩 명령하는 게 아니라, 점(.)을 찍어 연결하는 메서드 체이닝과 괄호 안에 ->가 들어가는 람다식이 섞여 있어서 처음에는 문법을 이해하는 것조차 꽤나 까다로웠습니다.
이 복잡해 보이는 코드가 핵심적으로 무슨 일을 하는지 딱 3가지로 정리했습니다.
핵심 코드 3단계 요약
1. CSRF 보호 비활성화 (.csrf().disable())
- 이유: 원래 웹 브라우저 기반 서비스에서는 필수적인 보안 기능입니다. 하지만 우리 서버는 웹 브라우저(쿠키/세션)를 쓰지 않고 JWT 토큰으로만 통신하는 API 서버이기 때문에 이 기능을 안전하게 꺼둔 것입니다.
2. 요청 권한 설정 (.authorizeRequests())
- /auth/signIn -> permitAll(): 로그인을 해야 토큰을 발급받을 수 있으므로, 로그인 경로만큼은 토큰 없이 누구나 접근할 수 있도록 문을 열어둡니다.
- anyRequest() -> authenticated(): 로그인을 제외한 그 외의 모든 요청은 무조건 인증(로그인)을 거쳐야만 통과시킵니다.
3. 무상태 세션 정책 (.sessionManagement())
- SessionCreationPolicy.STATELESS: 서버 메모리에 사용자를 기록하는 '세션'을 절대 쓰지 않겠다고 선언하는 것입니다. 서버는 완벽하게 무상태(Stateless)를 유지하고, 오직 사용자가 보낸 JWT 토큰만 검사하여 신원을 확인합니다.
어려웠던 것들은 반복하고 반복하기
'MSA' 카테고리의 다른 글
| 분산 추적(Zipkin) & 이벤트 드리븐 (0) | 2026.06.29 |
|---|---|
| Config(Spring Cloud Config) (0) | 2026.06.29 |
| API 게이트웨이 (Spring Cloud Gateway) (0) | 2026.06.27 |
| 서킷 브레이커 (Resilience4j) (0) | 2026.06.27 |
| 로드 밸런싱 (0) | 2026.06.26 |