스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security 1

Seung jun Cha·2022년 8월 4일
1
  • 스프링 시큐리티의 의존성 추가 시 일어나는 일들
  1. 별도의 설정이나 구현을 하지 않아도 기본적인 웹 보안 기능이 현재 시스템에 연동되어 작동함
  2. 모든 요청은 인증이 되어야 자원에 접근이 가능하다
  3. 인증 방식은 폼 로그인 방식과 httpBasic 로그인 방식을 제공한다
  4. 기본 로그인 페이지 제공한다
  5. 기본 계정 한 개 제공한다 – username : user / password : 랜덤 문자열, application 설정파일에서 바꿀 수 있다
spring.security.user.name=user
spring.security.user.password=1111

1. 기본 API & Filter

1-1 사용자 정의 필터


현재는 WebSecurityConfigurerAdater가 아닌 FilterChain 사용을 권장함

@Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests() // 인가가 필요한 경우 설정
                .anyRequest()  // 어떠한 url 요청이던지
                .authenticated(); // 인가 필요

1-2 httpBasic 인증


1-3 Form 인증


  1. .loginPage 아이디와 비밀번호를 입력하는 로그인 페이지 url주소를 설정할 수 있다. 사용자가 로그인 페이지를 직접 만들거나 로그인 주소를 변경할 때 사용된다. 기본 주소는 /login이다. 로그인하지 않은 상태에서 로그인이 필요한 페이지에 접근할 때 해당 url로 이동한다. (컨트롤러에서 @GetMapping의 url)

  2. .defaultSuccessUrl: 로그인 성공 후 자동으로 이동할 페이지이다.
    로그인 성공 후 redirect될 페이지를 정하는 것은 이 메소드 뿐이 아니다. .successHandler에서 send.redirect해줄 수도 있고, requestCache에서 직전에 접속하려 했던 url을 get해서 그 페이지로 이동하기도 한다. 그 중 .defaultSuccessUrl은 최후순위를 가지게 된다. 만약 이걸로 설정한 url로 무조건 리다이렉트가게 하고 싶다면 .defaultSuccessUrl("/home", true)같이 뒤에 인자로 true를 주면 된다.

  3. .failureUrl: 로그인 실패 후 자동으로 이동할 페이지이다.

  4. .usernameParameter: 아이디(username) 파라미터를 커스텀으로 정할 수 있다. 기본값은 username이다.
    ex) usernameParameter("email")로 설정하면 로그인에 필요한 아이디는 email로 세팅된다

  5. .passwordParameter: 비밀번호 파라미터를 커스텀으로 정할 수 있다. 기본값은 password이다.

  6. .loginProcessingUrl: 사용자 아이디와 비밀번호를 제출할 URL로 기본값은 /login이다. 해당 URl이 리턴하는 html파일에 파라미터 바인딩하여 post로 값이 넘어감
    UserDetailService를 구현한 클래스내에 loadUserByUsername 메소드가 자동으로 실행되어 저장되어 있는 user를 가지고와서 입력받은 아이디와 비교한다(컨트롤러를 따로 만들 필요가 없다.)
    loginProcessingUrl() -> loadUserByUsername -> 바인딩 된 username과 비교하여 저장된 user를 가지고옴

  7. .successHandler: 로그인 성공 이후 실행된다. 밑에서 자세히 알아보자.

  8. .failureHandler: 로그인 실패 이후 실행된다. 밑에서 자세히 알아보자.
    로그인 페이지와 로그인 정보를 전달할 action url은 .permitAll된다. 즉, 인증이 없이 접근이 가능하다. 로그인을 하기 위한 로그인은 필요없다.

  • usernameParameter , passwordParameter, loginProcessingUrl
    • .usernameParameter( 아이디 name ) : Id input의 name을 이 설정과 맞춰야 한다.
    • .passwordParameter( 비밀번호 name ) : Password input의 name을 이 설정과 맞춰야 한다.
    • .loginProcessingUrl( Action Url ) : 로그인의 Form Action Url과 맞춰야 한다.
      위 3개는 html의 < form > , < Input >와 맞춰야 한다.
    만약 기본 로그인 페이지를 사용하면서 다음과 같이 설정했다고 하자.
.usernameParameter("userId")
.passwordParameter("passwd")
.loginProcessingUrl("/login_proc")

그러면 스프링 시큐리티가 이렇게 이름을 따로 만들어준다.

  • successHandler, failureHandler
    파라미터로 각각 AuthenticationSuccessHandler
    AuthenticationFailureHandler를 받게 된다.
    오버라이드 메소드는 각각 onAuthenticationSuccess와 AuthenticationFailureHandler이다.
.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");
    }
  })

1-3-1 UsernamePasswordAuthenticationFilter

  • FormLogin 인증필터
  1. 아이디와 비밀번호를 입력
  2. AnPath필터에서 URL 요청이 loginProcessingUrl과 매칭되는지 확인
  3. 인증객체(Authentication) 객체 생성(Username과 password를 담음)
  4. 위에서 만든 객체를 이용해 AuthenticationManager에서 인증처리 (내부적으로 AuthenticationProvider에게 인증 위임)
    -> DB에 저장된 값을 가져오기 위해 UserDetailsService를 사용하고 입력받은 값과 비교
    ->인증 실패시 AuthenticationExcepion
    ->인증 성공시 Authentication객체를 만들어서 AuthenticationManager에게 리턴 (User 정보와 권한 정보 등을 담음)
  5. Authentication객체를 SecurityContext에 저장(인증을 받은 객체를 저장하는 곳) -> Session에 저장(전역으로 사용 가능하게 함)
  6. SuccessHandler 실행

1-3-2 Logout

  • 로그아웃 요청이 들어오면 세션 무효화, 인증토큰 삭제, 쿠키정보 삭제

    .logoutRequestMatcher(new AntPathRequestMatcher("/로그아웃 주소)) 와 .logoutUrl()의 차이점은?

  • Logout 로직
  1. AntPathRequestMatcher(기본 값은 "/logout") : 로그아웃 요청이 올바른 url로 들어왔는지 확인
  2. SecurityContext에서 Authentication객체를 꺼내 SecurityContextLogoutHandler에게 전달
  3. SecurityContextLogoutHandler가 세션 무효화, 쿠키 삭제, Authentication=null, SecurityContextHolder.clearContext() 등을 진행
  4. 로그아웃이 성공되면 SimpleUrlLogoutSuccessHandler을 통해 특정 페이지로 redirect (이때 리다이렉트 주소는 커스텀할 수 있다.)

1-4 Remember-me

  • JSESSIONID이 만료되거나 쿠키가 없을 지라도 어플리케이션이 사용자를 기억하는 기능이다. 자동 로그인 기능을 떠올리면 쉽다.
http.rememberMe()
		.rememberMeParameter("remember")  // 기본 파라미터명은 remember-me
        .tokenValiditySeconds(3600)  // 기본값은 14일
        .alwaysRemember(true)  // 기능이 활성화되지 않아도 항상 실행
        .userDetailsService(userDetailsService)  // 사용자의 정보를 가지고 옴


체크하고 로그인 시, JSESSIONID 말고도 remember-me 라는 쿠키가 날아온다. 이 쿠키에는 회원 아이디와 비밀번호 등이 인코딩 되어 들어있다.
따라서 JSESSIONID이 다른 요인에 의해 삭제되거나 request header에 보내지 않더라도서버에서는 회원 인증을 하고, 실제 회원과 일치한 정보가 있다면 로그인을 해주고 거기에 JSESSIONID 쿠키까지 새로 만들어서 보내준다.

1-4-1 remember-me 쿠키 사이클

  1. 로그인(인증) 성공 = remember-me 쿠키 발급
  2. 로그인(인증) 실패 = remember-me 쿠키가 있다면 무효화
    즉, 로그인이 성공했어도 사용자가 임의로 로그인 페이지로 돌아간 후 인증에 실패하면, 있는 쿠키도 무효화 시킨다.
  3. 로그아웃 = remember-me 쿠키가 있다면 무효화
  4. 만료시간 = 만료시간이 지나면 쿠키 무효화

1-4-2 remember-me 로직

  1. 실행 조건
    RememberMeAuthenticationFilter 필터가 실행될 조건은 다음과 같다.
    (1) 스프링 시큐리티에서 사용하는 인증객체(Authentication)가 Security Context에 없는 경우
    세션 만료(time out), 브라우저 종료, 세션id 자체를 모르는 등의 요인이 있다.
    인증객체가 있다라는 소리는 로그인이 정상적으로 되었고, 회원 정보도 정상적으로 세션에서 찾을 수 있다는 말이다. 따라서 이 필터가 실행될 필요가 없다.
    (2) 사용자 request header에 remember-me 쿠키 토큰이 존재해야 한다.

  2. 구현체의 차이
    위의 실행 조건이 부합하다면 RememberMeService(인터페이스)가 실행된다. 실제 구현체 2가지 있는데, 그 차이는 다음과 같다.

    (1) TokenBasedRememberMeServices: 메모리에 있는 쿠키와 사용자가 보내온 remember-me 쿠키를 비교(기본적으로 14일간 존재)
    (2) PersistentTokenBasedRememberMeServices: DB에 저장되어 있는 쿠키와 사용자가 보내온 remember-me 쿠키를 비교(이름 그대로 persistent)

  3. 로직
    (1) Token Cookie를 추출했을 때, 사용자가 request한 토큰이 remember-me 토큰인지 확인
    (2) Decode Token하여 토큰이 정상인지 판단
    (3) 사용자가 들고온 토큰과 서버에 저장된 토큰이 서로 일치하는지 판단
    (4) 토큰에 저장된 정보를 이용해 DB에 해당 User 계정이 존재하는지 판단
    (5) 위 조건을 모두 통과하면 새로운 인증객체(Authentication)을 생성 후 AuthenticationManager에게 인증 처리를 넘긴다. (물론 Security Context에도 인증 객체를 저장한다.)
    (6) 이후 response 될 때 JSESSIONID를 다시 보내준다.

  4. remember-me 토큰 초기화 시점

  • rembmer-me 토큰은 새롭게 로그인 인증을 받는 경우 초기화 된다.
    remember-me 쿠키로 인증을 진행해서 로그인 될 경우에는 JSESSIONID은 초기화되지만, remember-me 토큰은 초기화가 안된다. 두 토큰은 별개이다.

1-5 AnonymousAuthenticationFilter

  • 인증 객체가 없다고 null처리를 하는 것이 아닌 익명의 인증객체를 생성해서 처리

1-6 예외처리 및 요청 캐시 필터

1-6-1 ExceptionTranslationFilter

(1) AuthenticationException : 인증 예외가 발생하면 예외가 발생하기 전의 요청정보를 SavedRequestCache에 저장하고 AuthenticationEntryPoint를 호출해서 401오류코드를 전달
(2) AccessDeniedException : AccessDeniedHandler에서 인가 예외를 처리하도록 제공

protected void configure(HttpSecurity http) throws Exception {
	 http.exceptionHandling() 					
		.authenticationEntryPoint(authenticationEntryPoint())  
        // 인증실패 시 처리
		.accessDeniedHandler(accessDeniedHandler()) 
        // 인가실패 시 처리
        
        successHandler(new AuthenticationSuccessHandler()
        	RequestCache requestCache = new HttpSessionRequestCache();
            SavedRequest savedRequest = requestCache.getRequest(request, response);
            response.SendRedirect(savedRequest.getRedirectUrl())
            // RequestCache에 정보를 저장해놓고
            다시 인증, 인가를 받으면 원래 있던 페이지로 redirect
            

1-6-2. RequestCacheAwareFilter

1-7 사이트 간 위조 요청 - CSRF

  • 스프링 시큐리티가 기본적으로 CSRF 필터를 적용하고 있기 때문에,
    http.csrf().disable로 기능을 끄지 않는 이상 계속 작동하고 있음

2. 세션 제어 필터

2-1 동시 세션 제어

  • concurrentsessionfilter : 매 요청마다 사용자의 세션 만료여부 체크 , 세션이 만료되었을 경우 즉시 만료처리
http.sessionManagement()   // 세션 관리기능 작동
	.maximumSessions(1)    //  계정당 최대 허용가능 세션 수, -1일 경우 무제한 
    .maxSessionPreventsLogin(true/false)  
    // false : 기존세션만료 , true : 새로운 접근차단
    .invalidSessionUrl("/invalid")  
    // 세션이 유효하지 않을 때 이동할 페이지 expiredUrl 보다 우선적용
    .expireUrl("/expired")  // 세션이 만료된 경우 이동할 페이지 

2-2 세션 고정 보호, 세션정책

http.sessionManagement()
	.sessionFixation.changeSessionId  // 코드를 작성하지 않아도 기본적용됨
    // none, migrateSession(서블릿버전 3.1 이하에서 기본적용) , newSession
    //changeSessionId , migrateSession은 이전 세션의 정보까지 가져옴
  • sessionCreationPolicy.Always : 스프링 시큐리티가 항상 세션을 생성
  • sessionCreationPolicy.If_Required : 스프링 시큐리티가 필요시 생성(기본값)
  • sessionCreationPolicy.Never : 스프링 시큐리티가 생성하지 않지만 이미 존재하면 사용
  • sessionCreationPolicy.Stateless : 스프링 시큐리티가 생성x, 사용x
http.sessionManagement()
	.sessionCreationPolicy(SessionCreationPolicy.If_Required)

2-3 세션 인증 과정


1. UsernamePasswordAuth 필터 : 아이디와 비밀번호 일치여부로 인증
2. ConcurrentSessionControlAuth : 동시 세션처리를 하는 클래스로 인증을 요청하는 사용자가 현재 사용하는 세션이 몇 개인지 체크
3. ChangeSessionIdAuth : 세션 고정보호처리, 새롭게 세션처리
4. RegisterSessionAuth : 사용자의 세션을 등록, 사용자의 count가 증가함
5. concurrentSessionFilter : 현재 세션의 만료여부 체크

3. 권한설정과 표현식

  • 설정 시 구체적인 경로가 먼저오고, 큰 범위의 경로가 뒤에 와야한다.

authorizeRequests() : 요청Url에 대한 권한 지정. Security 처리에 HttpServletRequest를 이용한다는 것을 의미한다.
antMatchers() : 특정 경로를 지정해서 권한 설정. 보통 뒤에 permitAll(), hasRole() 등 다른 메서드가 붙습니다.

antMatcher와 mvcMatchers
일반적으로 mvcMatcher는 antMatcher보다 좀 더 포괄적이다.
antMatchers("/secured")는 정확한 /secured URL과만 일치하고,
mvcMatchers("/secured")는 /secured와 /secured/, /secured.html, /secured.xyz 등 과도 일치한다.

anyRequest() : 설정한 경로 외에 모든 경로를 뜻합니다.
authenticated() : 인증된 사용자만이 접근할 수 있습니다.
permitAll() : 어떤 사용자든지 접근할 수 있습니다.
hasRole() : 특정 ROLE을 가지고 있는 사람이 접근할 수 있습니다.
hasAuthority() : 특정 권한을 가지고 있는 사람만 접근할 수 있습니다. hasRole과 비슷하다고 볼 수 있습니다.
csrf() : CSRF 보안에 대한 설정입니다. 아무 설정도 하지 않으면 CSRF 보안을 하도록 설정됩니다. (http.csrf.disable())
disable() : 해당 기능을 해제 합니다. 여기서는 csrf()를 해제합니다.
unauthorizedEntryPoint는 AuthenticationEntryPoint가 리턴값입니다.
formLogin() : form 기반의 로그인을 할 수 있습니다.
loginPage() : 로그인 페이지의 URL을 설정합니다. 로그인이 필요한 페이지에 들어갔을 때 로그인 한 상태가 아니라면 해당 url로 redirect 해준다. 컨트롤러를 따로 만들 필요가 없다.

failureUrl() : 로그인에 실패했을 때에 해당 URL로 가게 합니다.
defaultSuccessUrl() : 로그인에 성공했을 때에 아무런 설정을 하지 않았을 시 넘어가는 페이지를 설정합니다.
successHandler() : defaultSuccessUrl과 비교해서 비슷하다고 생각할 수 있지만, 로그인에 성공했을 때 내가 원하는 대로 설정할 수 있습니다. 대신 () 안에 해당 객체를 넣어줘야 합니다.
failureHandler() : failureUrl과 비슷하지만, 로그인에 실패했을 때 내가 원하는 대로 설정합니다.
logout() : 로그아웃에 대해 설정할 수 있습니다.
logoutRequestMatcher() : 로그아웃을 실행할 주소를 나타냅니다. 새롭게 로그아웃 주소를 설정할 수 있습니다.
cf. logoutUrl() : 로그아웃을 실행할 주소를 나타낸다. 기본값으로 "/logout"이 적용된다고 합니다.
AntPathRequestMatcher() : HTTP 메서드와 일치하는 특정 패턴으로 Matcher를 작성합니다. AntPathRequestMatcher는 다른 곳에서도 종종 볼 수 있습니다.
logoutSuccessUrl() : 로그아웃을 성공했을 때 이동하는 페이지를 설정합니다.
sessionManagement() : 세션에 관한 설정을 한다.
sessionCreationPolicy() : 세션 create에 대해 설정한다.
SessionCreationPolicy.STATELESS : HTTPSession을 생성하지 않고 SecurityContext를 얻기 위해 HTTPSession을 사용하지 않는다.
exceptionHandling() : 예외사항을 설정한다.
authenticationEntryPoint() : 인증의 진입지점을 설정한다.
addFilter(filter, CLASS) : 지정된 필터 클래스 뒤에 필터를 추가한다.
UsernamePasswordAuthenticationfilter : Spring에서 기본적으로 제공하는 클래스이다. username과 password를 매개변수로 받는다.
usernameParameter() : 로그인에 사용될 파라미터 지정. 아이디 부분이 된다.
passwordParameter() : 로그인에 사용될 파라미터 지정. 비밀번호 부분이 된다.
invalidateHttpSession() : 로그아웃 시 인증정보를 지우고 설정된 세션을 무효화 시킨다는 설정
authenticationProvider() : AuthenticationProvider를 추가로 사용하게 허용한다.
-> AuthenticationProvider는 인터페이스로 화면에서 입력한 로그인 정보와 DB에서 가져온 사용자 정보를 비교해주는 인터페이스이다.
cors() : REST API를 개발할 때 백엔드와 프론트엔드를 연결하기 위하여 사용한다.
configurationSource() : cors 요청에 따라 어떤 방법으로 해결할 지 방법을 지정
httpBasic() : Basic Authentication을 정의할 때 사용한다.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
      .antMatchers(HttpMethod.OPTIONS).permitAll()
      .antMatchers("/ws/**").permitAll()
      .antMatchers("/h2-console", "/sign/**", "/error").permitAll()
      .antMatchers("/api/system/**").hasRole(AppUserRole.SYSTEM.name())
      .antMatchers("/api/manage/**").hasRole(AppUserRole.ADMIN.name())
      .antMatchers("/api/openbabel/**").authenticated()
      .antMatchers("/api/moleditor/**").authenticated()
      .antMatchers(HttpMethod.GET, "/api/common/**").permitAll()
      .anyRequest().authenticated()
      .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)
      .and().exceptionHandling().authenticationEntryPoint(unauthorizedEntryPoint())
      .and().httpBasic()
      .and().csrf().disable();
      
      http.headers().frameOptions().sameOrigin(); // h2-console
      http.cors().configurationSource(corsConfigurationSource());
}

0개의 댓글