Always
, if_required
, Never
, Stateless
session.isExpired() == true
session.expireNow()
ConcurrentSessionFilter
에서 이전 사용자의 세션이 만료되었는지 확인: SessionManagementFilter
안의 설정 참조http.antMatchers("/users/**").hasRole("USER")
@PreAuthorize("hasRole('USER')")
public void user() {System.out.println("user")}
@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 유저는 해당 검사를 통과하고 그 아래(좁은 범위)에서 걸리게 된다.
메소드 | 동작 |
---|---|
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";
}
}
Spring Security가 관리하는 보안 필터 중 마지막 필터가 FilterSecurityInterceptor
이고, 바로 전 필터가 ExceptionTranslationFilter
이다. 해당 필터에서 사용자의 요청을 받을 때, 그 다음 필터로 해당 요청을 전달할 때 try-catch
로 감싸서 FilterSecurityInterceptor
를 호출하고 있고, 해당 필터에서 생기는 인증 및 인가 예외는 ExceptionTranslationFilter
로 throw
하고 있다.
AuthenticationEntryPoint
호출RequestCache
: 사용자의 이전 요청 정보를 세션에 저장하고 이를 꺼내오는 캐시 매커니즘SavedRequest
: 사용자가 요청했던 request 파라미터 값들, 그 당시의 헤더값들 등이 저장AccessDeniedHandler
에서 예외 처리하도록 제공FilterSecurityInterceptor
권한 필터가 해당 요청(/user)을 받았지만, 해당 유저는 인증을 받지 않은 상태AccessDeniedException
)로 빠진다.AccessDeniedException
)는 익명 사용자이거나 RememberMe 사용자일 경우 AccessDeniedHandler
를 호출하지 않고 AuthenticationException
에서 처리하는 로직으로 보내게 된다.AuthenticationException
) 는 두 가지 일을 한다.AuthenticationEntryPoint
구현체 안에서 login 페이지로 리다이렉트 한다.(인증 실패 이후) Security Context를 null로 초기화 해주는 작업도 해준다.DefaultSavedRequest
객체에 저장하고 해당 객체는 Session
에 저장되며 Session
에 저장하는 역할을 HttpSessionRequestCache
에서 해준다.AccessDeniedException
이 발생AccessDeniedHandler
를 호출해서 후속 작업을 처리 (일반적으로 denied 페이지로 이동)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 페이지로 이동시키지만 별도로 다른 로직을 수행할 수 있음.사용자가 사이트에 접속하여 로그인 후 쿠키를 발급받은 뒤 공격자가 사용자의 이메일로 특정 링크를 전달하고 사용자가 해당 링크를 클릭하게 되면, 공격용 웹페이지에 접속하게 되고, 해당 페이지에 '로또 당첨'이라는 이미지가 노출된다. 유저가 이 이미지를 클릭하면 사이트에 특정 URL로 요청하게 되는데 해당 쿠키정보를 가지고 있기 때문에 해당 요청에 대해 정상적으로 동작을 하게 된다.
이처럼 사용자의 의도와는 무관하게 공격자가 심어놓은 특정 방식을 통해 자원 요청을 하게 되고 그것을 응답 받을 수 있도록 하는 것을 CSRF(사이트 간 요청 위조)라 한다.
<input type="hidden" name="${csrf.parameterName}" value="${_csrf.token}"/>
http.csrf()
: 기본 활성화 되어 있음http.csrf().disabled()
: 비활성화