
Spring 기반 애플리케이션의 보안을 담당하는 스프링 프레임워크
여기서 보안에는 인증 / 인가(Authentication / Authorization) 등을 말한다.
인증 / 인가를 Filter를 활용하여 처리한다.
인증 (Authentication) : 해당 사용자가 본인이 맞는지를 확인하는 절차
인가 (Authorization) : 인증된 사용자가 요청한 자원에 접근 가능한지 결정하는 절차
implementation 'org.springframework.boot:spring-boot-starter-security'
위 의존성을 추가한 후 서버가 기동되면 스프링 시큐리티의 초기화 작업 및 보안 설정 발생
별도의 설정 또는 구현 없이도 기본적인 웹 보안 기능 연동
모든 요청은 인증이 된 후에 접근 가능
인증은 FormLogin 방식과 httpBasic 로그인 방식 두가지가 있다.
SpringSecurity 기본 로그인 페이지 제공



Client에서 Get 방식으로 자원 접근 요청
Server에서는 인증된 사용자만 접근 허락.
Client 인증 시도 (post 방식, username / password)
Server에서 SessionId 생성 후 인증 결과를 담은 인증 토큰(Authentication) 생성 및 저장
Client에서 /home 접근 요청 시 세션에 저장된 인증 토큰으로 접근 및 인증 유지

Client에서 GET 방식으로 /logout 리소스 호출
Server에서 세션 무효화, 인증토큰 삭제, 쿠키정보 삭제 후 로그인 페이지로 리다이렉트

요청이 Logout URL인지 확인
맞을 경우 SecurityContext에서 인증객체(Authentication)를 꺼내옴
SecurityContextLogoutHandler에서 세션 무효화, 쿠키 삭제, clearContext()를 통해 SecurityContext 객체를 삭제
SimpleUrlLogoutSuccessHandler를 통해 로그인 페이지로 리다이렉트

사용자의 인증 요청 : 로그인 정보 동봉 (HttpRequest)
AuthenticationFilter가 일종의 Interceptor 역할을 하며 사용자의 인증 요청을 가로챈다.
가로챈 정보를 가지고 미검증 상태의 UsernamePasswordAuthenticationToken 객체를 생성한다.
ProviderManager(AuthenticationManager의 구현체)에게 2번에서 생성했던 UsernamePasswordAuthenticationToken 객체를 전달한다.
AuthenticationProvider에 UsernamePasswordAuthenticationToken 객체 전달
DB로부터 사용자 인증 정보를 가져오는 UserDetailsService에 사용자 정보를 넘겨준다.
해당 클래스 내부의 loadUserByUsername() 메서드에 의해 작동한다.
DB의 사용자 정보인 UserDetails 객체 생성
AuthenticationProvider는 UserDetails를 넘겨받고 사용자 정보 비교 (인증 진행)
인증 완료 시 사용자 정보를 담은 Authentication 객체 반환
AuthenticationFilter에 Authentication 객체 반환
반환된 Authentication 객체를 SecurityContext에 저장
OAuth(OpenID Authentication) : 특정 사이트의 접근 권한을 얻고 그 권한을 이용하여 개발을 할 수 있도록 도와주는 프레임워크
Access Token을 발급 받고, 그 토큰을 기반으로 원하는 기능을 구현
Access Token : 로그인을 하지 않고도 인증을 할 수 있도록 해주는 인증 토큰의 개념
spring-security-oauth2-client 라이브러리를 사용해서 진행한다.
Resource Owner : 개인 정보의 소유자 (유저)
Client : 제 3의 서비스로부터 인증을 받고자 하는 서버 (직접 개발한 웹사이트)
Resource Server : 개인 정보를 저장하고 있는 서버 (구글)
Client ID : Resource Server에서 발급해주는 ID
Client Secret : Resource Server에서 발급해주는 PW
Authorized Redirect Uri : Client 측에서 등록하는 Url.

사용자가 소셜 로그인 정상적으로 완료
AbstractAuthenticationProcessingFilter에서 OAuth 2.0 로그인 과정 호출
Resource Server에서 넘겨주는 정보를 토대로 OAuth2LoginAuthenticationFilter의 attempAuthentication()에서 인증 과정을 수행
attemptAuthentication() 처리 과정에서 OAuth2AuthenticationToken을 생성하기 위해 OAuth2LoginAuthenticationProvider의 authenticate() 호출
authenticate() 처리 과정에서 OAuth2User를 생성하기 위해 OAuth2UserService의 loadUser() 호출
loadUser() 처리 과정에서 CustomOAuth2User 반환
위 과정이 정상적으로 끝났다면 AbstractAuthenticationPRocessingFilter에서 successHandler의 onAuthenticationSuccess() 호출
1~6 과정이 정상적으로 끝나지 않았다면 AbstractAuthenticationProcessingFilter의 failureHandler에서 onAuthenticationFailure() 호출
서버 측에 사용자의 정보 저장
Spring Security에는 별도 설정이 없다면 세션을 이용하여 처리
사용자가 로그인을 하면 서버는 해당 사용자의 세션을 생성하고, 서버의 메모리와 DB에 저장한다.
리소스(사용자 정보)에 직접 접근할 수 있도록 해주는 정보만 가짐.
Refresh Token에 비해 짧은 만료 기간
주로 세션에 담아 관리
Acess Token을 발급받기 위한 정보를 담은 토큰
클라이언트가 Access Token이 없거나 만료된 상태라면, Refresh Token을 통해 Auth Server에 요청하여 새 Access Token을 발급받을 수 있다.
보통 외부에 노출되지 않기 위해 DB에 저장
각 사이트에서 제공하는 Authorization Server를 통해 회원 정보를 인증하고 Access Token을 발급받는다.
Access Token을 활용해서 직접 개발한 서버의 API 서비스를 이용하고 호출한다.
API 호출 요청에 대해 전달받은 Access Token이 유효한지 확인
서버의 수가 많아지면 각 서버가 Access Token의 유효성 및 권한 확인을 Auth Server에 요청하기 때문에 병목 현상 등이 발생하여 서버의 부하로 이어질 수 있다.
- 사용자 Auth Server 로그인
- Auth Server에서 인증을 완료한 사용자는 JWT 토큰을 전달 받음
- 클라이언트는 특정 애플리케이션 서버에 리소스를 요청할 때, 앞서 전달받은 JWT토큰을 Authorization Header에 넣어서 전달한다.
- 애플리케이션 서버는 전달 받은 JWT 토큰의 유효성을 직접 검사하여 사용자 인증을 할 수 있다.
다만 인증정보가 필요한 요청을 보낼 때 헤더에 JWT 토큰 값을 넣어 보내야 하므로 데이터가 증가하여 네트워크 부하가 늘어날 수 있다.
또한 토큰 자체에 사용자 정보를 담고 있기에 JWT가 만료되기 전에 탈취당하면 서버에서 처리당할 수 있는 일이 없다.
JWT는 만료 시간을 필수적으로 넣어야 한다.
보안성 / 편의성 관점에서 Trade Off로부터 적절한 협의점인 Refresh Token을 관리하는 방법이 택해진다.
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (isAppropriateRequestForFilter(request)) { // JWT 토큰 검증이 필요한 경우에만 동작하도록
try {
String token = jwtUtil.resolveToken(request);
Authentication authentication = jwtUtil.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (JWTVerificationException e) {
/* ... */
}
}
filterChain.doFilter(request, response);
}
/* ... */
}
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
/* ... */
http.addFilterAfter(jwtAuthenticationFilter, LogoutFilter.class);
}
/* ... */
}
@Component
public class JwtUtil {
public String generateRefreshToken(CustomOAuth2User customOAuth2User) {
/* ... */
}
public String generateAccessToken(String refreshToken) {
/* ... */
}
public String resolveToken(HttpServletRequest request) {
/* ... */
}
public Authentication getAuthentication(String accessToken) {
/* ... */
}
/* ... */
}
OAuth 2.0 로그인을 정상적으로 완료한 사용자는 토큰이 없는 상태
클라이언트는 발급받은 두 토큰을 안전한 공간에 보관
매 요청마다 Authorization Header에 Access Token을 추가하여 전송하고, Access Token이 만료되면 Access Token 재 발급을 위해 Refresh Token을 사용한다.