더 자세한 Gateway 내용은 여기 참고
URI Rule | 기능 | 토큰 필요 | Gateway 동작 | routing to |
---|---|---|---|---|
Path=/api/auth/** | 로그인, 비밀번호 관련 기능 | N | RestTemplate로 내부통신(devsta-user에서 받은 응답으로 JWT 생성 처리) | devsta-users |
Path=/api/user/profile/** | 내 프로필 정보, 수정 | Y | UserFilter에서 토큰 인증 후 라우팅 | devsta-users |
Path=/api/posts/** | 포스팅, 타임라인 관련 기능 | Y | UserFilter에서 토큰 인증 후 라우팅 | devsta-posts |
Path=/api/meetup/service/** | 밋업 관련 기능 중 로그인 필요한 기능 | Y | UserFilter에서 토큰 인증 후 라우팅 | devsta-meetup |
Path=/api/meetup/read/** | 밋업 관련 기능 중 로그인 필요 없는 기능 | N | uri rewrite만 하고 토큰 인증 없이 라우팅 | devsta-meetup |
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.3</version>
</dependency>
</dependencies>
RestTemplate
: HTTP get,post 요청을 날릴때 일정한 형식에 맞춰주는 template@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
var factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(30000);
factory.setReadTimeout(30000);
return new RestTemplate(factory);
}
}
@Component
@Slf4j
public class RestClient{
@Autowired
private RestTemplate restTemplate;
public String restTemplatePost(String serviceName, String endpoint, HashMap<String, ?> requestBody) {
try {
String serviceUrl = String.format("%s%s", serviceName, endpoint);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpEntity httpEntity = new HttpEntity(requestBody, httpHeaders);
log.info("restTemplate ->" + serviceUrl);
ResponseEntity<String> restExchange = restTemplate.exchange(serviceUrl, HttpMethod.POST, httpEntity, String.class, "");
log.info("restExchange -> " + restExchange);
log.info("body -> " + restExchange.getBody());
return restExchange.getBody();
} catch (Exception e) {
log.info(">>> " + e);
throw new CustomException(CommonCode.FAIL);
}
}
userUri
값을 application.yml 파일의 uri.user-service
의 값을 받아와서 사용하도록 구현했다.public static final
로 선언했다.authService
의 parseResponseWrapper
로 파싱
@Slf4j
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final RestClient restClient;
private final AuthService authService;
public static final String SIGN_IN = "signIn";
public static final String SIGN_UP = "signUp";
public static final String CHANGE_PW = "changePw";
@Value("${uri.user-service}")
private String userUri = "";
public AuthController(RestClient restClient, JwtUtils jwtUtils, AuthService authService) {
this.restClient = restClient;
this.authService = authService;
}
@PostMapping("/signIn")
public CommonResponse signIn(@RequestBody HashMap<String, String> requestBody) {
HashMap responseEntity;
String response = restClient.restTemplatePost(userUri, "/auth/signIn", requestBody);
return authService.parseResponseWrapper(response, SIGN_IN);
}
@PostMapping("/signUp")
public CommonResponse signUp(@RequestBody HashMap<String, Object> requestBody) {
HashMap responseEntity;
String response = restClient.restTemplatePost(userUri, "/auth/signUp", requestBody);
return authService.parseResponseWrapper(response, SIGN_UP);
}
@PostMapping("/changePW")
public CommonResponse changePw(@RequestBody HashMap<String, Object> requestBody) {
HashMap responseEntity;
String response = restClient.restTemplatePost(userUri, "/auth/changePW", requestBody);
return authService.parseResponseWrapper(response, CHANGE_PW);
}
}
parseResponseWrapper
구현parseSignInSuccess
: 응답에서 attribute에 담아서 보내준 id, email 값을 담아 JWT 토큰으로 Encoding, JWT 토큰을 CommonResponse에 담아 리턴@Slf4j
@Service
@AllArgsConstructor
public class AuthService {
private final Gson gson = new Gson();
private final JwtUtils jwtUtils;
public CommonResponse parseResponseWrapper(String response, String uri) {
HashMap responseEntity;
try {
responseEntity = gson.fromJson(response, HashMap.class);
Double codeDouble = (Double) responseEntity.get("code");
int code = codeDouble.intValue();
switch (code) {
case 200:
switch (uri) {
case SIGN_IN: return parseSignInSuccess(responseEntity);
case SIGN_UP: return parseSignUpSuccess(responseEntity);
default: return parseChangePwSuccess(responseEntity);
}
default:
return new CommonResponse(CommonCode.of((code)));
}
} catch (Exception e) {
log.info(">>> " + e);
return new CommonResponse(CommonCode.FAIL, Map.of("message", e.getMessage()));
}
}
private CommonResponse parseSignInSuccess(HashMap responseEntity) {
LinkedTreeMap attribute = (LinkedTreeMap) responseEntity.get("attribute");
String id = (String) attribute.get("id");
String email = (String) attribute.get("email");
String token = jwtUtils.generate(new TokenUser(id, email));
return new CommonResponse(CommonCode.SUCCESS, Map.of("Authorization", token));
}
private CommonResponse parseSignUpSuccess(HashMap responseEntity) {
LinkedTreeMap attribute = (LinkedTreeMap) responseEntity.get("attribute");
return new CommonResponse(CommonCode.SUCCESS, attribute);
}
private CommonResponse parseChangePwSuccess(HashMap responseEntity) {
return new CommonResponse(CommonCode.SUCCESS);
}
}
spring:
cloud:
gateway:
routes:
- id: meetup-read
uri: ${uri.meetup-service}
predicates:
- Path=/api/meetup/read/**
filters:
- RewritePath=/api/meetup/(?<path>.*),/$\{path}
UserFilter
: Header에 Authorization Token을 Deconding하여 인증/인가spring:
cloud:
gateway:
routes:
- id: user-service
uri: ${uri.user-service}
predicates:
- Path=/api/user/profile/**
filters:
- RewritePath=/api/user/profile/(?<path>.*),/$\{path}
- UserJwtFilter
- id: post-service
uri: ${uri.post-service}
predicates:
- Path=/api/posts/**
filters:
- RewritePath=/api/posts/(?<path>.*),/$\{path}
- UserJwtFilter
- id: meetup-service
uri: ${uri.meetup-service}
predicates:
- Path=/api/meetup/service/**
filters:
- RewritePath=/api/meetup/(?<path>.*),/$\{path}
- UserJwtFilter
@Component
@Slf4j
public class UserJwtFilter extends AbstractGatewayFilterFactory<UserJwtFilter.Config> {
private static final String USER_ID = "userId";
private static final String EMAIL = "email";
private final JwtUtils jwtUtils;
public UserJwtFilter(JwtUtils jwtUtils) {
super(Config.class);
this.jwtUtils = jwtUtils;
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
if (!containsAuthorization(request)) {
return onError(response, "헤더에 Authorization 토큰이 없습니다.", HttpStatus.BAD_REQUEST);
}
String token = extractToken(request);
if (!jwtUtils.isValid(token)) {
return onError(response, "Authorization 토큰이 유효하지 않습니다.", HttpStatus.BAD_REQUEST);
}
TokenUser tokenUser = jwtUtils.decode(token);
addAuthorizationHeaders(request, tokenUser);
return chain.filter(exchange);
};
}
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 void addAuthorizationHeaders(ServerHttpRequest request, TokenUser tokenUser) {
request.mutate()
.header(USER_ID, tokenUser.getId())
.header(EMAIL, tokenUser.getEmail())
.build();
}
private Mono<Void> onError(ServerHttpResponse response, String message, HttpStatus status) {
response.setStatusCode(status);
DataBuffer buffer = response.bufferFactory().wrap(message.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
}
@Component
@RequiredArgsConstructor
public class JwtUtils implements InitializingBean {
private static final String EMAIL = "email";
private final JwtProperties jwtProperties;
private Algorithm algorithm;
private JWTVerifier jwtVerifier;
@Override
public void afterPropertiesSet() {
this.algorithm = Algorithm.HMAC512(jwtProperties.getSecret());
this.jwtVerifier = JWT.require(algorithm).acceptLeeway(5).build();
}
public boolean isValid(String token) {
try {
jwtVerifier.verify(token);
return true;
} catch (RuntimeException e){
return false;
}
}
public TokenUser decode(String token) {
jwtVerifier.verify(token);
DecodedJWT jwt = JWT.decode(token);
String id = jwt.getSubject();
String email = jwt.getClaim(EMAIL).asString();
return new TokenUser(id, email);
}
public String generate(TokenUser user) {
Date now = new Date();
Date expiresAt = new Date(now.getTime() + jwtProperties.getExpirationSecond() * 1000);
return JWT.create()
.withSubject(user.getId())
.withClaim(EMAIL, user.getEmail())
.withExpiresAt(expiresAt)
.withIssuedAt(now)
.sign(algorithm);
}
}
jwt:
secret: [SECRET KEY]
expiration-second: 172800 #48시간