pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
📌 참고: 랜덤으로 생성되는 문자열 password을 고정 값으로 설정할 수 있다.
application.properties
에 다음과 같이 기본 username/password 값을 설정할 수 있다.spring.security.user.name=user spring.security.user.password=1234
WebSecurityConfigurerAdapter
: 스프링 시큐리티의 웹 보안 기능 초기화 및 설정
HttpSecurity
라는 세부적인 보안 기능을 설정할 수 있는 API를 제공한다.인증 API | 인가 API(chain method) |
---|---|
http.formLogin() | http.authorizeRequests().anyMatchers(/admin) |
http.logout() | http.authorizeRequests().hasRole(USER) |
http.csrf() | http.authorizeRequests().permitAll() |
http.httpBasic() | http.authorizeRequests().authenticated() |
http.SessionManagement() | http.authorizeRequests().fullyAuthentication() |
http.RememberMe() | http.authorizeRequests().access(hasRole(USER)) |
http.ExceptionHandling() | http.authorizeRequests().denyAll() |
http.addFilter() |
다음과 같이 SecurityConfig
설정 클래스를 만들어 인증/인가 API를 추가하여 보안성을 높일 수 있다.
@Configuration
@EnableWebSecurity // 웹 보안 활성화를 위한 annotation
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests() // 요청에 의한 보안 검사 시작
.anyRequest().authenticated() // 어떤 요청에도 보안 검사를 한다.
.and()
.formLogin(); // 보안 검증은 formLogin 방식으로 하겠다.
}
}
@EnableWebSecurity
애노테이션을 WebSecurityConfigurerAdapter
를 상속하는 설정 객체에 붙여주면 SpringSecurityFilterChain에 등록된다.📌 참고:
@EnableWebSecurity
스프링 MVC에서 웹 보안을 활성화하기 위한 애노테이션으로 핸들러 메소드에서@AuthenticationPrincipal
애노테이션이 붙은 매개변수를 이용해 인증 처리를 수행한다. 그리고 자동으로 CSRF 토큰을 스프링의 form binding tag library를 사용해 추가하는 빈을 설정한다.
로직 플로우는 다음과 같다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/loginPage") // 사용자 정의 로그인 페이지
.defaultSuccessUrl("/") // 로그인 성공 후 이동 페이지
.failureUrl("/login") // 로그인 실패 후 이동 페이지
.usernameParameter("userId") // 아이디 파라미터명 설정
.passwordParameter("passwd") // 패스워드 파라미터명 설정
.loginProcessingUrl("/login_proc") // 로그인 Form Action Url
.successHandler(new AuthenticationSuccessHandler() { // 로그인 성공 후 핸들러
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("authentication = " + authentication.getName());
response.sendRedirect("/");
}
})
.failureHandler(new AuthenticationFailureHandler() { // 로그인 실패 후 핸들러
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System.out.println("exception = " + exception.getMessage());
response.sendRedirect("/login");
}
})
.permitAll(); // 사용자 정의 로그인 페이지 접근 권한 승인
}
}
로그인 인증처리를 담당하고 인증처리에 관련된 요청을 처리하는 필터이다.
Login Form 인증 로직 플로우는 다음과 같다.
/login
으로 시작하는지 확인한다.chain.doFilter
)로 진행된다./login
URL은 .loginProcessingUrl()
로 변경 가능하다.Authentication
에서 실제 인증처리를 하게 되는데, 로그인 페이지에서 입력한 Username과 Password를 인증객체(Authentication
)에 저장해서 인증처리(AuthenticationManager
)를 맡기는 역할을 한다.AuthenticationManager
)는 내부적으로 AuthenticationProvider
에게 인증처리를 위임하게 된다. 해당 Provider가 인증처리를 담당하는 클래스로써 인증 성공/실패를 반환하는데 실패할 경우, AuthenticationException
예외를 반환하여 UsernamePasswordAuthenticationFilter
로 돌아가 예외처리를 수행하고, 인증에 성공할 경우 Authentication
객체를 생성하여 User 객체와 Authorities 객체를 담아 AuthenticationManager
에게 반환한다.AuthenticationManager
는 Provider로부터 반환받은 인증객체(인증결과 유저, 유저 권한정보)를 SecurityContext
객체에 저장한다.SecurityContext
는 Session에도 저장되어 전역적으로 SecurityContext
를 참조할 수 있다.SuccessHandler
에서 인증 성공 이후의 로직을 수행하게 된다.인증처리 필터(UsernamePasswordAuthenticationFilter
)는 Form 인증처리를 하는 필터로써 해당 필터는 크게 두 가지로 인증 전과 인증 후의 작업들을 관리한다.
인증처리 전에는 사용자 인증 정보를 담아서 전달하면서 인증 처리를 맡기고(AuthenticationManager
) 성공한 인증객체를 반환받아서 전역적으로 인증 객체를 참조할 수 있도록 설계된 SecurityContext
에 저장하고, 그 이후 SuccessHandler
를 통해 인증 성공 후의 후속 작업들을 처리한다.
// AbstractAuthenticationProcessingFilter.java
의 doFilter()
, attempAuthentication()
, ProviderManager.java
를 확인해보자.
/logout
리소스 호출@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.logout() // 로그아웃 처리
.logoutUrl("/logout") // 로그아웃 처리 URL
.logoutSuccessUrl("/login") // 로그아웃 성공 후 이동 페이지
.addLogoutHandler(new LogoutHandler() { // 로그아웃 핸들러
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
HttpSession session = request.getSession();
session.invalidate();
}
})
.logoutSuccessHandler(new LogoutSuccessHandler() { // 로그아웃 성공 후 핸들러
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect("/login");
}
})
.deleteCookies("remember-me"); // 로그아웃 후 쿠키 삭제
}
}
SecurityContext
에서 인증 객체(Authentication
)를 꺼내온다.SecurityContextLogoutHandler
에서 세션 무효화, 쿠키 삭제, clearContext()
를 통해 SecurityContext
객체를 삭제하고 인증 객체도 null로 만든다.SimpleUrlLogoutSuccessHandler
를 통해 로그인 페이지로 리다이렉트 시킨다.@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.rememberMe() // Remember-Me 기능 작동
.rememberMeParameter("remember") // 기본 파라미터명은 remember-me
.tokenValiditySeconds(3600) // Default는 14일
.alwaysRemember(true) // 리멤버 미 기능이 활성화되지 않아도 항상 실행
.userDetailsService(userDetailsService);
}
}
RememberMeAuthenticationFilter
가 동작RememberMeService
인터페이스의 구현체 동작TokenBasedRememberMeService
: 메모리에서 저장한 토큰과 사용자가 가져온 토큰을 비교(default는 14일 보존)하는 구현체PersistentTokenBasedRememberMeService
: DB에 발급한 토큰과 사용자가 가져온 토큰을 비교해서 인증 처리하는 구현체AuthenticationManager
인증 관리자에게 전달하여 인증처리 수행isAnonymous()
와 isAuthenticated()
로 구분해서 사용A 컴퓨터에서 로그인하여 서비스를 사용하다가 태블릿PC 혹은 다른 컴퓨터 등에서 같은 서비스를 이용하기 위해 로그인을 시도할 수 있다. 하지만 이런 다중 접속시도가 무한정 허용될 경우, 여러 문제점을 발생시킬 수 있다. 다중 로그인을 허용해 동시 접속이 된다면, 한 명이 서비스 결제를 한 뒤 모두가 공유해서 보는 문제가 발생하는 것이 그 예시이다.
Netflix같은 OTT 서비스에선 다중 접속의 경우 인원 제한을 두고 과금 모델을 만들어 제한적인 동시 세션을 허용해주고 있다. Spring Security에서는 이런 세션에 대한 관리 기능도 다음과 같이 제공한다.
ALWAYS
, IF_REQUIRED
, NEVER
, STATELESS
동시 세션 제어란 같은 계정(세션)을 동시에 몇 개까지 유지할 수 있게 할 지에 대한 제어를 의미한다.
기존 접속해있는 계정이 있다고 할 때, 새로운 사용자가 동일한 계정으로 접속을 시도했을 때 어떻게 대응할지에 대한 방법으로 기존 사용자를 로그아웃 시키거나 현재 사용자가 접속을 할 수 없게 막는 식이다.
최대 세션 허용 개수를 초과하였을 경우의 처리 로직 전략 2가지
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement() // 세션 관리 기능이 작동함
.maximumSessions(1) // 최대 허용 가능 세션 수, -1: 무제한 로그인 세션 허용
.maxSessionsPreventsLogin(true) // 동시 로그인 차단함, false: 기존 세션 만료(default)
.invalidSessionUrl("/invalid") // 세션이 유효하지 않을 때 이동할 페이지
.expiredUrl("/expired"); // 세션이 만료된 경우 이동할 페이지
}
}
사용자가 공격자 세션 쿠키로 로그인을 시도하더라도 로그인시마다 새로운 세션ID를 발급하여 제공하게 되면, JSESSIONID
가 다르기 때문에, 공격자는 같은 쿠키값으로 사용자 정보를 공유받을 수 없게 된다.
📌 세션 고정 공격?
공격자가 서버에 접속해서JSESSIONID
를 발급받아 사용자에게 자신이 발급받은 세션 쿠키를 심어놓게 되면 사용자가 세션 쿠키로 로그인을 시도했을 경우 공격자는 같은 쿠키값으로 인증되어 있기때문에 공격자는 사용자 정보를 공유하게 된다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionFixation().changeSessionId(); // 기본값: 세션은 유지하되 세션 아이디는 계속 새로 발급
// (Servlet 3.1 이상 기본값)
// none, migrateSession, newSession
}
}
none()
: 세션이 새로 생성되지 않고 그대로 유지되기 때문에 세션 고정 공격에 취약하다.migrateSession()
: 새로운 세션도 생성되고 세션아이디도 발급된다. 추가로 이전 세션의 속성값들도 유지된다.newSession()
: 세션이 새롭게 생성되고, 세션아이디도 발급되지만, 이전 세션의 속성값들을 유지할 수 없다.@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.If_Required);
}
}
SessionCreationPolicy.Always
: 스프링 시큐리티가 항상 세션 생성SessionCreationPolicy.If_Required
: 스프링 시큐리티가 필요 시 생성(기본값)SessionCreationPolicy.Never
: 스프링 시큐리티가 생성하지 않지만 이미 존재하면 사용SessionCreationPolicy.Stateless
: 스프링 시큐리티가 생성하지 않고 존재해도 사용하지 않음