Local Storage에 JWT토큰을
저장해서 포트폴리오를 만들던 도중
보안상 쿠키보다 훨씬 위험한 점이 있기 때문에 쿠키로 변경하려고 합니다.
Local Storage는 자바스크립트로 접근 가능하기 때문입니다.
자세한 것은 따로 링크나 글로 정리해서 올리겠습니다.
이미 어느정도 완성된 코드들을 수정하는 것은 쉬운 일은 아니 였습니다.
보안이 취약하다는 것을 알아버린 이상 일단 수정하겠습니다.
기존의 방식.
기존 방식은 간단했습니다.
accessToken과 RefreshToken둘을
쌍으로 해서 하나의 키로 사용했습니다. 인증에 성공해서 발급될 때는
유저와 토큰,리프레시 토큰 이렇게 쌍으로 DB와 저장해서 서로 검증하도록 만들었습니다.
둘 중 하나라도 다를 시 다시 인증 받도록 만들었습니다. (인증 받을
때 꼭 두개 다 필요했습니다.)
accessToken이 유출되더라도 RefreshToken은 알 수 없으니 결국 token두개를 탈취하지
않는 이상 쉽게 토큰을 사용할 수 없게 만들었습니다.
제가 생각한 단점은 크게 두가지였습니다.
1.
많은 사용자들이 있었다고 가정했을 때 사용자 수 만큼 서버가 감당해야하는 세션 방식과 똑같아져
JWT 만의 장점을 잃어 버린 것 같았습니다.
2.
토큰 저장장소는 LocalStorage 여서
보안상 매우 취약해질 수 밖에 없었습니다.
그래서 과감히 기존 방식을 버리고 다시 코드를 짜기 시작했습니다.
일단 DB에 저장하면서 추적하는 방식은 하지 않기로 했습니다.
@Configuration
public class PreFlightCorsConfiguration {
private static final String ALLOWED_HEADERS = "x-requested-with,authorization," +
"refreshtoken,serverToken,Access-Control-Allow-Origin,Content-Type," +
"credential,X-AUTH-TOKEN,X-CSRF-TOKEN,Cookie";
private static final String ALLOWED_METHODS = "GET, PUT, POST, DELETE, OPTIONS";
private static final String ALLOWED_ORIGIN = "http://localhost:3000";
private static final String ALLOWED_CREDENTIALS = "true";
// 중요 해당 헤더가 없다면 axios를 통한 프론트에서 확인이 불가능함
private static final String EXPOSE_HEADERS = "Authorization,Refreshtoken," +
"ServerToken,Cookie";
private static final String MAX_AGE = "3600";
@Bean
public WebFilter corsFilter() {
return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
if (CorsUtils.isPreFlightRequest(request)) {
ServerHttpResponse response = ctx.getResponse();
HttpHeaders headers = response.getHeaders();
headers.add("Access-Control-Allow-Origin", ALLOWED_ORIGIN);
headers.add("Access-Control-Allow-Methods", ALLOWED_METHODS);
headers.add("Access-Control-Max-Age", MAX_AGE);
headers.add("Access-Control-Allow-Headers",ALLOWED_HEADERS);
headers.add("Access-Control-Allow-Credentials",ALLOWED_CREDENTIALS);
// 프론트에서 해당 헤더를 확인할수잇게 허가해줌
headers.add("Access-Control-Expose-Headers", EXPOSE_HEADERS);
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
}
return chain.filter(ctx);
};
}
}
먼저 CORS 필터 설정을 해줍니다.
해당 코드는 Spring Security가 없는 API GATEWAY프로젝트의 코드입니다.
WebFilter라는 Bean등록과 Configuration 등록만 한다면 자동적으로 CORS설정 필터가
작동하니 따로 다른 곳에 등록할 필요가 없습니다.
물론 Spring Security가 들어간 다른 어플리케이션은 시큐리티에
등록해줘야합니다.
Origin은 말 그대로 주소,
Method는 Http 메소드, Age는 쿠키의
최대 시간 정도 됩니다.
Access-Control-Allow-Credentials : 프론트쪽에도
해야하지만 쿠키를 주고받으려면 서로 쿠키를 받고 보낼 수 있도록 true값을 넣어줘야합니다.
Access-Control-Expose-Headers : 만약 제가
원하는 값을 헤더에 담아 보냈다 하더라도, 지금 보신 설정을 허가 해주지 않는다면, 프론트에서 확인할 수 없으니 반드시 설정해놔야합니다.
@Configuration
public class CorsConfig {
// 스프링 시큐리티가 들고있는 cors 필터입니다.
// 프론트쪽에서 계속 막힌게 이녀석 때문
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// 내 서버 데이터 응답시 json을 자바 스크립트에서 처리할수
있도록
config.setAllowCredentials(true);
// 지금 코드가 위의 setAloowCredentials 와 같이
사용되는걸 권장한다
config.addAllowedOriginPattern("http://localhost:3000");
// 재밌는 점은 아래 코드는 이제 위의 setAllowCredentials 와
함께 사용하는걸
// 권장하지 않는다
//config.addAllowedOrigin("*"); // 모든
ip 응답을 허용
// 해당 헤더를 모두 허용해줘야 프론트에서 확인받아서
체크할수있다.
// jwt 를 담은 헤더를 리액트 쪽에서 확인할수있다는
뜻
config.setExposedHeaders(Arrays.asList("Authorization","RefreshToken"));
config.setAllowedHeaders(Arrays.asList("Authorization","RefreshToken"));
config.setAllowedMethods(Arrays.asList("GET","PUT","POST","DELETE","OPTIONS"));
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
위 코드는 API GATE쪽이 아닌 Spring Security가 있는 CORS FILTER 입니다.
AllowHeaders는 그냥 와일드 표시로 (*) 해주면 편하겠지만, 보안을 위해서 정확하게
Authorization, RefreshToken 정확하게 지정해주는
것이 좋습니다.
Cookie로 전환하면서 여러가지를 배웠는데 첫번째로
리액트와 스프링부트가 각각 만든 쿠키들은 브라우저에서 관리하기 때문에 어느 곳이든 접근 가능하다
하지만 HttpOnly 설정을 해둔다면 스프링부트만 접근이 가능하고
클라이언트쪽 자바스크립트가 접근할 수 없습니다.
(백엔드는 거의 다 해결 이제 프론트에서 받아온 정보들을 어떻게 관리
해야 할지 문제였습니다, 나중에 해결)
추가정보
API GATEWAY YML 파일 설정 부분에서
RemoveRequestHeader에서 Cookie를 지정해주면
위 설정에서 RemoveRequestHeader=Cookie
은 Gateway가 request를 받을 때 모든 Cookie
header를 제거하는 필터입니다. 이 필터를 적용하는 이유는 클라이언트 측에서 보내는 쿠키를 Gateway에서 사용하기 위함이 아니라, Gateway가 쿠키를 백엔드 서비스로 전달하지 않도록 하기 위함입니다.
즉, RemoveRequestHeader=Cookie
설정을 사용하면 클라이언트에서 보내는 쿠키는 백엔드 서비스로 전달되지 않고, Gateway에서 새로운 쿠키를 생성해서 백엔드 서비스로 전달할 수 있습니다. 이렇게 하면 보안에 민감한 쿠키를 클라이언트와 백엔드 서비스 간에 공유하지 않아도 되므로 보안성을 높일 수 있습니다.
(정확하지 않은 내용일수 있으니 주의해주세요.)