<Spring Security> 기본 API 및 Filter 이해(2)

라모스·2022년 3월 8일
0

Spring Security🔐

목록 보기
2/6
post-thumbnail

세션 제어 필터

SessionManagementFilter

  1. 세션 관리: 인증 시 사용자의 세션정보를 등록, 조회, 삭제 등의 세션 이력을 관리
  2. 동시적 세션 제어: 동일 계정으로 접속이 허용되는 최대 세션수를 제한
  3. 세션 고정 보호: 인증 할 때마다 세션 쿠키를 새로 발급하여 공격자의 쿠키 조작을 방지
  4. 세션 생성 정책: Always, if_required, Never, Stateless

ConcurrentSessionFilter

  • 매 요청 마다 현재 사용자의 세션 만료 여부 체크
  • 세션이 만료되었을 경우 즉시 만료 처리
  • session.isExpired() == true
    • 로그아웃 처리
    • 즉시 오류 페이지 응답(This session has been expired)

SessionManagementFilter, ConcurrentSessionFilter 흐름도

  1. Login 시도
  2. 최대 세션 허용 개수 확인: 최대 세션 허용 개수가 초과되었을 경우 정책별 로직 수행(이전 사용자 세션 만료/현재 사용자 인증 실패) - session.expireNow()
  3. 이전 사용자가 자원 접근(Request) 시도
  4. ConcurrentSessionFilter에서 이전 사용자의 세션이 만료되었는지 확인: SessionManagementFilter 안의 설정 참조
  5. 로그아웃 처리 후 오류 페이지 응답: This session has been expired

Sequence Diagram

인가 API - 권한 설정 및 표현식

선언적 방식

  • URL: http.antMatchers("/users/**").hasRole("USER")
  • Method
    • @PreAuthorize("hasRole('USER')")
    • public void user() {System.out.println("user")}

동적 방식 - DB 연동 프로그래밍

  • URL
  • Method

권한 설정

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .antMatcher("/shop/**") // 특정 경로를 지정 해당 메서드를 생략하면 모든 경로에 대해 검색함
        .authorizeRequests() // 보안 검사 기능 시작
          .antMatchers("/shop/login", "/shop/users/**").permitAll() // 해당 경로에 모든 접근을 허용
          .antMatchers("/shop/mypage").hasRole("USER") // USER 권한을 가지고 있는 사용자에게만 허용
          .antMatchers("/shop/admin/pay").access("hasRole('ADMIN')")
          .antMatchers("/shop/admin/**).access("hasRole('ADMIN') or hasRole('SYS ')")
          .anyRequest().authenticated();
}

📌 주의사항
설정 시 구체적인 경로("/shop/admin/pay")가 먼저 설정되고 그 다음 더 넓은 범위가 설정되야 한다. 이는 불필요한 검사를 막기 위해서다. 예를 들어, .antMatchers("/shop/admin/**).access("hasRole('ADMIN') or hasRole('SYS ')")가 먼저 설정된다면, SYS 유저는 해당 검사를 통과하고 그 아래(좁은 범위)에서 걸리게 된다.

인가 API - 표현식

메소드동작
authenticated()인증된 사용자의 접근을 허용
fullyAuthenticated()인증된 사용자의 접근을 허용, rememberMe 인증 제외
permitAll()무조건 접근을 허용
denyAll()무조건 접근을 허용하지 않음
anonymous()익명사용자의 접근을 허용
rememberMe()기억하기를 통해 인증된 사용자의 접근을 허용
access(String)주어진 SpEL 표현식의 평가 결과가 true이면 접근을 허용
hasRole(String)사용자가 주어진 역할이 있다면 접근을 허용
hasAuthority(String)사용자가 주어진 권한이 있다면 접근을 허용
hasAnyRole(String...)사용자가 주어진 권한이 있다면 접근을 허용
hasAnyAuthority(String...)사용자가 주어진 권한 중 어떤 것이라도 있다면 접근을 허용
hasIpAddress(String)주어진 IP로부터 요청이 왔다면 접근 허용

예제 코드

@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    private final UserDetailsService userDetailsService;
    
    // 메모리 방식으로 사용자 생성 및 비밀번호와 권한 설정(실제로는 이렇게 하면 안됨)
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("{noop}1111").roles("USER");
        auth.inMemoryAuthentication().withUser("sys").password("{noop}1111").roles("SYS");
        auth.inMemoryAuthentication().withUser("admin").password("{noop}1111").roles("ADMIN");
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers().hasRole("USER")
            .antMatchers("/shop/admin/pay").access("hasRole('ADMIN')")
            .antMatchers("/shop/admin/**).access("hasRole('ADMIN') or hasRole('SYS ')")
            .anyRequest().authenticated();
        http
            .formLogin();
    }
}
@RestController
public class SecurityController {
    
    @GetMapping("/")
    public String index() {
        return "home";
    }
    
    @GetMapping("/loginPage")
    public String loginPage() {
        return "loginPage";
    }
    
    @GetMapping("/user")
    public String user() {
        return "user";
    }
    
    @GetMapping("/admin/pay")
    public String adminPay() {
        return "adminPay";
    }
    
    @GetMapping("/admin/**")
    public String adminAll() {
        return "admin";
    }
}

예외 처리 및 요청 캐시 필터

ExceptionTranslationFilter, RequestCacheAwareFilter

Spring Security가 관리하는 보안 필터 중 마지막 필터가 FilterSecurityInterceptor이고, 바로 전 필터가 ExceptionTranslationFilter이다. 해당 필터에서 사용자의 요청을 받을 때, 그 다음 필터로 해당 요청을 전달할 때 try-catch로 감싸서 FilterSecurityInterceptor를 호출하고 있고, 해당 필터에서 생기는 인증 및 인가 예외는 ExceptionTranslationFilterthrow 하고 있다.

AuthenticationException

  • 인증 예외 처리
    1. AuthenticationEntryPoint 호출
      → 로그인 페이지 이동, 401 오류 코드 전달 등
    2. 인증 예외가 발생하기 전의 요청 정보를 저장
      RequestCache: 사용자의 이전 요청 정보를 세션에 저장하고 이를 꺼내오는 캐시 매커니즘
      SavedRequest: 사용자가 요청했던 request 파라미터 값들, 그 당시의 헤더값들 등이 저장

AccessDeniedException

  • 인가 예외 처리
    • AccessDeniedHandler에서 예외 처리하도록 제공

흐름도

  1. 익명 사용자가 /user 에 접근을 시도한다고 가정
  2. FilterSecurityInterceptor 권한 필터가 해당 요청(/user)을 받았지만, 해당 유저는 인증을 받지 않은 상태
  3. 해당 필터는 인증 예외를 발생. → 정확히는 인가 예외를 던진다. 해당 사용자는 익명 사용자이기에 인증을 받지 않은 상태라서 인가 예외(AccessDeniedException)로 빠진다.
  4. 인가 예외(AccessDeniedException)는 익명 사용자이거나 RememberMe 사용자일 경우 AccessDeniedHandler를 호출하지 않고 AuthenticationException에서 처리하는 로직으로 보내게 된다.
  5. 인증 예외(AuthenticationException) 는 두 가지 일을 한다.
    AuthenticationEntryPoint 구현체 안에서 login 페이지로 리다이렉트 한다.(인증 실패 이후) Security Context를 null로 초기화 해주는 작업도 해준다.
    → 예외 발생 이전에 유저가 가고자 했던 요청정보를 DefaultSavedRequest 객체에 저장하고 해당 객체는 Session에 저장되며 Session에 저장하는 역할을 HttpSessionRequestCache에서 해준다.

  1. 인증 절차를 밟은 일반 유저가 /user 자원에 접근을 시도하는데 해당 자원에 설정된 허가 권한이 ADMIN일 경우
  2. 권한이 없기 때문에 인가 예외 발생
  3. AccessDeniedException이 발생
  4. AccessDeniedHandler를 호출해서 후속 작업을 처리 (일반적으로 denied 페이지로 이동)

API

protected void configure(HttpSecurity http) throws Exception {
    //...(중략-위 예제코드와 동일)
    http.formLogin()
        .successHandler({
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request,
                                                HttpServletResponse response,
                                                Authentication authentication)
                                        throws IOException, ServletException {
                RequestCache requestCache = new HttpSessionRequestCache();
                SavedRequest savedRequest = requestCache.getRequest(request, response);
                String redirectUrl = savedRequest.getRedirectUrl();
                response.sendRedirect(redirectUrl);
            
            }
        });
    
    http.exceptionHandling() // 예외 처리 기능이 작동함
         .authenticationEntryPoint(new AuthenticationEntryPoint() {
             @Override
             public void commence(HttpServletRequest request,
                                  HttpServletResponse response,
                                  AuthenticationException authException)
                          throws IOException, ServletException {
                 response.sendRedirect("/login");
             }
         }) // 인증 실패 시 처리
         .accessDeniedHandler(new AccessDeniedHandler() {
             @Override
             public void handle(HttpServletRequest request,
                                  HttpServletResponse response,
                                  AccessDeniedException accessDeniedEception)
                          throws IOException, ServletException {
                 response.sendRedirect("/denied");
             }
         }); // 인증 실패 시 처리
}
  • onAuthenticationSuccess: SavedRequest 객체에 RequestCache 객체가 담고 있는 사용자가 원래 가려던(요청하려던) 자원의 요청정보를 가져와 활용할 수 있도록 한다.
  • AuthenticationEntryPoint: 인증 예외 발생시 수행 메소드(commence()) 오버라이딩. 해당 코드에서는 login 페이지로 이동시키지만 다른 로직을 수행할 수 있음.
  • AccessDeniedHandler: 인가 예외 발생시 처리 로직 수행. 해당 코드에서는 denied 페이지로 이동시키지만 별도로 다른 로직을 수행할 수 있음.

Sequence Diagram

CSRF(사이트 간 요청 위조)

사용자가 사이트에 접속하여 로그인 후 쿠키를 발급받은 뒤 공격자가 사용자의 이메일로 특정 링크를 전달하고 사용자가 해당 링크를 클릭하게 되면, 공격용 웹페이지에 접속하게 되고, 해당 페이지에 '로또 당첨'이라는 이미지가 노출된다. 유저가 이 이미지를 클릭하면 사이트에 특정 URL로 요청하게 되는데 해당 쿠키정보를 가지고 있기 때문에 해당 요청에 대해 정상적으로 동작을 하게 된다.

이처럼 사용자의 의도와는 무관하게 공격자가 심어놓은 특정 방식을 통해 자원 요청을 하게 되고 그것을 응답 받을 수 있도록 하는 것을 CSRF(사이트 간 요청 위조)라 한다.

CsrfFilter

  • 모든 요청에 랜덤하게 생성된 토큰을 HTTP 파라미터로 요구
  • 요청 시 전달되는 토큰 값과 서버에 저장된 실제 값과 비교한 후 일치하지 않으면 요청은 실패
  • Client
    • HTTP 메소드: PATCH, POST, PUT, DELETE
<input type="hidden" name="${csrf.parameterName}" value="${_csrf.token}"/>
  • Spring Security
    • http.csrf(): 기본 활성화 되어 있음
    • http.csrf().disabled(): 비활성화

References

profile
Step by step goes a long way.

0개의 댓글