1. 스프링 시큐리티란?
스프링 기반 애플리케이션의 인증(Authentication)과 권한(Authorization)을 처리하는 보안 프레임워크다.
2. 주요 개념
2-1) 인증(Authentication)
사용자 요청을 처리하려면 사용자가 누구인지 인증을 해야한다.
사용자의 신원 확인을 하는 것!
* 인터페이스
Authentication : 현재 인증된 사용자의 정보를 담은 객체.
AuthenticationManager : 인증을 처리하는 주요 엔트리 포인트다.
SecurityContext : 현재 애플리케이션의 보안 컨텍스트. 인증된 사용자 정보를 담는다.
*클래스
SecurityContextHolder : 현재 스레드의 SecurityContext를 관리하는 헬퍼 클래스.
UsernamePasswordAuthenticationToken : Authentication 인터페이스의 구현체. 사용자 이름, 비밀번호 기반 인증 정보를 저장.
2-2) 인가(Authorization)
인증된 사용자가 리소스(URL, API 등)에 접근할 수 있는지 결정한다.
사용자가 요청을 하면 리소스에 접근 가능한지 확인해야한다.
*인터페이스
GrantedAuthority : 역할(Role)이나 권한(Permission)을 나타냄.
AccessDecisionManager : 사용자가 요청한 리소스에 접근할 수 있는지 결정하는 핵심 컴포넌트.
AccessDecisionVoter : AccessDecisionManager에서 호출되어 인가여부를 판단하는 역할.
*클래스
SimpleGrantedAuthority : GrantedAuthority 인터페이스의 기본 구현체. 역할 또는 권한 이름을 문자열로 저장.
FilterSecurityIntercepter : HTTP 요청을 가로채 인증 및 인가를 수행하는 주요 필터.
2-3) 사용자 정보 관리
사용자의 기본 정보를 관리하는 객체다.
*인터페이스
UserDetails : 사용자의 기본 정보를 제공. (ex. 이름, 권한 등)
UserDetailsService : 사용자 정보를 로드하는 핵심 인터페이스. DB나 기타 저장소에서 사용자 정보를 조회.
*클래스
User : UserDetails 인터페이스의 기본 구현체.
InMemoryUserDetailsManager : 메모리에 사용자 정보를 저장하고 관리하는 UserDetailsService의 구현체.
2-4) 필터 체인
요청(Request)가 애플리케이션에 도달하기 전에 여러 시큐리티 필터를 통해 인증 및 인가를 처리한다.
사용자가 요청한 리소스에 접근할 수 있는지 검증하는 단계에서 필요하다.
*인터페이스
Filter : HTTP 요청을 처리.
SecurityFilterChain : 스프링 시큐리티의 필터 체인을 구성하는 객체.
*클래스
UsernamePasswordAuthenticationFilter : 사용자 이름과 비밀번호 기반 로그인 요청을 처리하는 기본 필터
FilterSecurityInterceptor : 최종적으로 인가를 처리하는 필터.
2-5) 역할 기반 접근 제어(RBAC)
사용자의 역할(Role)과 리소스 간 매핑을 통해 접근 권한을 부여하는 접근 제어 방식이다.
2-6) 정책 기반 접근 제어(PBAC)
사용자 상태, 요청 정보, 시간 등의 동적 조건을 기반으로 접근 권한을 결정하는 접근 제어 방식이다.
사용자 요청 처리와 관리자 승인을 통해 동적으로 권한을 부여하려면 PBAC를 구현해야한다.
2-7) 기타 핵심 개념
*인터페이스
AuthenticationProvider : 특정 인증방식(ex. OAuth2.0 등)을 처리하는 컴포넌트.
*클래스
HttpSecurity : 인증/인가 규칙을 설정하는 DSL
WebSecurityConfigurerAdapter : 보안 설정을 구성하는 데 사용되는 추상 클래스
BCryptPasswordEncoder : 비밀번호를 암호화하기 위한 기본 제공 클래스.
3. 스프링 시큐리티 설정
3-1) 요구사항
▪️ 강력한 인증
: 비밀번호 암호화 및 강도 설정, 다중인증(MFA)
▪️ 역할 및 권한 관리
: URL 접근 제어, 세부 권한 기반 접근 제어.
▪️ CSRF 보호
: CSRF 공격방지
▪️ 세션 관리
: 동시 세션 제한, 세션 고정 공격 방지
▪️ HTTP 보안
: HTTPS 강제, Content Security Policy(CSP) 설정, XSS 및 클릭재킹 방지.
▪️ 로깅 및 모니터링
: 인증 및 인가 이벤트 로깅.
▪️ 다양한 인증 방식을 고려
: OAuth2.0, 클라이언트 인증, ID/PASSWORD 등.
3-2) 설정 예제
위 요구사항을 고려한 SecurityConfig 전체 코드다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private CustomOAuth2UserService oAuth2UserService;
@Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 1. 인증 및 인가
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN") // 관리자만 접근
.antMatchers("/user/**").hasRole("USER") // 사용자만 접근
.antMatchers("/oauth2/**").permitAll() // 민간 인증서 URL
.antMatchers("/cert/**").permitAll() // 공동 인증서 URL
.antMatchers("/public/**").permitAll() // 공개 URL
.anyRequest().authenticated() // 나머지 인증 필요
.and()
// 2. 로그인 및 로그아웃
.formLogin()
.loginPage("/login") // 사용자 정의 로그인 페이지
.defaultSuccessUrl("/home") // 로그인 성공 후 리디렉션
.failureUrl("/login?error=true") // 로그인 실패 시 리디렉션
.permitAll()
.and()
.oauth2Login()
.loginPage("/login") // 동일한 로그인 페이지
.defaultSuccessUrl("/home") // 로그인 성공 후 리디렉션
.failureUrl("/login?error=true") // 실패 시 리디렉션
.clientRegistrationRepository(clientRegistrationRepository()) // OAuth2 클라이언트 등록
.authorizedClientService(authorizedClientService()) // 인증된 클라이언트 저장소
.userInfoEndpoint().userService(oAuth2UserService) // 민간 인증서
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true) // 세션 무효화
.deleteCookies("JSESSIONID") // 쿠키 삭제
.and()
// 3. CSRF 보호
.csrf()
.ignoringAntMatchers("/api/**") // REST API 제외
.and()
// 4. HTTPS 강제 및 HTTP 보안
.requiresChannel()
.anyRequest().requiresSecure() // HTTPS 강제
.and()
.headers()
.contentSecurityPolicy("default-src 'self'; script-src 'self' https://trusted.com")
.and()
.xssProtection().block(true) // XSS 방지
.and()
.frameOptions().deny() // 클릭재킹 방지
.httpStrictTransportSecurity() // HSTS 설정
.includeSubDomains(true)
.maxAgeInSeconds(31536000) // 1년
.and()
// 5. 세션 관리
.sessionManagement()
.maximumSessions(1) // 동시 세션 1개 제한
.maxSessionsPreventsLogin(true) // 중복 로그인 차단
.expiredUrl("/session-expired") // 세션 만료 시 리디렉션
.and()
.sessionFixation().migrateSession(); // 세션 고정 공격 방지
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider) // 공동 인증서
.userDetailsService(userDetailsService) // ID/Password
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 비밀번호 암호화 강도 설정
}
}
3-3) 설정 코드 상세 설명
1. 인증 및 인가
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN") // ADMIN 역할만 접근 허용
.antMatchers("/user/**").hasRole("USER") // USER 역할만 접근 허용
.antMatchers("/oauth2/**").permitAll() // 민간 인증서 관련 URL은 인증 없이 접근 가능
.antMatchers("/cert/**").permitAll() // 공동 인증서 관련 URL은 인증 없이 접근 가능
.antMatchers("/public/**").permitAll() // 공개 URL은 인증 없이 접근 가능
.anyRequest().authenticated() // 나머지 모든 요청은 인증 필요
URL 경로별로 접근 제어 정책을 설정한다.
authorizeRequests()
URL 패턴별로 접근 권한을 정의한다.
URL 보호 설정의 시작점이다.
antMatchers(String)
특정 URL 패턴에 대해 권한을 설정.
hasRole(String)
지정된 역할(Role)을 가진 사용자만 접근 가능.
permitAll()
누구가 접근 가능.
anyRequest().authenticated()
인증된 사용자만 접근 가능
2. 로그인 및 로그아웃
.formLogin()
.loginPage("/login") // 사용자 정의 로그인 페이지
.defaultSuccessUrl("/home") // 로그인 성공 후 리디렉션
.failureUrl("/login?error=true") // 로그인 실패 시 리디렉션
.permitAll()
.and()
.oauth2Login()
.loginPage("/login") // 동일한 로그인 페이지
.defaultSuccessUrl("/home") // 로그인 성공 후 리디렉션
.failureUrl("/login?error=true") // 실패 시 리디렉션
.clientRegistrationRepository(clientRegistrationRepository()) // OAuth2 클라이언트 등록
.authorizedClientService(authorizedClientService()) // 인증된 클라이언트 저장소
.userInfoEndpoint().userService(oAuth2UserService) // 민간 인증서
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true) // 세션 무효화
.deleteCookies("JSESSIONID") // 쿠키 삭제
ID/PASSWORD로 기본 로그인을 설정, OAuth2 인증을 설정하고 로그아웃 후 세션과 쿠키를 정리한다.
formLogin()
기본 로그인 설정
oauth2Login()
OAuth2 인증 설정
logout()
로그아웃 URL과 후속 동작을 정의
3. CSRF 보호
.csrf()
.ignoringAntMatchers("/api/**") // REST API 제외
csrf()
기본적으로 CSRF 보호가 활성화되고 REST API(/api/**) 경로는 CSRF 보호를 비활성화 한다.
4. HTTPS 강제 및 HTTP 보안
.requiresChannel()
.anyRequest().requiresSecure() // HTTPS 강제
.and()
.headers()
.contentSecurityPolicy("default-src 'self'; script-src 'self' https://trusted.com")
.and()
.xssProtection().block(true) // XSS 방지
.and()
.frameOptions().deny() // 클릭재킹 방지
.httpStrictTransportSecurity() // HSTS 설정
.includeSubDomains(true)
.maxAgeInSeconds(31536000) // 1년
HTTPS를 강제하고 브라우저 보안 헤더를 설정한다.
requiresSecure()
모든 요청에서 HTTPS를 강제.
contentSecurityPolicy()
스크립트 및 리소스의 허용 도메인을 제한.
httpStrictTransportSecurity()
HTTPS만 허용
includeSubDomains(true)
HSTS(HTTP Strict Transport Security) 설정에서 현재 도메인뿐 아니라 모든 서브도메인에서도 HTTPS를 강제.
서브도메인도 HTTPS만 허용하도록 해서 보안성을 강화.
maxAgeInSeconds()
HSTS 정책이 브라우저에 적용되는 유효 기간을 설정한다.
브라우저는 설정된 기간 동안 HTTP 연결 시 HTTPS로 리디렉션.
💡HSTS 유효기간에 대한 Q&A
Q1. 그냥 이 시스템 자체를 유효기간없이 HTTPS로 하면 좋을 텐데 유효기간이 필요한 이유는?
A1. 브라우저와 서버 간의 통신 환경의 특성과 유연성 때문이다.
- HTTPS 인증서의 만료 및 갱신
HTTPS 인증서가 만료되거나 인증서 교체 중 문제가 생기면 브라우저가 HTTPS 연결을 거부한다.
- 실수로 잘못된 설정 적용
테스트 서버에 HSTS를 활성화하고, 이 서버에 실제 트래픽이 유입되면 테스트 환경과 프로덕션 환경 모두에 문제가 생길 수 있다. 유효기간이 없다면, 실수로 설정한 HSTS 정책을 되돌리기 어려워질 수 있다.
- 도메인 소유권 변경
도메인이 다른 조직으로 넘어가 HTTPS를 지원하지 않으면, 브라우저가 HTTP 접근을 계속 HTTPS로 강제하기 때문에 사용자가 불편을 겪을 수 있다.
Q2. 유효 기간 만료 전에 개발자가 문제가 되는 설정을 수정할 수 없는 이유?
A2. HSTS는 브라우저 캐시에 의존한다.
브라우저는 서버가 제공한 Strict-Transport-Security 헤더를 캐싱하고 캐싱된 정책은 유효 기간 동안 브라우저 내부에서 강제 된다. HSTS는 브라우저가 서버와 통신하기 전에 동작하기 때문에 개발자가 서버에서 정책을 제거해도 이미 캐싱된 브라우저는 이를 무시한다.
Q3. HSTS을 실수로 설정했을 때 해결방법이 있을 까?
A3. 사용자가 직접 브라우저 측에서 캐시를 수동 삭제해야한다.
Q4. 개발 및 테스트 환경 팁을 알려줘!
A4.
- 개발 및 테스트 서버에서는 HSTS를 설정하지 않는 것이 좋다.
설정이 필요하면, 내부 네트워크에서만 적용하고 캐싱을 짧게 설정한다.
- preload는 신중하게 사용
HSTS를 영구적으로 적용하는 preload옵션은 신중하게 활성화해야한다.
HSTS preload 리스트에서 도메인을 제거하기 위해 HSTS Preload Submission Removal 요청을 보낼 수 있다.
하지만, 제거에는 2~4주가 소요되고 이후 브라우저 업데이트를 통해 반영되니 조심 또 조심하자.
- 짧은 유효 기간 설정
처음에는 짧은 유효기간(ex.일주일)을 설정하고, 안정성이 확인되면 유효 기간을 늘려간다.
5. 세션 관리
.sessionManagement()
.maximumSessions(1) // 동시 세션 1개 제한
.maxSessionsPreventsLogin(true) // 중복 로그인 차단
.expiredUrl("/session-expired") // 세션 만료 시 리디렉션
.and()
.sessionFixation().migrateSession(); // 세션 고정 공격 방지
sessionFixation().migrateSession()
로그인 후 세션 ID를 변경하여 세션 고정 공격을 방지한다.
6. 사용자 인증 처리 로직 정의
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider) // 공동 인증서
.userDetailsService(userDetailsService) // ID/Password
.passwordEncoder(passwordEncoder());
}
ID/Password 인증, 공동 인증서 인증 등 여러 인증 방식을 처리하는 로직을 정의한다.
userDetailService()
사용자 정보를 데이터베이스 또는 다른 저장소에서 로드.
(사용자 정보를 가져오기 위해 CustomUserDetailsService를 구현해야함.)
authenticationProvider()
인증 제공자 등록
사용자 이름/비밀번호 외의 인증방식을 처리.
(어떤 인증 제공자를 등록할지는 CustomAuthenticationProvider는 직접 구현해야함.)
passwordEncoder()
비밀번호 암호화.
7. 비밀번호 암호화
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // 비밀번호 암호화 강도 설정
}
비밀번호를 암호화하고 강도를 설정한다.
BCryptPasswordEncoder()
비밀번호 암호화 기능
암호화 강도의 기본값은 10이다.
강도는 해시 알고리즘의 연산 횟수를 결정하는 로그 라운드 값이다.
강도10 ->1024번 연산
강도12 -> 4096번 연산
강도가 높아질수록, 보안성이 증가하고 성능이 저하된다.
- 해시 생성에 시간이 더 걸리므로 공격자가 무차별 대입공격을 시도할 때 더 많은 시간이 소요된다.
- 서버에서 비밀번호를 비교하는 시간이 증가한다.
- 사용자 수가 많거나 요청이 많아지면, 높은 강도는 서버 부하를 증가 시킬 수 있다.
3-4) 보안 개념
1. CSP (Content Security Policy)
브라우저에서 로드할 리소스의 출처를 제한하는 HTTP 헤더.
악의적인 스크립트 실행을 방지하고, XSS 공격으로부터 보호할 수 있다.
2. MFA (Multi-Factor Authentication)
사용자가 로그인을 할 때, 두 가지 이상의 인증 요소를 사용하는 방식
3. XSS (Cross-Site-Scripting)
웹사이트에서 악성 스크립트를 삽입하여 사용자의 데이터를 탈취하는 공격
4. 클릭재킹
악성 웹페이지가 사용자를 속여 다른 웹사이트의 버튼이나 링크를 클릭하게 하는 공격
5. 세션 고정 공격
공격자가 사용자의 세션 ID를 탈취하여 인증된 세션을 가로채는 공격
6. CSRF (Cross-Site Request Forgery)
사용자가 인증된 상태에서 공격자가 의도한 요청을 실행하게 하는 공격
'Spring' 카테고리의 다른 글
[스프링 시큐리티] 역할기반접근제어(RBAC)와 정책기반접근제어(PBAC) (0) | 2024.11.16 |
---|