Spring Security, Spring Cloud, JWT, MSA
JWT + Spring Security + Spring Cloud = MSA 2. Spring Security์ ์ด์ด์ ApiGateway๋ฅผ ์ฌ์ฉํด๋ณด์
ApiGateway๋ Load Balancer๋ผ๊ณ ๋ ํ๋ฉฐ ํด๋ผ์ด์ธํธ์ request๊ฐ ๋ค์ด์ค๋ฉด ์ค์ ํด ๋์ ๋ผ์ฐํ ์ค์ ์ ๋ฐ๋ผ์ ๊ฐ๊ฐ์ endpoint๋ก ํด๋ผ์ด์ธํธ ๋์ ์ ์์ฒญ์ ๋ณด๋ด๊ณ ์๋ต์ ๋ฐ์ ํด๋ผ์ด์ธํธ์๊ฒ ์ ๋ฌํ๋ Proxy์ญํ ์ ํ๋ค
1. application.yml
server:
port: 8090
eureka:
client:
fetch-registry: true
register-with-eureka: true
service-url:
defaultZone: http://localhost:8761/eureka
spring:
application:
name: apigateway-service
redis:
host: localhost
port: 6379
cloud:
gateway:
default-filters:
- name: GlobalFilter
args:
baseMessage: Spring Cloud Gateway Global Filter
preLogger: true
postLogger: true
routes:
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/
filters:
- AuthorizationHeaderFilter
...
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/tested #requestํ controller
- Method=GET
filters:
- AuthorizationAdminFilter # ๊ด๋ฆฌ์๋ง ์ ๊ทผ๊ฐ๋ฅํ๋ค
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/jwt_check
- Method=GET
filters:
- AuthorizationFilter #ํ์๋ง ์ ์ ๊ฐ๋ฅํ๋ค
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/membertest
- Method=GET
filters:
- AuthorizationHeaderFilter #ํ์ ๋นํ์ ๋๋ค ์ ์ํ ์์์ง๋ง ์ฌ์ฉํ ์ ์๋ ๊ธฐ๋ฅ๋ฒ์๊ฐ ๋ค๋ฆ
...
# ํํฐ๋ฅผ ํตํด ๊ฐ ์๋น์ค ์๋ฒ๋ ๋น์ง๋์ค ๋ก์ง์๋ง ์ ๊ฒฝ์ฐ๋ฉด ๋จ
token:
expiration_time: 86400000
secret: user_token
- routes์์ id, uri๋ Service Discovery์ ๋ฑ๋กํด๋์ ๊ฐ์ ํตํด ํด๋ผ์ด์ธํธ์ request๊ฐ ์ด๋ endpoint๋ก ๊ฐ์ง ์ ํด์ค๋ค
- endpoint์ ๋ชจ๋ request๊ฐ ์ ์ก๋๋ ๊ฒ์ด ์๋๋ผ filter๋ฅผ ํตํด request์ ๊ฐ์ rewriteํด์ค๋ค
์ฌ๊ธฐ์๋ ์ด3๊ฐ์ง์ ํํฐ๋ฅผ ๊ฐ๋ฐํ๋ค
AuthorizationHeaderFilter: ํ์ ๋นํ์ ๋๋ค ์ ์ํ ์์์ง๋ง ์ฌ์ฉํ ์ ์๋ ๊ธฐ๋ฅ๋ฒ์๊ฐ ๋ค๋ฆ
AuthorizationFilter: ํ์๋ง ์ ์ ๊ฐ๋ฅํ๋ค
AuthorizationAdminFilter: ๊ด๋ฆฌ์๋ง ์ ๊ทผ๊ฐ๋ฅํ๋ค
2. Filter
- ๋ชจ๋ ํํฐ๋
AbstractGatewayFilterFactory
๋ฅผ ์์๋ฐ์apply()
๋ฅผ ๊ตฌํํ์ฌ ํํฐ๋ง ์๋น์ค๋ฅผ ๊ฐ๋ฐํ๋ค isJwtValid()
๋ฅผ ํตํด ํ ํฐ์ ๊ฒ์ฆํ๋ค2.1 AuthorizationHeaderFilter
- ํ์๊ณผ ๋นํ์ ํํฐ
- ํ์๊ณผ ๋นํ์ ๋ชจ๋๊ฐ ์ฌ์ฉํ ์ ์๋ ์๋น์ค์ผ ๊ฒฝ์ฐ ์ด ํํฐ๋ฅผ ํ๊ฒ ๋๋ค
- ๋นํ์์ ์๋น์ค ์ด์ฉ์ด ๊ฐ๋ฅํ์ง๋ง request header์ userId๊ฐ์ ๋ฃ์ด์ฃผ์ง์๋๋ค
- ํ์์ผ ๊ฒฝ์ฐ header์ userId๋ฅผ ๋ฃ์ด์ค๋ค. ์ด๋ฅผ ํตํด Controller๋ userId๋ฅผ ์์ฝ๊ฒ ํ์ธํ ์ ์๋ค
@Component
@Slf4j
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
Environment env;
private final RedisService redisService;
@Autowired
public AuthorizationHeaderFilter(Environment env, RedisService redisService) {
super(Config.class);
this.env = env;
this.redisService = redisService;
}
public static class Config {
// Put configuration properties here
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, HttpCookie> cookies = request.getCookies();
String jwt = "";
try {
jwt = cookies.get("token").get(0).getValue();
}
catch (NullPointerException e) {
return chain.filter(exchange);
}
if (redisService.isAccessTokenStored(jwt) || !isJwtValid(jwt)) {
log.info("ํ ํฐ({})์ ์ฌ์ฉ๋ถ๊ฐ ํ ํฐ์
๋๋ค.", jwt);
return chain.filter(exchange);
}
//์ฌ์ฉ์ ์์ด๋(pk)๊ฐ ๊ฐ์ ธ์ค๊ธฐ
String id = Jwts.parser().setSigningKey(env.getProperty("token.secret"))
.parseClaimsJws(jwt).getBody()
.getSubject();
request.mutate()
.header("userId", id)
.header("token", jwt)
.build();
ServerWebExchange exchange1 = exchange.mutate().request(request).build();
log.info("exchange1 {}", id);
log.info("ํ ํฐ์ ๋ณด : {}", jwt);
return chain.filter(exchange1);
};
}
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
log.error(err);
return response.setComplete();
}
private boolean isJwtValid(String jwt) {
boolean returnValue = true;
String subject = null;
try {
subject = Jwts.parser().setSigningKey(env.getProperty("token.secret"))
.parseClaimsJws(jwt).getBody()
.getSubject();
} catch (Exception ex) {
returnValue = false;
}
if (subject == null || subject.isEmpty()) {
returnValue = false;
}
log.info("subject {}", subject);
return returnValue;
}
}
redis๋ jwtํ ํฐ์ด ๋ง๋ฃ๋์ด์๋์ง ํ์ธ์ ์ํ์ฌ ์ฌ์ฉํ๋ค. ๋ก๊ทธ์์์ ํ ์ ํ ํฐ์ด redis์ ์ ์ฅ๋๋๋ฐ ํํฐ์์ ํด๋น ํ ํฐ์ด redis์ ์๋์ง ์๋์ง ๊ฒ์ฌํ๊ณ ์๋ค๋ฉด ๋ง๋ฃํ ํฐ์ด๊ธฐ๋๋ฌธ์ ์ ์ํ์ง๋ชปํ๋ค.
redis
- ๋ก๊ทธ์์์ ํ ๊ฒฝ์ฐ user-service ์์
logout()
๋ก์ง์ ํ์ด๋ค. user-service์ ๋ํ์ฌ๋ JWT + Spring Security + Spring Cloud = MSA 2. Spring Security์์ ์ค๋ช ํ์๋ค.@Transactional public void logout(String token) { Date expiration = jwtTokenProvider.getExpiredTime(token); redisService.setAccessToken(token, expiration); // ํ ํฐ์ ์ ์ฅ }
์์ฐธ, RedisService.class๋ ์๋์ฝ๋
@Slf4j
@Component
@RequiredArgsConstructor
public class RedisService {
private final RedisTemplate<String, String> redisTemplate;
public void setAccessToken(String accessToken, Date expiration) {
redisTemplate.opsForValue()
.set(accessToken, "logout", expiration.getTime() - new Date().getTime(), TimeUnit.MILLISECONDS);
}
public boolean isAccessTokenStored(String accessToken) {
return redisTemplate.opsForValue()
.get(accessToken) != null;
}
}
2.2 AuthorizationFilter
- ํ์๊ณผ ๋นํ์ ํํฐ
- ๋นํ์์ 401ํ๋ฉด์ ๋ณด์ฌ์ค๋ค -> ์ด ๋
onError()
๋ฅผ ํตํด HTTP ์ํ์ฝ๋๋ฅผ ์ถ๋ ฅ
@Component
@Slf4j
public class AuthorizationFilter extends AbstractGatewayFilterFactory<AuthorizationFilter.Config> {
Environment env;
private final RedisService redisService;
@Autowired
public AuthorizationFilter(Environment env, RedisService redisService) {
super(Config.class);
this.env = env;
this.redisService = redisService;
}
public static class Config {
// Put configuration properties here
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, HttpCookie> cookies = request.getCookies();
//log.info("cookie {}({})", cookies, cookies.get("token").get(0).getValue());
String jwt = "";
try {
jwt = cookies.get("token").get(0).getValue();
}
catch (NullPointerException e) {
return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED);
}
if (redisService.isAccessTokenStored(jwt) || !isJwtValid(jwt)) {
log.info("ํ ํฐ({})์ ์ฌ์ฉ๋ถ๊ฐ ํ ํฐ์
๋๋ค.", jwt);
return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED);
}
String id = Jwts.parser().setSigningKey(env.getProperty("token.secret"))
.parseClaimsJws(jwt).getBody()
.getSubject();
request.mutate()
.header("token", jwt)
.header("userId", id)
.build();
ServerWebExchange exchange1 = exchange.mutate().request(request).build();
log.info("ํ ํฐ์ ๋ณด : {}", jwt);
return chain.filter(exchange1);
};
}
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
log.error(err);
return response.setComplete();
}
private boolean isJwtValid(String jwt) {
boolean returnValue = true;
String subject = null;
try {
subject = Jwts.parser().setSigningKey(env.getProperty("token.secret"))
.parseClaimsJws(jwt).getBody()
.getSubject();
} catch (Exception ex) {
returnValue = false;
}
if (subject == null || subject.isEmpty()) {
returnValue = false;
}
log.info("subject {}", subject);
return returnValue;
}
}
2.3 AuthorizationAdminFilter
๊ด๋ฆฌ์ ํํฐ์ด๋ค
์ ์ ๊ด๋ฆฌ ํ์ด์ง๋ ๊ด๋ฆฌ์ ๋ชจ๋ ํ์ด์ง์ ์ ์ํ๋ ค๋ ๊ฒฝ์ฐ ํํฐ๋งํด์ค๋ค
- ๊ด๋ฆฌ์์ธ์ง๋ฅผ ํ์ธํด์ผํ๊ธฐ๋๋ฌธ์ Spring Security Role์ ์ฌ์ฉํ๋ค
@Component
@Slf4j
public class AuthorizationAdminFilter extends AbstractGatewayFilterFactory<AuthorizationAdminFilter.Config> {
Environment env;
private final RedisService redisService;
@Autowired
public AuthorizationAdminFilter(Environment env, RedisService redisService) {
super(Config.class);
this.env = env;
this.redisService = redisService;
}
public static class Config {
// Put configuration properties here
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, HttpCookie> cookies = request.getCookies();
String jwt = "";
try {
jwt = cookies.get("token").get(0).getValue();
}
catch (NullPointerException e) {
return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED);
}
if (redisService.isAccessTokenStored(jwt)) {
log.info("ํ ํฐ({})์ ์ฌ์ฉ๋ถ๊ฐ ํ ํฐ์
๋๋ค.", jwt);
return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED);
}
if (StringUtils.isEmpty(isJwtValid(jwt))) {
System.out.println("jwt๋ null = " + jwt);
return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED);
}
String id = Jwts.parser().setSigningKey(env.getProperty("token.secret"))
.parseClaimsJws(jwt).getBody()
.getSubject();
request.mutate()
.header("token", jwt)
.header("userId", id)
.build();
ServerWebExchange exchange1 = exchange.mutate().request(request).build();
log.info("ํ ํฐ์ ๋ณด : {}", jwt);
return chain.filter(exchange1);
};
}
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
log.error(err);
return response.setComplete();
}
private String isJwtValid(String jwt) {
String role = null;
try {
role = String.valueOf(Jwts.parser()
.setSigningKey(env.getProperty("token.secret"))
.parseClaimsJws(jwt)
.getBody().get("role"));
} catch (Exception ex) {
role = null;
}
if (role == null || role.isEmpty() || role.equals("null") || role.equals("ROLE_USER")) {
role = null;
}
log.info("role {}", role);
return role;
}
}
- role์ ๋ํด์ Spring Security - Role๋ฅผ ํ์ธ
- JWT + Spring Security + Spring Cloud = MSA 2. Spring Security์์ ์ฌ์ฉํ๋ role์ด๋ค.
์ ํํฐ๋ฅผ ํตํด ํ์ header์ userId๋ฅผ ์ ์ฅํ ์ ์๋ค
์๋ฅผ ๋ค์ด ๋ค๋ฅธ ์๋น์ค ํด๋ผ์ด์ธํธ์์ ์๋์ ๊ฐ์ด ์ฌ์ฉํ๋ค
@GetMapping("/board-service/test/nickname")
@ResponseBody
public String nicknameEx(HttpServletRequest req){
String userId = req.getHeader("userId");
String token = req.getHeader("token");
log.info("userId {}", userId);
String userName = api.requestName(userId, token);
return userName;
}
๋๊ธ ์ฐ๊ธฐ