기존 JWT를 사용한 회원관리 방법은 여기를 참조 하자.
위 포스팅과는 다르게 이번글은 하나의 프로젝트에서 회원 인증인가를 진행하지 않고 다른 프로젝트에서 인증인가를 거친 후 여러 서비스를 이용하도록 해 볼 것이다.
이렇게 스프링 클라우드, 게이트 웨이를 이용해 인증인가를 진행하게 되면
많은 서비스를 사용해도 한군데에서 인증인가를 진행할 수 있기 때문에 마이크로 서비스 구현이 가능해진다.
단계별로 하나씩 구현 해 보자
언제나 그랬듯 build.gradle에 먼저 라이브러리를 받아준다.
주요 implemention은 아래 6개 되시겠다.
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation group: 'org.glassfish.jaxb', name: 'jaxb-runtime', version: '2.3.2'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
스프링 클라우드와 JWT, 우리의 친구 롬복을 설치 해 준다.
들어온 요청의 JWT를 Parsing하고 검증한다.
역시 유효한 JWT가 아니라면 예외처리를 해 준다.
@Component
public class JwtTokenProvider {
@Value("${app.security.jwtSecret}")
private String jwtSecret;
public Claims getUserFromJWT(String token) {
try {
return Jwts.parser().setSigningKey(jwtSecret)
.parseClaimsJws(token).getBody();
} catch (Exception e) {
throw new UnauthorizedException(e.getMessage());
}
}
public boolean verifyJWT(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (Exception e) {
throw new UnauthorizedException(e.getMessage());
}
}
}
이제 스프링 게이트웨이로 요청하는 API를 검사해 인증인가를 진행할 수 있도록 도와주는 필터를 작성한다.
@Component
public class AuthorizationHeaderFilter extends
AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
private final JwtTokenProvider jwtTokenProvider;
private final String HEADER_USER_ID = "userId";
@Autowired
private UserRepository userRepository;
public AuthorizationHeaderFilter(JwtTokenProvider jwtTokenProvider) {
super(Config.class);
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String jwt = getJwtFromRequest(request);
try {
if (StringUtils.hasText(jwt) && jwtTokenProvider.verifyJWT(jwt)) {
addAuthorizationHeaders(exchange.getRequest(), jwt);
return chain.filter(exchange);
}
} catch (Exception e) {
throw new UnauthorizedException(e.getMessage());
}
throw new UnauthorizedException();
};
}
private String getJwtFromRequest(ServerHttpRequest request) {
if (containsAuthorization(request)) {
String bearerToken = extractToken(request);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
}
throw new UnauthorizedException("헤더에 토큰이 존재하지않음");
}
// 검증 후 인증된 헤더로 요청을 변경
private void addAuthorizationHeaders(ServerHttpRequest request, String jwt) {
Claims users = jwtTokenProvider.getUserFromJWT(jwt);
request.mutate()
.headers(httpHeader -> httpHeader.setAll(usersConvertToString(users))).build();
}
private boolean containsAuthorization(ServerHttpRequest request) {
return request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION);
}
private String extractToken(ServerHttpRequest request) {
return request.getHeaders().getOrEmpty(HttpHeaders.AUTHORIZATION).get(0);
}
private Map<String, String> usersConvertToString(Claims claims) {
try {
ObjectMapper objectMapper = new ObjectMapper();
if (!claims.containsKey(HEADER_USER_ID) || ObjectUtils
.isEmpty(claims.get(HEADER_USER_ID))) {
throw new UnauthorizedException("토큰 user가 유효하지않음 ");
}
String userId = (String) claims.get(HEADER_USER_ID);
Map<String, String> headers = new HashMap<>();
if (userRepository.findByUserId(userId).isPresent()) {
User user = userRepository.findByUserId(userId).get();
String userJson = objectMapper.writeValueAsString(user);
headers.put("userJson", userJson);
} else {
throw new GateWayUserNotFoundException("토큰 userId 값이 유효하지 않음.");
}
return headers;
} catch (Exception e) {
throw new UnauthorizedException(e.getMessage());
}
}
public static class Config {
}
}
API 요청 헤더에 들어있는 userId를 통해 JWT로 인증인가를 진행 할 것이다.
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String jwt = getJwtFromRequest(request);
try {
if (StringUtils.hasText(jwt) && jwtTokenProvider.verifyJWT(jwt)) {
addAuthorizationHeaders(exchange.getRequest(), jwt);
return chain.filter(exchange);
}
} catch (Exception e) {
throw new UnauthorizedException(e.getMessage());
}
throw new UnauthorizedException();
};
}
private String getJwtFromRequest(ServerHttpRequest request) {
if (containsAuthorization(request)) {
String bearerToken = extractToken(request);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
}
throw new UnauthorizedException("헤더에 토큰이 존재하지않음");
}
private void addAuthorizationHeaders(ServerHttpRequest request, String jwt) {
Claims users = jwtTokenProvider.getUserFromJWT(jwt);
request.mutate()
.headers(httpHeader -> httpHeader.setAll(usersConvertToString(users))).build();
}
게이트웨이 필터의 기본 설정이다. request객체해서 JWT를 찾아 getJwtFromRequest
를 통해 extractToken
로 토큰을 추출하고 JWT 부분을 찾아준다.
api 요청 헤더에 jwt가 올바르게 들어가 있다면 addAuthorizationHeaders
를 통해 인증된 헤더로 요청을 변경 해 준다.
addAuthorizationHeaders
에서는 usersConvertToString
를 통해 claims의 userId를 찾아 user를 판별하고, 유효한 유저가 아니라면 예외처리를 해 준다.
이 때 userRepository
에는 userId로 회원을 조회하는 부분을 미리 만들어둬야 한다.
이미 존재하는 유저, 즉 유효한 유저라면 요청 헤더에 유저 정보를 넣어준다.
스프링 게이트웨이에서 가장 중요한 yml설정이다. cloud부분에 여러 서비스를 등록하고 api를 다른 서비스에 연결 해 주며, 클라우드 세팅을 진행할 수 있다.
app.security.jwtSecret: SecretKey
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/dbName?characterEncoding=UTF-8&serverTimezone=Asia/Seoul
username: root
password: Admin
driverClassName: com.mysql.cj.jdbc.Driver
jpa:
show-sql: false
open-in-view: false
properties.hibernate:
format_sql: false
hibernate:
ddl-auto: none
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: 'http://localhost:3000'
allowedMethods: '*'
allowedHeaders: '*'
allowCredentials: true
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE
routes:
- id: admin-api
uri: http://localhost:8090/
predicates:
- Path=/admin/api/**
filters:
- name: CustomLogFilter
args:
baseMessage: Spring Cloud Gateway
기본 JPA 설정은 넘어가고 cloud 부분을 살펴보자
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: 'http://localhost:3000'
allowedMethods: '*'
allowedHeaders: '*'
allowCredentials: true
allowedOrigins
에는 들어오는 요청의 도메인을 작성 해 준다.
allowedMethods
에는 들어오는 요청의 Http Method를, Header또한 모두 받아줄 것이므로 * 를 작성한다.
allowCredentials
로 쿠키요청을 허용한다. 이 부분은 CORS 설정에도 영향을 준다.
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE
Access-Control-Allow-Origin을 허용해준다. CORS에러가 나지 않도록..
routes:
- id: admin-api
uri: http://localhost:8090/
predicates:
- Path=/admin/api/**
filters:
- name: CustomLogFilter
args:
baseMessage: Spring Cloud Gateway AdminApiFilter
스프링 게이트웨이에서 route부분에는
등등을 설정 해 준다.
이제 route에 정의된 url로 들어오는 모든 요청을 스프링 게이트웨이가 받아 다른 서비스로 연결 해 줄 준비가 되었다.
다음 포스팅에서는 개별 서비스에서 어떻게 설정하는지 알아보도록 하겠다.