[데브코스] Spring Security - 3
Updated: Categories: TILW16D2 - Session 관련 필터 / 인가처리에 대해 알아보자
SecurityContextPersistenceFilter
- 로그인 후 세션을 통해 SecurityContext를 관리해주는 필터.
- SecurityContextRepository인터페이스 구현체를 통해 사용자의- SecurityContext를 가져오거나 갱신함- 인증 관련 필터 중 최상단에 위치 — 이미 인증된 사용자는 다시 로그인할 필요가 없음
- SecurityContext가 존재하지 않는다면, empty- SecurityContext를 생성함
 
- SecurityContextRepository의 default 구현체는 세션을 이용하는- HttpSessionSecurityContextRepository클래스
SessionManagementFilter
- 사용자의 세션을 관리하는 필터
Session-fixation attack

- 악성 유저가 어떤 사용자의 로그인 전 세션을 탈취하여 가지고 있다면?
- 해당 사용자가 로그인 했다면 악성 사용자는 해당 세션을 가지고 똑같이 인증이 가능함..!
Session-fixation protection
- 위와 같은 세션 하이재킹을 막기 위해선 로그인 후에 세션을 새로 발급하면 된다.
- 로그인 전 세션은 쓸모가 없어지므로 해당 공격에 효과적으로 대응이 가능하다.

필터 사용법
http.sessionManagement()
    .invalidSessionUrl(URL) // 유효하지 않은 세션 감지 시 지정된 URL로 리다이렉트
    .sessionFixation().보호전략()
    .sessionCreationPolicy(SessionCreationPolicy.생성전략)
    .maximumSessions(세션 수)  // 동일 사용자의 최대 세션 수
        .maxSessionsPreventsLogin(bool값)   // 최대 세션 개수 도달시 로그인 가능 여부
        .and()
- 세션 보호 전략 4가지 옵션
    - .none(): 아무것도 하지 않음 (세션을 그대로 유지함)
- .newSession(): 새로운 세션을 만들고, 기존 데이터는 복제하지 않음
- .migrateSession(): 새로운 세션을 만들고, 데이터를 모두 복제함
- .changeSessionId(): 새로운 세션을 만들지 않지만, session-fixation 공격을 방어함 (단, servlet 3.1 이상에서만 지원)
 
- 세션 생성 전략 4가지 옵션
    - IF_REQUIRED: 필요시 생성함 (기본값)
- NEVER: Spring Security에서는 세션을 생성하지 않지만, 세션이 존재하면 사용함
- STATELESS: 세션을 완전히 사용하지 않음 (JWT 인증이 사용되는 REST API 서비스에 적합)
- ALWAYS: 항상 세션을 사용함
 
Authorization
- 인증된 사용자와 권한을 매핑해야 함
    - Spring Security에서는 보통 ‘역할’이라고 함 (예: ROLE_USER, ROLE_ADMIN, ROLE_ANONYMOUS)
 
- 보호되는 리소스에 대한 권한 확인
    - 관리자 권한을 가진 사용자만 관리자 페이지에 접근 가능
 

FilterSecurityInterceptor
- 필터체인 가장 마지막에 위치하며 사용자의 권한과 요청한 리소스의 권한을 비교, 접근가능 여부를 판단함
    - 실질적으로 접근 허용 여부 판단은 AccessDecisionManager인터페이스 구현체에서 이루어짐
 
- 실질적으로 접근 허용 여부 판단은 
- 해당 필터가 호출된다면 사용자는 이미 인증이 완료되어 있으므로 권한 목록을 가져올 수 있다!
    - 익명 사용자도 인증이 완료된 것으로 간주하며, ROLE_ANONYMOUS 권한을 가짐
 
- 보호되는 리소스에서 요구하는 권한 정보는 SecurityMetadataSource인터페이스를 통해ConfigAttribute타입으로 가져옴

AccessDecisionManager
- 사용자가 갖고 있는 권한과 리소스에서 요구하는 권한을 확인하고, 사용자가 적절한 권한을 갖고 있지 않다면 접근 거부 처리함
- AccessDecisionVoter목록을 갖고 있음
- AccessDecisionVoter들의 투표(vote)결과를 취합하고, 접근 승인 여부를 결정하는 3가지 구현체를 제공함- AffirmativeBased:- AccessDecisionVoter가 승인하면 이전에 거부된 내용과 관계없이 접근이 승인됨 (기본값)
- ConsensusBased: 다수의- AccessDecisionVoter가 승인하면 접근이 승인됨
- UnanimousBased: 모든- AccessDecisionVoter가 만장일치로 승인해야 접근이 승인됨
 
WebExpressionVoter
- SpEL(Spring Expression Language) 표현식을 사용해 접근 승인 여부에 대한 규칙을 지정할 수 있음
- SpEL 표현식 처리를 위해 DefaultWebSecurityExpressionHandler그리고WebSecurityExpressionRoot구현에 의존함
- WebSecurityExpressionRoot클래스는 SpEL 표현식에서 사용할수 있는 다양한 메소드를 제공
| 표현식 | 설명 | 예시 | 
|---|---|---|
| hasIpAddress(ipAddress) | 요청 IP 주소가 특정 IP 주소 또는 특정 대역에 해당하는지 확인 | access=”hasRole(‘ADMIN’) and hasIpAddress(‘192.168.1.0/24’)” | 
| hasRole(String role) | 사용자가 특정 role을 갖고 있는지 확인 | access=”hasRole(‘USER’)” | 
| hasAnyRole(String… roles) | 사용자가 주어진 role 목록 중 매칭되는 role을 갖고 있는지 확인 | access=”hasAnyRole(‘USER’, ‘ADMIN’)” | 
| hasAuthority(String authority) | 사용자가 특정 권한을 갖고 있는지 확인 | access=”hasAuthority(‘ROLE_USER’)” | 
| hasAnyAuthority(String… authorities) | 사용자가 주어진 권한 목록 중 매칭되는 권한을 갖고 있는지 확인 | access=”hasAnyAuthority(‘ROLE_USER’, ‘ROLE_ADMIN’)” | 
| permitAll | 모든 사용자에 대해 접근 허용 | access=”permitAll” | 
| denyAll | 모든 사용자에 대해 접근 거부 | access=”denyAll” | 
| isAnonymous() | 사용자가 익명 사용자인지 확인 | access=”isAnonymous()” | 
| isRememberMe() | 사용자가 remember-me 를 통해 인증되었는지 확인 | access=”isRememberMe() or hasRole(‘USER’)” | 
| isAuthenticated() | 사용자가 인증되었는지 확인 | access=”isAuthenticated()” | 
| isFullyAuthenticated() | 사용자가 익명 사용자가 아니고, remember-me 인증 사용자도 아닌지 확인 | access=”isFullyAuthenticated() and hasRole(‘USER’)” | 
- 기본적으로 제공하는 SpEL 표현식 외에 커스텀 표현식을 생성할 수 있다.
권한 규칙 커스텀
- 1. 표현식 추가 : WebSecurityExpressionRoot를 상속받은 클래스를 생성하고 커스텀 규칙 메소드를 추가한다- 해당 메소드는 반환값이 bool값이어야 함.
 
public class CustomWebSecurityExpressionRoot extends WebSecurityExpressionRoot {
    ...
    커스텀 규칙 메소드
    ...
}
- 2. 핸들러 추가 : AbstractSecurityExpressionHandler<FilterInvocation>를 상속받은 클래스를 생성한다- 표현식에 커스텀한 WebSecurityExpressionRoot를 지정해주기 위함.
- DefaultWebSecurityExpressionHandler와 거의 유사하게 만들어줌
 
- 표현식에 커스텀한 
public class CustomWebSecurityExpressionHandler extends AbstractSecurityExpressionHandler<FilterInvocation> {
    private final AuthenticationTrustResolver trustResolver;
    private final String defaultRolePrefix;
    public CustomWebSecurityExpressionHandler(AuthenticationTrustResolver trustResolver, String defaultRolePrefix) {
        this.trustResolver = trustResolver;
        this.defaultRolePrefix = defaultRolePrefix;
    }
    @Override
    protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, FilterInvocation fi) {
        CustomWebSecurityExpressionRoot root = new CustomWebSecurityExpressionRoot(authentication, fi);
        root.setPermissionEvaluator(this.getPermissionEvaluator());
        root.setTrustResolver(this.trustResolver);
        root.setRoleHierarchy(this.getRoleHierarchy());
        root.setDefaultRolePrefix(this.defaultRolePrefix);
        return root;
    }
}
- 3. 커스텀 핸들러 팩토리 메소드를 만들고 필터 체이닝에서 추가해준다.
public SecurityExpressionHandler<FilterInvocation> securityExpressionHandler() {
    return new CustomWebSecurityExpressionHandler(
            new AuthenticationTrustResolverImpl(),
            "ROLE_"
    );
}
http.authorizeRequests()
    ...
    .expressionHandler(securityExpressionHandler())
- 원래는 자동으로 DefaultWebSecurityExpressionHandler로 지정된다.
