[Spring] Spring Security

박세윤·2023년 7월 10일

Spring Security

목록 보기
1/1
post-thumbnail

📖 Spring Security


✅ Spring Security란?

  • Spring 기반 애플리케이션의 보안을 담당하는 스프링 프레임워크

  • 여기서 보안에는 인증 / 인가(Authentication / Authorization) 등을 말한다.

  • 인증 / 인가를 Filter를 활용하여 처리한다.



✅ 인증 / 인가

  • 인증 (Authentication) : 해당 사용자가 본인이 맞는지를 확인하는 절차

  • 인가 (Authorization) : 인증된 사용자가 요청한 자원에 접근 가능한지 결정하는 절차


  • 인증 이후 인가 진행
    • 인가 과정에서 리소스에 대한 접근 권한을 확인한다.
  • Spring Security에서는 인증/인가를 위해 Principal을 아이디로, Credential을 비밀번호로 사용하는 인증 방식을 사용한다.
    • Principal(접근 주체) : 보호받는 Resource에 접근하는 대상
    • Credential(비밀번호) : Resource에 접근하는 대상의 비밀번호



✅ SpringSecurity Dependency 추가

implementation 'org.springframework.boot:spring-boot-starter-security'
  • 위 의존성을 추가한 후 서버가 기동되면 스프링 시큐리티의 초기화 작업 및 보안 설정 발생

  • 별도의 설정 또는 구현 없이도 기본적인 웹 보안 기능 연동

  • 모든 요청은 인증이 된 후에 접근 가능

  • 인증은 FormLogin 방식과 httpBasic 로그인 방식 두가지가 있다.

  • SpringSecurity 기본 로그인 페이지 제공

  • 기본 계정 제공
    • id : user
    • pw : 서버 콘솔에 뜨는 랜덤 문자열
      - application.properties에 기본 name/pw 설정도 가능



✅ Form Login 인증

  1. Client에서 Get 방식으로 자원 접근 요청

  2. Server에서는 인증된 사용자만 접근 허락.

    • 인증 안되면 로그인 페이지로 리다이렉트
  3. Client 인증 시도 (post 방식, username / password)

  4. Server에서 SessionId 생성 후 인증 결과를 담은 인증 토큰(Authentication) 생성 및 저장

  5. Client에서 /home 접근 요청 시 세션에 저장된 인증 토큰으로 접근 및 인증 유지



✅ Logout 처리

  1. Client에서 GET 방식으로 /logout 리소스 호출

  2. Server에서 세션 무효화, 인증토큰 삭제, 쿠키정보 삭제 후 로그인 페이지로 리다이렉트



  1. 요청이 Logout URL인지 확인

  2. 맞을 경우 SecurityContext에서 인증객체(Authentication)를 꺼내옴

  3. SecurityContextLogoutHandler에서 세션 무효화, 쿠키 삭제, clearContext()를 통해 SecurityContext 객체를 삭제

    • 인증 객체(Authentication)도 null로 만든다.
  4. SimpleUrlLogoutSuccessHandler를 통해 로그인 페이지로 리다이렉트



✅ Spring Security의 구조

  1. 사용자의 인증 요청 : 로그인 정보 동봉 (HttpRequest)

  2. AuthenticationFilter가 일종의 Interceptor 역할을 하며 사용자의 인증 요청을 가로챈다.
    가로챈 정보를 가지고 미검증 상태의 UsernamePasswordAuthenticationToken 객체를 생성한다.

  3. ProviderManager(AuthenticationManager의 구현체)에게 2번에서 생성했던 UsernamePasswordAuthenticationToken 객체를 전달한다.

  4. AuthenticationProvider에 UsernamePasswordAuthenticationToken 객체 전달

  5. DB로부터 사용자 인증 정보를 가져오는 UserDetailsService에 사용자 정보를 넘겨준다.
    해당 클래스 내부의 loadUserByUsername() 메서드에 의해 작동한다.

  6. DB의 사용자 정보인 UserDetails 객체 생성

  7. AuthenticationProvider는 UserDetails를 넘겨받고 사용자 정보 비교 (인증 진행)

  8. 인증 완료 시 사용자 정보를 담은 Authentication 객체 반환

  9. AuthenticationFilter에 Authentication 객체 반환

  10. 반환된 Authentication 객체를 SecurityContext에 저장


  • 만약 OAuth 2.0 로그인을 사용하면 UsernamePasswordAuthenticationFilter 대신 OAuth2LoginAuthenticationFilter가 호출됨.
  • UsernamePasswordAuthenticationFilter(인증처리필터)는 Form 인증처리를 하는 필터로써 크게 인증 전 후 두가지의 작업을 관리



✅ OAuth 2.0

  • OAuth(OpenID Authentication) : 특정 사이트의 접근 권한을 얻고 그 권한을 이용하여 개발을 할 수 있도록 도와주는 프레임워크

    • ex> 카카오, 네이버, 구글, 페이스북, 트위터 등
  • Access Token을 발급 받고, 그 토큰을 기반으로 원하는 기능을 구현

  • Access Token : 로그인을 하지 않고도 인증을 할 수 있도록 해주는 인증 토큰의 개념

  • spring-security-oauth2-client 라이브러리를 사용해서 진행한다.



✅ OAuth 2.0 용어

  • Resource Owner : 개인 정보의 소유자 (유저)

  • Client : 제 3의 서비스로부터 인증을 받고자 하는 서버 (직접 개발한 웹사이트)

  • Resource Server : 개인 정보를 저장하고 있는 서버 (구글)

  • Client ID : Resource Server에서 발급해주는 ID

    • 웹 사이트에 구글이 할당한 ID를 알려주는 것
  • Client Secret : Resource Server에서 발급해주는 PW

  • Authorized Redirect Uri : Client 측에서 등록하는 Url.



✅ OAuth 2.0을 활용한 소셜 로그인

  • application.yml에 위와 같이 소셜 로그인의 정보를 등록해야 한다.



✅ OAuth 2.0 인증 과정

  1. 사용자가 소셜 로그인 정상적으로 완료

  2. AbstractAuthenticationProcessingFilter에서 OAuth 2.0 로그인 과정 호출

  3. Resource Server에서 넘겨주는 정보를 토대로 OAuth2LoginAuthenticationFilter의 attempAuthentication()에서 인증 과정을 수행

  4. attemptAuthentication() 처리 과정에서 OAuth2AuthenticationToken을 생성하기 위해 OAuth2LoginAuthenticationProvider의 authenticate() 호출

  5. authenticate() 처리 과정에서 OAuth2User를 생성하기 위해 OAuth2UserService의 loadUser() 호출

  6. loadUser() 처리 과정에서 CustomOAuth2User 반환

  7. 위 과정이 정상적으로 끝났다면 AbstractAuthenticationPRocessingFilter에서 successHandler의 onAuthenticationSuccess() 호출

  8. 1~6 과정이 정상적으로 끝나지 않았다면 AbstractAuthenticationProcessingFilter의 failureHandler에서 onAuthenticationFailure() 호출



✅ OAuth 2.0 로그인 시 사용하는 방법들

서버 기반 인증 방식

  • 서버 측에 사용자의 정보 저장

  • Spring Security에는 별도 설정이 없다면 세션을 이용하여 처리

  • 사용자가 로그인을 하면 서버는 해당 사용자의 세션을 생성하고, 서버의 메모리와 DB에 저장한다.



Access Token

  • 리소스(사용자 정보)에 직접 접근할 수 있도록 해주는 정보만 가짐.

  • Refresh Token에 비해 짧은 만료 기간

  • 주로 세션에 담아 관리



Refresh Token

  • Acess Token을 발급받기 위한 정보를 담은 토큰

  • 클라이언트가 Access Token이 없거나 만료된 상태라면, Refresh Token을 통해 Auth Server에 요청하여 새 Access Token을 발급받을 수 있다.

  • 보통 외부에 노출되지 않기 위해 DB에 저장



OAuth 2.0

  • 각 사이트에서 제공하는 Authorization Server를 통해 회원 정보를 인증하고 Access Token을 발급받는다.

  • Access Token을 활용해서 직접 개발한 서버의 API 서비스를 이용하고 호출한다.

  • API 호출 요청에 대해 전달받은 Access Token이 유효한지 확인

    • 서버에서 유효성을 관리
    • API 호출 마다 Access Token이 유효한지 매번 DB에서 조회하고 새로 갱신 시 업데이트 작업을 해야 함.
  • 서버의 수가 많아지면 각 서버가 Access Token의 유효성 및 권한 확인을 Auth Server에 요청하기 때문에 병목 현상 등이 발생하여 서버의 부하로 이어질 수 있다.

    • 이러한 문제를 해소하기 위해 JWT 기반 인증 도입!



✅ JWT

  • JWT는 Claim 기반 방식 사용
    • Claim : 사용자에 대한 속성값들
  • JWT는 사용자의 상태를 가지는 의미있는 토큰으로 구성되어 있어, 기존에 Auth Server에 검증요청을 보내야 했던 과정들을 생략하고 각 서버에서 수행할 수 있게 되어 비용이 절감하고 Stateless 아키텍처를 구성할 수 있다.
  • 사용자 Auth Server 로그인
  • Auth Server에서 인증을 완료한 사용자는 JWT 토큰을 전달 받음
  • 클라이언트는 특정 애플리케이션 서버에 리소스를 요청할 때, 앞서 전달받은 JWT토큰을 Authorization Header에 넣어서 전달한다.
  • 애플리케이션 서버는 전달 받은 JWT 토큰의 유효성을 직접 검사하여 사용자 인증을 할 수 있다.

  • JWT는 확장성이 강점이다.
    • 세션을 사용하는 경우에는 특정 서버에서 로그인 인증을 받을 때 다른 서버에서는 로그인을 했는지 알 수 없다는 단점이 있다.
    • JWT는 토큰을 인증하는 방식을 알고 있따면 인증 과정에 문제가 없다.
    • 웹과 앱 간의 쿠키 세션 처리에도 유용하다. (브라우저와 앱의 쿠키 처리 방법이 다를 수 있어 JWT를 이용하는 것이 다양한 디바이스를 사용하는 측면에서는 좋다.)

  • 다만 인증정보가 필요한 요청을 보낼 때 헤더에 JWT 토큰 값을 넣어 보내야 하므로 데이터가 증가하여 네트워크 부하가 늘어날 수 있다.

  • 또한 토큰 자체에 사용자 정보를 담고 있기에 JWT가 만료되기 전에 탈취당하면 서버에서 처리당할 수 있는 일이 없다.

  • JWT는 만료 시간을 필수적으로 넣어야 한다.

    • 짧은 만료 시간 : Access Token
    • 긴 만료 시간 : Refresh Token
  • 보안성 / 편의성 관점에서 Trade Off로부터 적절한 협의점인 Refresh Token을 관리하는 방법이 택해진다.



✅ JWT 기반 토큰 인증

  • JwtAuthenticationFilter : JWT 토큰 인증 과정을 처리
    • 토큰 처리를 위한 필터를 구현해서 FilterChain에 추가해야 함
@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);
    }
    
    /* ... */

}
  • 클라이언트에서는 Authorization Header에 토큰을 담아 보내므로 HttpServletRequest에서 토큰을 추출한 후 검증하여 Authentication을 SecurityContext에 저장한다.
@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);
    }
    
    /* ... */

}
  • 실질적인 인증 과정이 일어나기 직전 위치인 LogoutFilter 직후 위치에 추가

  • JwtUtil : 토큰 검증 및 생성 과정 처리
@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 로그인을 정상적으로 완료한 사용자는 토큰이 없는 상태

    • 특정 URL로 리다이렉션 하여 Controller에서 JWT 기반 Access Token / Refresh Token을 발급 및 Response로 전달
  • 클라이언트는 발급받은 두 토큰을 안전한 공간에 보관

  • 매 요청마다 Authorization Header에 Access Token을 추가하여 전송하고, Access Token이 만료되면 Access Token 재 발급을 위해 Refresh Token을 사용한다.


  • Access Token 재발급을 위해 클라이언트가 Refresh Token을 Authorization Header에 추가하여 보낸 경우 Auth Server는 DB에 기록되어 있는 사용자의 Refresh Token과 동일한지 검증 하고 새 Access Token을 발급하여 Response로 전달한다.





profile
개발 공부!

0개의 댓글