반응형
250x250
Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

샘오리_개발노트

최신 Spring Security 사용법 - SecurityFilterChain 본문

개발자 전향 프로젝트

최신 Spring Security 사용법 - SecurityFilterChain

샘오리 2023. 2. 9. 17:14
728x90
반응형

Spring Security 5.7.0-M2 부터 WebSecurityConfigurerAdapter가 Deprecated 되었고

기존에 security 예외 url을 설정하던 antMatchers는 아예 삭제되었다.

 

하지만 아직까지 수많은 블로그들은 예전 Spring security 버전을 기준으로 글이 쓰여져있고

현재 최신 버전의 업데이트 사항은 영문 공식문서에 존재하기 때문에 어떻게 다가서야하는지 난감할 수 있다.

해서 업데이트된 내용들과 어떻게 사용해야 하는지 간략하게 공유하려고 한다.

일단 Spring Security의 본체라고도 할 수 있는 Configuration파일에는 크게 두가지 configure를 설정하게 되는데

 

바로 WEB 과 HTTP이다.

아래 블로그에서는 Spring Security에서 WEB과 HTTP 설정을 하는 이유와 그 차이를 간략하게 설명해준다.

https://velog.io/@gkdud583/HttpSecurity-WebSecurity%EC%9D%98-%EC%B0%A8%EC%9D%B4

 

[Spring] HttpSecurity, WebSecurity의 차이

HttpSecurity와 WebSecurity의 차이점에 대해 찾아본 내용을 정리한 글입니다.antMatchers에 파라미터로 넘겨주는 endpoints는 Spring Security Filter Chain을 거치지 않기 때문에 '인증' , '인가' 서비스가 모두 적용

velog.io

위 블로그의 내용처럼, 사용자가 예외처리를 하고 싶은 URL을 설정할 때

그 목적과 필요성에 따라 WEB이나 HTTP설정중에 하나를 골라서, 혹은 두곳 다 넣으면 되는것이다.

 

먼저 WebSecurityConfigurerAdapter가 Deprecated 되기 전 Securiy Config 샘플 소스는 아래와 같다.

 

기존

@Configuration
@EnableWebSecurity // Spring Security 설정 활성화

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
     
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/login").permitAll()
                .antMatchers("/users/**", "/settings/**").hasAuthority("Admin")
                .hasAnyAuthority("Admin", "Editor", "Salesperson")
                .hasAnyAuthority("Admin", "Editor", "Salesperson", "Shipper")
                .anyRequest().authenticated()
                .and().formLogin()
                .loginPage("/login")
                    .usernameParameter("email")
                    .permitAll()
                .and()
                .logout().permitAll();
 
        http.headers().frameOptions().sameOrigin();
    }
     
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/images/**", "/js/**", "/webjars/**"); 
    }
}

 

 


최신

 

 

본체

@EnableWebSecurity
@EnableMethodSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
    
    
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().requestMatchers("/예외처리하고 싶은 url", "/예외처리하고 싶은 url");
    }
    
    @Bean
    protected SecurityFilterChain webSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests()
                .requestMatchers("/로그인페이지", "/css/**", "/images/**", "/js/**").permitAll()
                .anyRequest().authenticated()

            .and()
                .formLogin()
                .loginPage("/로그인페이지")
                .loginProcessingUrl("/실제 로그인이 되는 url")
                .permitAll()
                .successHandler(로그인 성공 시 실행할 커스터마이즈드 핸들러)
                .failureHandler(로그인 실패 시 실행할 커스터마이즈드 핸들러);

        http
                .sessionManagement()
                .invalidSessionUrl("/로그인페이지")

            .and()
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/실제 로그아웃이 되는 url"))
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
                .permitAll();


        //CSRF 토큰
        http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());

        return http.build();
    }
}

* JWT 토큰을 사용한다면 아래와 같이 CSRF를 끄면 된다.

httpSecurity
        // token을 사용하는 방식이기 때문에 csrf를 disable합니다.
        .csrf().disable()

샘플 로그인 성공 핸들러 => 

로그인 성공 시 첫 화면으로 보여주고 싶은 화면의 url로 redirect 하는 로직

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		response.sendRedirect(request.getContextPath() + "/");
	}

 

 

샘플 로그인 실패 핸들러 기본 =>

어떠한 안내 메세지도 뜨지 않아서 로그인이 왜 실패했는지 모른채 다시 로그인 페이지로 리로드 되는 로직

@Component
public class CustomAuthenticationFailHandler implements AuthenticationFailureHandler {
	
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
            
		response.sendRedirect(request.getContextPath() + "/loginPage");
        
	}
}

 

샘플 로그인 실패 핸들러 응용 =>

로그인 실패 할 때마다 로그인 실패 회수를 1씩 증가시켜서 db에 누적시키고 설정한 숫자에 도달하면 lock을 한다든지 응용할 수 있고 왜 로그인에 실패했는지 에러메세지를 받아서 전달해주는 로직

 

@Component
public class CustomAuthenticationFailHandler implements AuthenticationFailureHandler {
	
	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {

		// 로그인 실패 시 로그인 실패 횟수를 증가하도록 trigger
		if (exception instanceof BadCredentialsException) {
			increaseLoginFailCount(request.getParameter("username"));
		}

		//해당 메세지를 상황에 따라 화면으로 회신 한다.
		request.setAttribute("errorMsg", exception.getMessage());
		request.getRequestDispatcher("/loginPage?error=true").forward(request, response);
	}
    
    private void increaseLoginFailCount(String username) {
        //로그인 실패 시 로그인 실패 횟수를 증가시키는 함수
	}
}

* 에러 메세지 같은 경우

UsernameNotFoundException , BadCredentialsException , 혹은 LockedException 것들을 설정해 줄 수 있는 

AuthenticationProvider 클래스에서 커스터마이즈 하면 된다.

 

예시:

    @Override
    public Authentication authenticate(Authentication authentication) throws UsernameNotFoundException, AccountExpiredException {
        String username = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
        
        UserContext user = null;
             
        //관리자 계정 그룹을 조회한다.
        OperatorDto operator = selectOperator(username);
        	
        //관리자 계정의 경우 사용자 객체를 만들어서 통과시킨다.
        if (operator != null && passwordEncoder.matches(password, operator.getPwd())) {

		if (operator == null) {
            throw new UsernameNotFoundException("ID 또는 비밀번호를 확인해주세요.");
         }
            ...
            ...
            ...

 

 

아까 실패 핸들러에서 exception 메세지를 errorMsg라는 변수에 담아서 request의 attribute로 설정해놨기에

request.setAttribute("errorMsg", exception.getMessage());

에러메세지를 받을 errorMsg변수를 html 코드에 작성해주고 적당히 빨간색으로 부각되게 하면 된다. 

그리고 항상 에러메세지가 뜨면 안되니 th:if 조건문을 걸어

에러 exception이 터졌을 경우 해당 메세지가 튀어나오도록 설정한다.

*타임리프 기준

<ul class="parsley-errors-list filled" id="parsley-id-7" th:if="${param.error}">
    <li class="parsley-required">
        <p style=" color: #FF1C19;" th:text="${errorMsg}"></p>
    </li>
</ul>

 

728x90
반응형
Comments