참고 (개념이 복잡해서 빠르게 넘어간다고 했다...) 용어, 역할, 부여 방식 등
리소스 소유자(Resource Owner)
OAuth 2.0 프로토콜을 사용하여 보호되는 리소스에 대한 액세스 권한을 부여하는 사용자(엔티티)입니다. 클라이언트를 인증(Authorize)하는 역할을 수행합니다. 예를 들어 네이버 로그인에서 네이버 아이디를 소유하고 third-party 애플리케이션(클라이언트)에 네이버 아이디로 소셜 로그인 인증을 하는 사용자를 의미합니다.
클라이언트(Client)
OAuth 2.0을 사용하여 리소스에 접근하려는 third-party 애플리케이션이나 서비스입니다.
권한 서버(Authorization Server)
권한 서버는 클라이언트가 리소스 소유자의 권한을 얻을 수 있도록 도와주는 서버입니다. 권한 서버는 사용자 인증, 권한 부여 및 토큰 발급을 관리합니다.
리소스 서버(Resource Server)
리소스 서버는 보호되는 리소스를 호스팅하는 서버로, 액세스를 허용하거나 거부합니다. 이 서버는 OAuth 2.0 토큰을 사용하여 클라이언트에게 리소스에 액세스할 권한을 부여하고 실제 데이터를 제공합니다.
데이터가 전송 중에 변조가 되지는 않았는지 확인하는 것이 중요하다.
페이로드 등의 데이터 자체의 보안이 중요한 것은 아니다.
❗ 이번 강의에서는 실습을 통해 클라우드 게이트웨이의 Pre 필터에서 JWT 인증을 진행해봅니다.
우선 “스프링 클라우드 게이트웨이”에서 학습한 모든 프로젝트를 복사하여 사용하겠습니다.
여기에 Auth Service 를 생성하여 로그인 기능을 아주 간단하게 구현하겠습니다.
클라우드 게이트웨이에 Pre 필터를 하나 더 생성하여 로그인을 체크 하겠습니다.
PostMan 또는 크롬 익스텐션 중 Talend API Tester를 설치합니다.
강의에서는 Talend를 사용하겠습니다.
PostMan : https://www.postman.com/
Talend : https://chromewebstore.google.com/detail/talend-api-tester-free-ed/aejoelaoggembcahagimdiliamlcdmfm?hl=ko

이미지의 점선만 실행하여 확인하겠습니다.
1) 사용자가 gateway로 요청 시 auth에서 토큰 생성 후 사용자에게 부여
2) 사용자는 토큰을 가진 채로 product 호출
3) gateway에서 토큰 체크 후 product 호출
3-1) 토큰 체크를 auth에서 확인할 수도 있다 (권한 확인을 위해 gateway에서 auth로 보냄)
3-2) 권한 체크만 각 app이 auth를 호출하여 체크할 수도 있다

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'
}spring:
application:
name: auth-service
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/
service:
jwt:
access-expiration: 3600000
secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"
# 실제로는 노출되면 안 되므로 이렇게 사용하지 않는다
server:
port: 19095
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();
}
}
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}")
private String issuer;
// 토큰 만료 시간
@Value("${service.jwt.access-expiration}")
private Long accessExpiration;
//SecretKey 타입의 객체를 아래 함수에서 디코딩하여 생성
private final SecretKey secretKey;
/**
* AuthService 생성자.
* Base64 URL 인코딩된 비밀 키를 디코딩하여 HMAC-SHA 알고리즘에 적합한 SecretKey 객체를 생성합니다.
*
* @param secretKey Base64 URL 인코딩된 비밀 키
*/
public AuthService(@Value("${service.jwt.secret-key}") String secretKey) {
this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
}
/**
* 사용자 ID를 받아 JWT 액세스 토큰을 생성합니다.
*
* @param user_id 사용자 ID
* @return 생성된 JWT 액세스 토큰
*/
public String createAccessToken(String user_id) {
return Jwts.builder()
// 사용자 ID를 클레임으로 설정
.claim("user_id", user_id)
.claim("role", "ADMIN")
// JWT 발행자를 설정
.issuer(issuer)
// JWT 발행 시간을 현재 시간으로 설정
.issuedAt(new Date(System.currentTimeMillis()))
// JWT 만료 시간을 설정
.expiration(new Date(System.currentTimeMillis() + accessExpiration))
// SecretKey를 사용하여 HMAC-SHA512 알고리즘으로 서명
.signWith(secretKey, io.jsonwebtoken.SignatureAlgorithm.HS512)
// JWT 문자열로 컴팩트하게 변환
.compact();
}
}
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;
/**
* 사용자 ID를 받아 JWT 액세스 토큰을 생성하여 응답합니다.
*
* @param user_id 사용자 ID
* @return JWT 액세스 토큰을 포함한 AuthResponse 객체를 반환합니다.
*/
@GetMapping("/auth/signIn")
// 어떤 타입이건 상관 없이 ResponseEntity로 리턴
public ResponseEntity<?> createAuthenticationToken(@RequestParam String user_id){
// user_id를 가지고 아래 함수로 액세스 토큰 생성 후 response
return ResponseEntity.ok(new AuthResponse(authService.createAccessToken(user_id)));
}
/**
* JWT 액세스 토큰을 포함하는 응답 객체입니다.
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
static class AuthResponse {
private String access_token;
}
}

실행 시 웹에 토큰 값을 띄우는 것을 알 수 있다.
위의 사이트에 토큰 값을 넣으면 payload에서 우리가 넘긴 데이터를 알 수 있다.
build.gradle 파일에 필요한 의존성을 추가합니다. ( jwt 추가 )dependencies {
**implementation 'io.jsonwebtoken:jjwt:0.12.6'**
implementation 'org.springframework.boot:spring-boot-starter-actuator'
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'
}server:
port: 19091 # 게이트웨이 서비스가 실행될 포트 번호
spring:
main:
web-application-type: reactive # Spring 애플리케이션이 리액티브 웹 애플리케이션으로 설정됨
application:
name: gateway-service # 애플리케이션 이름을 'gateway-service'로 설정
cloud:
gateway:
routes: # Spring Cloud Gateway의 라우팅 설정
- id: order-service # 라우트 식별자
uri: lb://order-service # 'order-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/order/** # /order/** 경로로 들어오는 요청을 이 라우트로 처리
- id: product-service # 라우트 식별자
uri: lb://product-service # 'product-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/product/** # /product/** 경로로 들어오는 요청을 이 라우트로 처리
- id: auth-service # 라우트 식별자
uri: lb://auth-service # 'auth-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
predicates:
- Path=/auth/signIn # /auth/signIn 경로로 들어오는 요청을 이 라우트로 처리
discovery:
locator:
enabled: true # 서비스 디스커버리를 통해 동적으로 라우트를 생성하도록 설정
eureka:
client:
service-url:
defaultZone: http://localhost:19090/eureka/ # Eureka 서버의 URL을 지정
service:
jwt:
secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"
헤더에 Authentication | Bearer 토큰값 이 담겨서 로그인 체크하게 된다.
ServerWebExchange 참고
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;
@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();
// /signIn 경로는 필터를 적용하지 않고 다음 필터로 넘김
if (path.equals("/auth/signIn")) {
return chain.filter(exchange);
}
String token = extractToken(exchange);
// 토큰이 없거나 유효하지 않을 경우
if (token == null || !validateToken(token)) {
// 인증되지 않음 (401) 코드 전달
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()); // payload 값 확인
// 추가적인 검증 로직 (예: 토큰 만료 여부 확인 등)을 여기에 추가할 수 있습니다.
return true;
} catch (Exception e) {
return false;
}
}
}
gateway를 통하지 않은 접근은 로그인 되지 않은 사용도 접근이 가능하다 (방화벽 처리를 통해 막아야 한다)

Bearer 란?
- Bearer는 OAuth 2.0 프로토콜에서 사용하는 인증 토큰 유형 중 하나로, 액세스 토큰을 통해 보호된 리소스에 접근할 수 있도록 합니다. Bearer 토큰은 요청 헤더에 포함되어 서버에 전달되며, 서버는 이를 검증하여 요청이 유효한지 확인합니다.
- 간단한 사용법: 클라이언트는 서버에서 받은 Bearer 토큰을 HTTP 요청 헤더에 포함시키기만 하면 됩니다.
- 서버 측 검증: 서버는 이 토큰을 검증하여 요청이 인증된 사용자의 요청인지 확인합니다. 일반적으로 토큰의 유효성, 만료 시간 등을 확인합니다.
- 보안: Bearer 토큰은 HTTPS를 통해 전달되어야 합니다. 이를 통해 토큰이 전송 중에 도난당하지 않도록 보호할 수 있습니다
