스프링 시큐리티는 스프링 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크로 인증(Authenticate, 누구인지?) 과 인가(Authorize, 어떤것을 할 수 있는지?)를 담당한다. 스프링 시큐리티에서는 주로 서블릿 필터와 이들로 구성된 필터체인으로의 구성된 위임모델을 사용한다. 그리고 보안과 관련해서 체계적으로 많은 옵션을 제공해주기 때문에 개발자 입장에서는 일일이 보안관련 로직을 작성하지 않아도 된다는 장점이 있다.
Maven project - pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Gradle project - build.gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
서버가 기동되면 스프링 시큐리티의 초기화 작업 및 보안 설정이 이뤄진다.
별도의 설정이나 구현을 하지 않아도 기본적인 웹 보안 기능이 현재 시스템에 연동
http.formLogin() http.authorizeRequests().antMatchers(/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()
@Configuration
@EnableWebSecurity //웹보안 활성화를위한 annotation
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests() // 요청에 의한 보안검사 시작
.anyRequest().authenticated() //어떤 요청에도 보안검사를 한다.
.and()
.formLogin();//보안 검증은 formLogin방식으로 하겠다.
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()//Form 로그인 인증 기능이 작동함
.loginPage("/login.html")//사용자 정의 로그인 페이지
.defaultSuccessUrl("/home")//로그인 성공 후 이동 페이지
.failureUrl("/login.html?error=true")// 로그인 실패 후 이동 페이지
.usernameParameter("username")//아이디 파라미터명 설정
.passwordParameter("password")//패스워드 파라미터명 설정
.loginProcessingUrl("/login")//로그인 Form Action Url
.successHandler(loginSuccessHandler())//로그인 성공 후 핸들러 (해당 핸들러를 생성하여 핸들링 해준다.)
.failureHandler(loginFailureHandler());//로그인 실패 후 핸들러 (해당 핸들러를 생성하여 핸들링 해준다.)
.permitAll(); //사용자 정의 로그인 페이지 접근 권한 승인
}
}
AntPathRequestmatcher(/login)
Authentication
→ 사용자가 요청한 요청정보를 확인하여 요청정보 Url이 /login으로 시작하는지 확인한다.
요청한다면 다음단계로(인증처리) 진행되고, 일치하지 않는다면 다음 필터로 진행된다.(chain.doFilter)
Authentication 에서 실제 인증처리를 하게 되는데, 로그인 페이지에서 입력한 Username과 Password를 인증객체(Authentication)에 저장해서 인증처리(AuthenticationManager)를 맡기는 역할을 한다.
→ 여기까지가 인증처리를 하기전에 필터가 하는 역할.
인증관리자(AuthenticationManager)는 내부적으로 AuthenticationProvider 에게 인증처리를 위임하게 된다. 해당 Provider가 인증처리를 담당하는 클래스로써 인증에 성공/실패를 반환하는데 실패할 경우 AuthenticationException 예외를 반환하여 UsernamePasswordAuthenticationFilter로 돌아가서 예외처리를 수행하고, 인증에 성공하게 되면, Authentication 객체를 생성하여
User객체와 Authorities객체를 담아서 AuthenticationManager에게 반환한다.
AuthenticationManager는 Provider로부터 반환받은 인증객체(인증결과 유저(User), 유저권한정보(Authorities))를 SecurityContext객체에 저장한다.
SecurityContext는 Session에도 저장되어 전역적으로 SecurityContext를 참조할 수 있다.
인증 성공 이후에는 SuccessHandler에서 인증 성공이후의 로직을 수행하게 된다.
정리해보자면
UsernamePasswordAuthenticationFilter는 Form인증처리를 하는 필터로써 해당 필터는 크게 두가지로 인증전과 인증후의 작업들을 관리한다.
인증처리전에는 사용자 인증정보를 담아서 전달하면서 인증처리를 맡기고(AuthenticationManager) 성공한 인증객체를 반환받아서 (전역적으로 인증객체를 참조할 수 있도록 설계 된)SecurityContext에 저장하고, 그 이후 SuccessHandler를 통해 인증 성공후의 후속 작업들을 처리합니다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.logout()//로그아웃 처리
.logoutUrl("/logout")// 로그아웃 처리 URL
.logoutSuccessUrl("/login")//로그아웃 성공 후 이동페이지
.deleteCookies("JSESSIONID","remember-me")//로그아웃 후 쿠키 삭제
.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");//쿠키 삭제
}
세션이 만료되고 웹 브라우저가 종료된 후에도 어플리케이션이 사용자를 기억하는 기능
Remember-Me 쿠키에 대한 HTTP 요청을 확인한 후 토큰 기반 인증을 사용해 유효성을 검사하고 토큰이 검증되면 사용자는 로그인 된다.
사용자 라이프 사이클
SessionID 쿠키를 삭제하더라도 Remember-Me가 있다면 해당 쿠키를 decoding한 다음 로그인 상태를 유지할 수 있도록 한다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.rememberMe()//rememberMe기능 작동
.rememberMeParameter("remember-me") //기본 파라미터명은 remember-me
.tokenValiditySeconds(3600)//default는 14일
.alwaysRemember(true)//remember me 기능이 활성화되지 않아도 항상 실행. default false
.userDetailsService(userDetailsService);//Remember me에서 시스템에 있는 사용자 계정을 조회할때 사용할 클래스
}
}
Remember Me 인증 Flow
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/shop/**") //특정 경로를 지정 해당 메서드를 생략하면 모든 경로에 대해 검색하게 된다.
.authorizeRequests() //보안 검사기능 시작
.antMatchers("/shop/login", "/shop/users/**").permitAll() //해당경로에 대한 모든 접근을 하용한다.
.antMatchers("/shop/mypage").hasRole("USER") // /show/mypage는 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유저는 해당 검사를 통과하고 그 아래에서 걸리게 된다.
표현식 인가 API
authenticated() 인증된 사용자의 접근을 허용
fullyAuthenticated() 인증된 사용자의 접근을 허용, rememberMe 인증 제외
permitAll() 무조건 접근을 허용
denyAll() 무조건 접근을 허용하지 않음
anonymous() 익명사용자의 접근을 허용
rememberMe() 기억하기를 통해 인증된 사용자의 접근을 허용
access(String) 주어진 SpEL표현식의 평과 결과가 true이면 접근을 허용
hasRole(String) 사용자가 주어진 역할이 있다면 접근을 허용
hasAuthority(String) 사용자가 주어진 권한이 있다면 접근을 허용
hasAnyRole(String...) 사용자가 주어진 권한이 있다면 접근을 허용
hasAnyAuthority(String...) 사용자가 주어진 권한 중 어떤 것이라도 있다면 접근을 허용
hasIpAddress(String) 주어진 IP로부터 요청이 왔다면 접근을 허용.
예제 코드
/*Application*/
@Slf4j
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
/*메모리방식으로 사용자 생성및 비밀번호와 권한 설정 메서드*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//{noop}비밀번호 형식을 적어준 것으로 noop은 따로 인코딩방식X
auth.inMemoryAuthentication().withUser("user").password("{noop}1111").roles("USER");
auth.inMemoryAuthentication().withUser("sys").password("{noop}1111").roles("SYS","USER");
auth.inMemoryAuthentication().withUser("admin").password("{noop}1111").roles("ADMIN","SYS","USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/user").hasRole("USER")
.antMatchers("/admin/pay").hasRole("ADMIN")
.antMatchers("/admin/**").access("hasRole('ADMIN') or hasRole('SYS')")
.anyRequest().authenticated();
http
.formLogin();
}
}
configure()메서드의 순서 /admin/~~ 과 /admin/pay 의 순서를 바꾸면?
SYS권한 유저가 /admin/pay 에 접근 권한 검사를 하기전 /admin/~~ 권한 검사에서 통과가 되버리기 때문에 /admin/pay 경로 접속이 허용되게 된다. 따라서 접속 권한 설정시 작은 부분에서 큰부분으로 설정을 해야 한다.
/*Controller*/
@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";
}
}