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