문제상황
•
42 측의 휴학 유저(AGU) 추가 와 Cabi 내에서 수요지식회 업데이트 가 진행되면서 기존 인증, 인가 방식에 대해 부족함을 느끼게 되었다.
•
휴학 유저는 42 oauth 로그인 불가능 → cabi 서비스를 이용할 수 없음
사물함 반납을 못하게되니 채널에 대리반납을 요청하거나 문의를 넣는 유저들이 꽤 발생했다.
42 oauth 서버 다운 시 대체 로그인 방식을 도입할 겸, 소셜 로그인 연동기능을 추가해 휴학 유저도 서비스 이용 가능케하자.
•
수요 지식회가 리뉴얼되며 발표에 대한 사진 및 영상을 발표자가 포트폴리오로 활용할 수 있도록 기록하는 것이 큰 목표가 되었고, 이로 인해 외부 이용자도 자유롭게 발표 내용 및 일정을 볼 수 있어야한다.
기존 서비스에서는 accessToken이 없으면 무조건 서비스 이용이 불가능했으니, 인가 방식을 변경해보자
기존 방식
•
OAuth2 로그인
ScribeJava 라이브러리를 사용, yml 파일을 주입받은 url 관련 Property 객체 생성, 지정된 url로 요청을 보내 정보를 내려받음
◦
새로운 소셜 로그인을 추가할 때마다 객체를 새로 만들고, 컨트롤러도 새로 만들어야해서 내가 언해피함
•
모든 요청에 대한 Interceptor + 커스텀 어노테이션 AuthGuard, AOP 방식
요청 시 access_token 검증, 토큰 파싱 후 내부의 email을 쿼리로 돌아 실제 유저인지 검증 및 인가
◦
public한 페이지에 대해서는 어떻게..? 모든 요청에 대해서 불필요하더라도 유저인지 검증하기 위해 쿼리가 1회씩 꼭 돌아서 인증 / 인가의 경계가 모호함
목표
1.
ServletFilter에서 url별 인가 검증하기
2.
여러 개의 소셜 로그인 연동 추가 방식 간소화하기
3.
access, refresh token 도입하기 및 정보 덜어내기
4.
Security에서 발생하는 예외와 MVC단에서 발생하는 예외 별도처리하기
쉽게, SNS로그인 + JWT 인가 구현할건데, 이걸 Spring Security와 함께 어떻게 구현했는지 꼭꼭 씹어먹어봅시다
Spring Security?
•
인증(Authentication, 로그인), 인가(Authorization, 권한 부여)와 관련해 편리한 기능들을 제공해주는 라이브러리이다.
이 글을 읽는 Security에 대해 궁금한 누군가도 함께 어떻게 구현했는지 구경하며 Security를 느껴보자.
Q. 그럼 Security를 이해하기 위해서는 뭐부터 알아야해요?
A.
도망가지 말아다오.. 생긴거만 험악하지 읽어보면 다들 따스한 필터들이다..
하지만 아~ 대충 rgrg, 일단 박죠? 했을 경우 남는건 수많은 Servlet 및 FilterChain Exception 관련 로그와 어디에서 발생한지도 모른 채 돌아온 401, 403 에러들 뿐ㅠㅠ
저런 수많은 필터를 쓰는 근본적인 이유는 아래의 두 개념을 세분화하여 관리하기 위함이다!
일단 의존성에 spring security를 추가했다면, 빨리 yml파일에 두가지를 일단 추가해보셈
logging:
level:
org.springframework.security.oauth2: DEBUG
org.springframework.security.web: DEBUG
YAML
복사
자 벌써 에러 300개 미리 잡았다고 생각해도 된다 ㅇㅇ 오늘은 끄고 쉬어도됨
일단 저거 키고 어디에서 에러가 났는지, 어디로 redirect 되는지 꼼꼼히 잘 따라가야한다
인증(Authentication)
인증은 쉽게 말하면 로그인 이라고 생각하면 된다.
웹앱이 너 누구야? 했을 때 아이디 비밀번호를 입력 → 자격 증명 → 저 유저 맞는데요
로그인 실패 시 security는 AuthenticationException을 반환하고, Unauthorized 401으로 처리된다
•
에러 이름은 authorized(인가)인데 왜 authentication(인증)이랑 관련?
◦
저도 궁금했는데요.. Unauthorized 용어 자체가 사용자가 인증되지 않았거나, 인증 정보가 유효하지 않다는 뜻까지 확장됐기 때문이라고합니다. 오래된 애라 그렇다함..
인가(Authorization)
권한, 즉 어디까지 접근 가능하세요?를 물어본다.
서버의 cabinet 관련 리소스는 ‘USER’ 롤 이상부터 접근 가능하다고 판별을 한다던지 머..
security에서는 AccessDeniedException을 반환하고, 403 Forbidden으로 처리된다.
캬~ 인증 인가에 대해 깨달았다? 일단 나보다 Security 잘한다고 자부한다.
하지만 Security를 구현하며 424242번의 에러가 터질 것인데 크게 네 가지 예외로 축약된다.
1.
인증 실패(AuthenticationException)
2.
권한 부족(AccessDeniedException)
3.
CSRF 관련(InvalidCsrfTokenException, MissingCsrfTokenException)
4.
세션 만료(SessionAuthenticationException)
우리 서비스에서는 세션을 stateless로 지정해놨으니 4는 치우고 1,2,3이 언제 발생하고, 어떻게 대응할 지 생각해보자
이제 코드와 함께 필터가 어떻게 작동하는지 알아보자!
SecurityConfig
어떤 필터를 어떤 순서로 작동시킬건지 하나씩 뜯어보자
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final JwtExceptionFilter jwtExceptionFilter;
private final LoggingFilter loggingFilter;
private final CustomSuccessHandler customSuccessHandler;
private final CsrfCookieConfig csrfCookieConfig;
private final CustomAuthenticationEntryPoint entryPoint;
private final CustomAccessDeniedHandler accessDeniedHandler;
private final SecurityExpressionHandler<FilterInvocation> expressionHandler;
private final LogoutHandler logoutHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http)
throws Exception {
http.csrf(csrf -> csrf
.csrfTokenRepository(csrfCookieSecurityFilterChain filterChainConfig)) // csrf 공격 방지를 위해 활성화. JWT 인가 방식 사용 시 보통 비활성화를한다.(refreshToken 때문에 우린 활성화..)
// oauth2 로그인 방식만을 사용할거라 둘 다 비활성화
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.cors().and() // CORS 관련 설정 활성화. 기존에 사용하던 MVC CORS Config 그대로 사용
// 세션 비활성화
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 인가 범위를 url 단위로 지정 어떤 리소스는 최소 role을 가져야 접근 가능
.authorizeRequests(auth -> auth
// master role에게 최상위 권한을 주기 위해 expressionHandler 커스텀
.expressionHandler(expressionHandler)
.mvcMatchers(SecurityPathPatterns.PUBLIC_ENDPOINTS)
.permitAll()
.mvcMatchers(SecurityPathPatterns.ADMIN_ENDPOINTS).hasRole("ADMIN")
.mvcMatchers(SecurityPathPatterns.USER_ADMIN_ENDPOINTS)
.hasAnyRole("USER", "ADMIN")
.mvcMatchers(SecurityPathPatterns.USER_AGU_ENDPOINTS)
.hasAnyRole("USER", "AGU")
.anyRequest().hasRole("USER")
)
// oauth2 활성화. 커스터마이징한 userService를 사용하고, 성공 시 지정한 커스텀 successHandler로 ㄱㄱ
.oauth2Login(oauth -> oauth
.userInfoEndpoint(user -> user.userService(customOAuth2UserService))
.successHandler(customSuccessHandler)
)
// 로그아웃 순서는 url 요청 -> 관련 메서드(세션, authentication 제거) 수행 -> logoutHandler 수행
.logout(logout -> logout
// 해당 api로 요청 시 로그아웃을 수행한다(컨트롤러 내부 메서드는 수행 x, url만 연결)
.logoutUrl("/v5/auth/logout")
// 여기에서 본격적인 logout 과정 수행(jwt 파기, 쿠키 삭제)
.logoutSuccessHandler(logoutHandler)
// 세션 무효화, authentication 제거
.invalidateHttpSession(true)
.clearAuthentication(true)
)
// 필터 순서 및 커스텀 필터 정의
// addBefore(필터명A, 필터명B): 필터B 이전에 필터A 수행할래
.addFilterBefore(loggingFilter, SecurityContextHolderFilter.class)
.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter.class)
// 예외 핸들링 커스터마이징
.exceptionHandling(handler -> handler
.authenticationEntryPoint(entryPoint)
.accessDeniedHandler(accessDeniedHandler))
;
return http.build();
}
}
Java
복사
클래스 단위의 어노테이션의 의미
@Configuration // SecurityConfig.class 자체를 컨테이너 빈에 등록합니다
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
Java
복사
•
Configuration → SecurityConfig.class 클래스 자체를 컴포넌트 서치 방식을 통해 컨테이너 빈에 등록
◦
컴포넌트서치 했는데 filterChain 부분을 왜 따로 Bean으로 등록함??
▪
Configuration이 붙은 클래스를 스프링 컨테이너에서 빈을 등록하는 역할을 하는거 맞음. 하지만 클래스 내부에서 선언된 모든 메서드가 자동으로 빈으로 등록되는건 아니다.
즉, 클래스 자체 빈으로 등록 O, 안에 정의된 메서드를 빈으로 등록하는건 아니므로 명시적으로 적어준거
•
EnableWebSecurity → Spring Security 활성화, 작성한 config를 구성할 수 있도록 한다. Spring security 기본 설정 비활성화, 내가 작성한 Config 쓸래잉
•
EnableGlobalMethodSecurity(prePostEnabled = true) → 메서드 단위에서 인가(Authorization) 활성화. config에서 작성해둔 authorizeRequests 부분은 url을 기준으로 인가를 검증하는데, 해당 어노테이션을 추가해 url을 통한 인가 검증 뿐만 아니라 메서드 단위에서도 인가 검증 기능을 활성화한다.
◦
주의! 방금 말했듯, @PreAuthorize, @Secured 어노테이션은 메서드 단위에서 인가를 활성화한다고 했다. 이 말은 Security Servlet 부분은 지났으니, 에러 핸들링은 AccessDeniedHandler가 아닌 MVC단까지 도달했다는거고, ExceptionController 부분에서 에러 처리가 된다.
filterChain 다이어그램
캬.. junsbae 님이 협찬해주신 컴춘식이 다이어그램을 그려줬다.
서비스에서 동작하는 security는 다음과 같은 흐름으로 진행되고, 이제 내부에서 동작하는 과정을 자세히 보자.
OAuth2 로그인 과정
•
yml 파일 설정
security:
oauth2:
client:
registration:
# 유저 로그인용
ft:
client-name: ft
authorization-grant-type: authorization_code
client-id: test
client-secret: test
redirect-uri: ${cabinet.server.be-host}/login/oauth2/code/ft
client-authentication-method: client_secret_basic
scope: public
provider:
ft:
authorization-uri: https://api.intra.42.fr/oauth/authorize
token-uri: https://api.intra.42.fr/oauth/token
user-info-uri: https://api.intra.42.fr/v2/me
users-info-uri: https://api.intra.42.fr/v2/users
user-name-attribute: login
YAML
복사
이런 식으로 yml파일을 설정해두면, security는 이를 기반으로 ClientRegistration 클래스를 만들고 주입한다
더 궁금하다면 직접 뜯어보시고, 읽기 ㄱㄱㄱ
Core Interfaces / Classes :: Spring Security
이 객체가 형성되었다면 oauth2 로그인을 수행할 준비가 되었다!
그렇담 우리 서비스에서 어떻게 수행되는지 알아보자. 다이어그램과 함께 읽어보아요
0.
BE는 security 흐름에 따라 yml 파일에 설정된 객체를 만듦.
1.
FE → BE 에게 /oauth2/authorization/{provider} redirect 요청
2.
BE는 oauth provider가 제공하는 로그인 페이지(authorization-uri)로 리다이렉트
a.
이 때 client-id, redirect-uri, scope 등 정보가 함께 전달된다
3.
유저가 아이디, 비밀번호 입력해서 로그인 수행, 자격증명
4.
인증 성공 시, redirect-uri로 인증 코드와 함께 리다이렉트 수행
5.
받은 인증 코드, client-id, client-secret을 포함하여 제공자의 token-uri에 액세스 토큰 요청
6.
provider의 accessToken을 받아온 뒤, 유저의 정보를 받으러 user-info-uri로 ㄱㄱ
7.
사용자 정보 처리
a.
UserService 클래스를 커스터마이징한 CustomOAuth2UserService의 loadUser 호출 → OAuth2 제공자로부터 받은 사용자 정보 처리, OAuth2User 객체 반환 및 SecurityContextHolder에 Authorization 객체 저장
b.
보통 security의 흐름에서는 여기에서 신규 사용자 등록 및 매핑을 수행하지만, 42에서 내려주는 JSON이 너무 지저분하고(..), 여러 객체의 공통적인 attribute 및 DefaultOAuth2User를 만들기 위해 여기에서는 유저의 정보를 내려온 뒤 공통적으로 사용할 정보를 파싱만 수행했다.
8.
인증 성공 처리
a.
SuccessHandler 호출 → 신규 유저 판별 및 사용자 권한(Role) 설정, Authorization 설정 및 JWT 토큰 생성, 쿠키 설정 후 Role에 따라 FE의 특정 페이지로 리다이렉트를 수행.
9.
FE는 쿠키를 헤더에 저장하고, 모든 요청에 대해 CSRF 토큰 + JWT 토큰을 헤더에 담아 보낸다면 BE는 이를 기반으로 인증, 인가를 진행해 접근하려는 url에 대한 권한이 충분하다면 리소스를 내려준다.
0 ~ 6의 과정은 yml 파일 설정만 잘 해두면 끝이다!
우리의 목표였던 새로운 oauth provider가 추가되어도 새로운 컨트롤러를 만들어주지 않아도 된다. 벌써 Security를 도입한 1, 2번 목표를 달성해버렸음!!
근데 security 없으면 어떻게 구현해요? 궁금하신 분들은 이전 기수분들이 피땀눈물로 작성하신 ScribeJava를 활용한 인증, 인가 방식을 뜯어보면 정말 많은 도움이 될것이다!!
Spring Security 안쓰고 AOP로 OAuth 구현하기 - 1
구현할 부분은 서비스의 목표에 맞춰 7, 8번만 잘 작성해주면 된다!
사용자 정보 처리 - loadUser 커스터마이징
oauth2 provider의 accessToken을 갖고 유저의 정보를 가져왔으니, 각자 조금씩 다른 객체의 필드를 파싱해 successHandler에서 편하게 이용할 수 있도록 1차 거름망 역할을 해주자.
OAuth2UserRequest 객체를 받아서, OAuth2User 타입으로 반환해야하는구나를 느껴보자. 객체 내부가 궁금하다면 OAuth2UserRequest, DefaultOAuth2User 검색 ㄱㄱ
커스텀 객체의 목표는 attribute를 파싱하고, OAuth2User를 상속받은 CustomOAuth2User에 이를 담아 반환하는 것이다
public class Oauth2Attributes {
private final String provider;
private final String name;
private final String email;
private final Map<String, Object> attributes;
public Oauth2Attributes(String provider, String name, String email,
Map<String, Object> attributes) {
// 필수 타입들만 null 검증 수행
if (provider == null || name == null || email == null) {
throw new SpringSecurityException(ExceptionStatus.INVALID_ARGUMENT);
}
this.provider = provider;
this.name = name;
this.email = email;
this.attributes = attributes;
}
public static Oauth2Attributes of(String provider, Map<String, Object> attributes,
String attributeKey) {
if (provider.equals("google")) {
return ofGoogle(attributes, attributeKey);
}
if (provider.equals("naver")) {
return ofNaver(attributes, attributeKey);
}
if (provider.equals("kakao")) {
return ofKakao(attributes, attributeKey);
}
if (provider.equals("github")) {
return ofGithub(attributes, attributeKey);
}
if (provider.equals("ft")) {
return ofFt(attributes, attributeKey);
}
throw new SpringSecurityException(ExceptionStatus.NOT_SUPPORT_OAUTH_TYPE);
}
Java
복사
attribute를 다음과 같이 구축해 조금씩 다른 필드 명에서 우리 서비스에서 공통적으로 사용할 name, email, provider만 파싱
public class CustomOAuth2User implements OAuth2User {
private final Oauth2Attributes oauth2Attributes;
private String name;
private String provider;
private String email;
public String getEmail() {
return oauth2Attributes.getEmail();
}
public String getProvider() {
return oauth2Attributes.getProvider();
}
@Override
public String getName() {
return oauth2Attributes.getName();
}
@Override
public Map<String, Object> getAttributes() {
return oauth2Attributes.getAttributes();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of();
}
}
Java
복사
attribute를 갖고 OAuth2User를 상속한 CustomOAuth2User를 만들자.
authorities는 successHandler에서 부여할것이기 때문에 이 단계에서는 빈 컬렉션으로 반환했다
여기까지 우리가 사용할 일차 거름망 역할을 다 수행했으니, 지옥의 successHandler로 가보자..
인증 성공 처리 - SuccessHandler
해당 부분에서 처리할 것들이 많다
1.
main provider(ft) 에서 로그인
a.
신규 유저 : JsonNode 파싱 → FtOauthProfile 객체 생성 후 이를 갖고 User 생성
b.
기존 유저 : JsonNode를 파싱한 후 role, blackholedAt 업데이트 수행
2.
다른 oauth 로그인 - (계정 연동)
a.
신규 유저 : FT 로그인 상태에서 계정연동을 요청한건지 검수 후 OauthLink 저장
b.
기존 유저 : AGU 및 블랙홀 검증, 업데이트
3.
Authorization 생성 및 JWT 발급 및 쿠키에 저장, FE로 리다이렉트
a.
JWT는 Authorization Header에 세팅하는거 아님? 왜 쿠키요?
i.
redirect-uri를 BE로 지정해 Oauth2 로그인 처리를 모두 백엔드에서 진행했기 때문에, 해당 과정을 모두 마치고 나서는 토큰 전달 + FE로 강제로 흐름을 틀어줘야함. 이 과정에서 리다이렉트를 수행하게되는데, 정보를 안전하게 저장할 수 있는 방식이 제 머리의 한계로는 cookie 뿐이엇음..
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
CustomOAuth2User fromLoadUser = (CustomOAuth2User) authentication.getPrincipal();
String provider = fromLoadUser.getProvider();
try {
//provider에 따라 OauthResult 형성
OauthResult oauthResult = processOAuthLogin(request, provider, fromLoadUser);
// SecurityContextHolder에 정보 저장 및 JWT를 cookie에 저장
authFacadeService.processAuthentication(request, response, oauthResult, provider);
// FE로 돌아가자 ㄱㄱ role 따라서 response.sendRedirect 시킴
redirectUser(response, oauthResult);
} catch (Exception e) {
SecurityContextHolder.clearContext();
securityExceptionHandlerManager.handle(response, e, true);
}
}
private OauthResult processOAuthLogin(HttpServletRequest req,
String provider, CustomOAuth2User oauth2User) {
if (provider.equals(mainProvider)) {
JsonNode rootNode =
objectMapper.convertValue(oauth2User.getAttributes(), JsonNode.class);
return oauthFacadeService.handleFtLogin(rootNode);
}
return oauthFacadeService.handleExternalOAuthLogin(oauth2User, req);
}
// JWT 생성시 필요한 정보와 redirect 경로를 담은 DTO
public class OauthResult {
private Long userId;
private String roles;
private String redirectionUrl;
public boolean hasRole(String role) {
return role != null && role.contains(roles);
}
}
Java
복사
public class OauthFacadeService {
생략..
// ftOauth 로그인 시, 신규 유저를 등록하거나 유저 상태 업데이트 userFacdeService 내에서 트랜잭션 작업 처리 끝
public OauthResult handleFtLogin(JsonNode rootNode) {
FtOauthProfile profile = oauthProfileService.convertJsonNodeToFtOauthProfile(rootNode);
User user = userFacadeService.createOrUpdateUserFromProfile(profile);
return new OauthResult(user.getId(), user.getRoles(), authPolicyService.getMainHomeUrl());
}
@Transactional
public OauthResult handleExternalOAuthLogin(CustomOAuth2User oauth2User,
HttpServletRequest request) {
String oauthMail = oauth2User.getEmail();
// 관리자 로그인 구분
if (securityPathPolicy.isAdminContext()) {
return adminAuthService.handleAdminLogin(oauthMail);
}
// 계정 연동 관련 Facade 서비스
return oauthLinkFacadeService.handleLinkUser(request, oauth2User);
}
}
Java
복사
클래스를 나누면서도 고민고민.. 각각의 서비스를 successHandler에서 호출하자니 주입받아야하는 서비스가 너무너무너무 많아 분리를 했다.
OauthFacadeService 클래스는 Oauth와 관련된 FacadeService 들의 머리 역할을 하도록 구현해 조율에 중점을 둬 다른 FacadeService들을 호출하여 단일 책임 및 가독성을 높혀보자
계정 연동파트를 구현하며 고민한 부분은 다음과 같다
1.
신규 연동 시 까비 마이페이지에서 연동시도(oauth 로그인)한걸 어떻게 입증할까?
a.
백엔드도메인/oauth2/authorization/google 이런 식으로 직접 입력해서 로그인해버리는거면?
2.
신규 연동 시 기존 유저 정보를 어떻게 가져올거고, 새로 연동하는 정보 사이의 공통점을 어떻게 만들까?
a.
이미 우리 서비스에 가입했다는 증거로 User를 특정할 수 있는 PK값이 필요
3.
기존 연동 유저가 휴학(AGU)를 했다가, 휴학을 풀었을 때 리소스 접근 권한을 확장해야함
크아악.. 늘 생각하지만 정책 정하기, 객체지향적인 코드 짜기는 너무너무 어렵다
1, 2번 방식을 해결하기 위해 Session, SecurityContextHolder 내부 정보 확인 등등 다양한 방식을 시도해봤지만 세션을 사용한다면 언제 만료를 시킬지와 유저 정보가 담겨있다보니 보호 방식에 대해 부적합하다는 판단을 했고, SecurityContextHolder 방식은 생명주기가 하나의 HTTP 요청에 대해서만 유효하기 때문에 oauth 로그인을 수행하며 발생하는 여러 번의 redirect를 시도하는 과정에서 정보를 유지할 수 없었다..
Resolver를 커스터마이징하는 방식..도 트라이해봤지만 규격에 맞춰 파라미터에 값을 유지하는게 쉽지 않았고, 뭔가 구현할수록 Security를 이용해 OAuth2 로그인방식을 편리하게 구현하는 방식과 멀어지는것 같아 포기 어흑
남은건 너뿐이다.. 쿠키. httpOnly, secure 옵션을 잘 활용해 보안에 신경쓰고, csrf 토큰을 활성화하자.
1, 2번 고민에 대해서는 Cookie에 담긴 RefreshToken을 통해 해결했다.
1.
신규 연동 시 까비 마이페이지에서 연동시도(oauth 로그인)한걸 어떻게 입증할까?
a.
서비스 로그인 시 발급하는 RefreshToken의 유무를 판별
2.
신규 연동 시 기존 유저 정보를 어떻게 가져올거고, 새로 연동하는 정보 사이의 공통점을 어떻게 만들까?
a.
RefreshToken 파싱, 내부의 userId값과 oauth 로그인 시 얻은 email 정보를 갖고 OauthLink 저장
3번 고민은 OauthProfileService를 통해 42 api를 호출하고, 42서버가 문제가 있는게 아니라면 update를 진행했다.
CSRF가 모에요?
1.
redirect 시 쿠키에 토큰을 담아 보낸다
2.
refreshToken이 쿠키에 유지되고 있다(httpOnly, secure는 true)
3.
FE는 withCredentials 옵션을 활성화했다
4.
반드시 CSRF 공격에 대해 방어해야한다
컴춘식의 CSRF 공격에 대한 깔끔한 설명이다. security와 함께 방어해보자
http.csrf(csrf -> csrf
.csrfTokenRepository(csrfCookieConfig)) // csrf 공격 방지를 위해 활성화. JWT 인가 방식 사용 시 보통 비활성화를한다.(refreshToken 때문에 우린 활성화..)
Java
복사
앞서 securityConfig를 설명하며 security의 csrf 옵션을 활성화 시켰다
이후 모든 GET을 제외한 모든 요청에 대해 security는 필터단에서 자동으로 csrf 토큰을 검증하고, 존재하지 않거나 변경되었을 경우 CsrfException(InvalidCsrfTokenException or MissingCsrfException)과 함께 403을 반환한다.
Config 부분은 다음과같이 작성해 cookieService에서 도메인명에 따라 secure 옵션을 t/f로 세팅하자.
아예 전체적인 쿠키들을 불변성을 유지할겸, 헤더에 바로 이용할 수 있도록 ResponseCookie로 만들까 생각도 했지만 잊지말자. 우리는 FE로 redirect를 수행해야한다는 것을 흑흑..
얘만 따로 빼서 하기도 애매해서 csrf 토큰도 쿠키로 설정하기로 타협했고, sameSite 설정은 서비스 내부에서 실행 환경이 local인지에 따라 결정해줬다.
토큰 세팅했는데 모 어쩌라고.. 왜 방어되는거임?
컴춘식의 깔끔한 설명22..
이제 로그인 시 CSRF 토큰을 발급하고, FE는 이를 Jwt토큰과 함께 헤더에 세팅해 요청한다면 CSRF 공격에 방어할 수 있게되었다!
자, 여기까지 왔다면 OAuth2 로그인, 토큰 발급을 통해 FE에게 안전하게 리소스를 내려줄 준비가 드디어 끝났다!
다음엔 이 JWT에 Role을 담아주고, 이를 기반으로 Authorization을 구축해 Security에게 현재 유저가 어떤 Role을 갖고 있는지 알려주고, 이 권한에 따라 접근할 수 있는 리소스 범위를 지정해주자