Spring Security

루미·2022년 8월 11일
0

Spring

목록 보기
6/11

스프링 시큐리티는 스프링 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크로 인증(Authenticate, 누구인지?) 과 인가(Authorize, 어떤것을 할 수 있는지?)를 담당한다. 스프링 시큐리티에서는 주로 서블릿 필터와 이들로 구성된 필터체인으로의 구성된 위임모델을 사용한다. 그리고 보안과 관련해서 체계적으로 많은 옵션을 제공해주기 때문에 개발자 입장에서는 일일이 보안관련 로직을 작성하지 않아도 된다는 장점이 있다.

dependency 추가

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'

스프링 시큐리티의 의존성 추가 시 일어나는 일들

서버가 기동되면 스프링 시큐리티의 초기화 작업 및 보안 설정이 이뤄진다.
별도의 설정이나 구현을 하지 않아도 기본적인 웹 보안 기능이 현재 시스템에 연동

  1. 모든 요청은 인증이 되어야 자원에 접근이 가능하다.
  2. 인증 방식은 폼 로그인 방식과 httpBasic로그인 방식을 제공한다.
  3. 기본 로그인 페이지 제공
  4. 기본 계정 한 개 제공 user/랜덤문자열

사용자 정의 보안 기능 구현

  • Security Dependency를 추가한 이후 기본적인 security를 설정및 구현하는 클래스
  • HttpSecurity 라는 세부적인 보안기능을 설정할수 있는 API를 제공하는 클래스를 생성한다.

제공 API

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()
  • 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에 등록된다.

Form Login인증

Login Flow

  1. Client에서 Get방식으로 Home Url자원접근 요청
  2. Server에서는 인증된 사용자만 접근가능하다고 판단해 인증이 안되면 로그인 페이지로 리다이렉트
  3. Client는 로그인페이지의 username/password 입력하여 Post방식으로 인증 시도
  4. Server에서는 Session ID생성후 인증결과를 담은 인증 토큰(Authentication) 생성 및 저장
  5. Client에서 /home 접근요청 시 세션에 저장된 인증 토큰으로 접근및 인증 유지

SecurityConfig 설정

@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(); //사용자 정의 로그인 페이지 접근 권한 승인
    }
  }

Form Login 인증 필터

UsernamePasswordAuthenticationFilter

  • 로그인 인증처리를 담당하고 인증처리에 관련된 요청을 처리하는 필터

  1. AntPathRequestmatcher(/login)
    Authentication

    → 사용자가 요청한 요청정보를 확인하여 요청정보 Url이 /login으로 시작하는지 확인한다.
    요청한다면 다음단계로(인증처리) 진행되고, 일치하지 않는다면 다음 필터로 진행된다.(chain.doFilter)

  2. Authentication 에서 실제 인증처리를 하게 되는데, 로그인 페이지에서 입력한 Username과 Password를 인증객체(Authentication)에 저장해서 인증처리(AuthenticationManager)를 맡기는 역할을 한다.
    → 여기까지가 인증처리를 하기전에 필터가 하는 역할.

  3. 인증관리자(AuthenticationManager)는 내부적으로 AuthenticationProvider 에게 인증처리를 위임하게 된다. 해당 Provider가 인증처리를 담당하는 클래스로써 인증에 성공/실패를 반환하는데 실패할 경우 AuthenticationException 예외를 반환하여 UsernamePasswordAuthenticationFilter로 돌아가서 예외처리를 수행하고, 인증에 성공하게 되면, Authentication 객체를 생성하여
    User객체와 Authorities객체를 담아서 AuthenticationManager에게 반환한다.

  4. AuthenticationManager는 Provider로부터 반환받은 인증객체(인증결과 유저(User), 유저권한정보(Authorities))를 SecurityContext객체에 저장한다.

  5. SecurityContext는 Session에도 저장되어 전역적으로 SecurityContext를 참조할 수 있다.

  6. 인증 성공 이후에는 SuccessHandler에서 인증 성공이후의 로직을 수행하게 된다.

정리해보자면

UsernamePasswordAuthenticationFilter는 Form인증처리를 하는 필터로써 해당 필터는 크게 두가지로 인증전과 인증후의 작업들을 관리한다.
인증처리전에는 사용자 인증정보를 담아서 전달하면서 인증처리를 맡기고(AuthenticationManager) 성공한 인증객체를 반환받아서 (전역적으로 인증객체를 참조할 수 있도록 설계 된)SecurityContext에 저장하고, 그 이후 SuccessHandler를 통해 인증 성공후의 후속 작업들을 처리합니다.


Logout

  1. Client에서 GET방식의 /logout 리소스 호출
  2. Server에서 세션무효화, 인증토큰 삭제, 쿠키정보 삭제 후 로그인페이지로 리다이렉트

  1. 요청이 Logout Url 인지 확인
  2. 맞을 경우 SecurityContext에서 인증객체(Authentication)객체를 꺼내옴
  3. SecurityContextLogoutHandler에서 세션 무효화, 쿠키삭제, clearContext()를통해 SecurityContext객체를 삭제하고 인증객체도 null로 만든다.
  4. SimpleUrlLogoutSuccessHandler를 통해 로그인페이지로 리다이렉트 시킨다.
  	@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 인증

세션이 만료되고 웹 브라우저가 종료된 후에도 어플리케이션이 사용자를 기억하는 기능
Remember-Me 쿠키에 대한 HTTP 요청을 확인한 후 토큰 기반 인증을 사용해 유효성을 검사하고 토큰이 검증되면 사용자는 로그인 된다.

사용자 라이프 사이클

  • 인증 성공(Remember-Me 쿠키 설정)
  • 인증 실패(쿠키가 존재하면 쿠키 무효화)
  • 로그아웃(쿠키가 존재하면 쿠키 무효화)

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

  1. Client에서 요청 (세션이 만료되었고, 사용자는 Form인증 받을 당시 Remember me를 사용하였기에 Remember me cookie를 가지고 있음)
  2. RememberMeAuthenticationFilter 가 동작
  3. RememberMeService interface의 구현체 동작
    TokenBasedRememberMeService → 메모리에서 저장한 토큰과 사용자가 가져온 토큰을 비교(default 14일 보존)하는 구현체
    PersistentTokenBasedRememberMeService → DB에 발급한 토큰과 사용자가 가져온 토큰을 비교해서 인증 처리 하는 구현체
  4. Token Cookie 추출
  5. Token이 존재하는지 검사 → 만약 없다면 다음 필터 동작
  6. Decode Token으로 Token의 format이 규칙에 맞는지 판단(유효성 검사)
    → 유효성이 invalidate 하다면 Exception 발생
  7. 토큰이 서로 일치하는지 검사
    → 토큰이 일치하지 않을경우 Exception 발생
  8. 토큰에 User 계정이 존재하는지 검사
    → 없을 경우 Exception 발생
  9. 새로운 Authentication Object를 생성하여 인증처리
  10. AuthenticationManager 인증관리자에게 전달하여 인증처리 수행


권한설정과 표현식

@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";
    }
}
profile
Backend 개발자가 되어보자!!

0개의 댓글

관련 채용 정보