[Security] Spring-Security6.2 기초 및 Config

728x90

들어가며


토이 프로젝트에서 JWT 랑 Spring Security 를 사용하여 로그인을 구현하는 기능을 만들고 있던 중

여러 인증,인가 관련된 여러 에러 를 만나게 되었습니다.

 

위 기능을 잘 사용하기 위해서는 Security 기본은 알아야 더 잘 구현할 수 있겠다는 생각이 들어

기초적인 내용을 공부를 해본 후 중요한 내용만 포스팅을 하려고 합니다.

 

들어가기 전에 앞서 Spring Security 의 동작 구조에 대해서 간단하게 설명을 하며,

Spring Security 가 어느 부분에서 개입을 하는지, 어떤 역할을 하는지를 위주로 알아보겠습니다.

 

보통 Java, Spring 개발자들이 security 를 사용하는 이유는 여러 이유가 있겠지만,

저는 JWT토큰 로그인을 기능을 알아보면서 자연스럽게 spring-security 를 접하게 되었습니다.

 

그리고 보통 인증, 인가, 권한 관련된 기능을 개발을 하기 위해서 Spring-Security 를 사용하고는 합니다.

 

 

[ 1. 동작 구조 ] 

https://substantial-park-a17.notion.site/2-faeb3260a4ab47f386aa8cee3aa24814

 

위 사진은 스프링 시큐리티가 없을 때 스프링 부트 동작 구조 입니다.

 

클라이언트에서 Http 요청이 오면 서블릿 컨테이너(=톰캣) 이 요청을 받아 서블릿 컨테이너 필터를 거친 후에
스프링 컨테이너에서 요청을 받습니다.

 

요청을 받은 스프링 컨테이너는 디스패처 서블릿에 의해 핸들러 매핑으로 가고 컨트롤러를 할당 받아 뷰리졸버를 가고

이런 동작구조를 가집니다.

 

여러분들이 생각하기에 위 아키텍쳐에서 스프링 시큐리티가 끼여 들려면 어디서 끼어 들어야 한다고 생각하시나요?

 

바로 필터 입니다.

 

스프링 시큐리티 공식 문서 사진을 보면 대표 사진에 

 

위 필터 사진이 있습니다.

 

스프링 시큐리티는 서블릿 컨테이너에서 스프링 컨테이너로 요청이 가는 필터에서 개입을 합니다. 

 

즉 스프링 시큐리티는 스프링시큐리티 필터를 서블릿 컨테이너에 추가를 하여서 http 요청을 검증을 하여 

스프링 컨테이너로 요청을 넘기는 것입니다.

 

그래서 아래서 다루겠지만 SecurityFilterChain 이라는 클래스가 있습니다. 

위 클래스가 바로 서블릿 컨테이너 필터 역할을 하는 것 입니다.

 

요약을 하자면

  1. WAS(=서블릿 컨테이너) 의 필터에 하나의 시큐리티 전용 필터를 만들어서 넣고 해당 필터에서 요청을 가로챔
  2. 해당 요청은 스프링 컨테이너 내부에 구현되어 있는 스프링 시큐리티 감시 로직을 거침
  3. 시큐리티 로직을 마친 후 다시 WAS의 다음 필터로 복귀한다.

간략하게 위 구조라고 생각하면 될 것 같습니다.

 

 

위 사진을 보면 필터가 하나 껴 있는 것을 볼 수 있습니다.

 

FilterChainProxy 가 바로 스프링 시큐리티 의존성을 추가하면 자동으로 추가되는 대표 필터 입니다.

위 필터는 위 사진과 같이 서블릿 컨테이너에 끼는 필터 입니다.

 

그렇다면 이제 위 필터를 컨트롤 하기 위한 스프링 컨테이너 안에 필터를 따로 구성을 해야 합니다.

 

https://substantial-park-a17.notion.site/2-faeb3260a4ab47f386aa8cee3aa24814

 

위 사진을 보면 대표 필터 아래에 여러개의 시큐리티 필터가 있습니다.

 

위 필터를 구성하는 방법은 아래서 설명할 것이며,

여러개의 필터들이 체인형식으로 묶여 필터 체인 형태 라고 부릅니다.

 

각각의 필터에서 로그인,로그아웃,인가,인증,검증, CSRF, JWT 등 여러 역할을 할 수 있습니다.

 

위 구조에서 중요한 클래스가 3가지가 있습니다.

 

- DelegatingFilterProxy : 스프링 Bean 을 찾아 요청을 넘겨주는 서블릿 필터(=대표 필터)

- FilterChainProxy : 스프링 시큐리티 의존성을 추가하면 DelegatingFilterProxy 에 의해 호출되는 SecurityFilterChain을들고 있는 Bean

- SecurityFilterChain : 시큐리티 필터들의 묶음으로 실제 시큐리티 로직이 처리되는 부분

 

우리가 가장 잘 알아야 하는 것은 SecurityFilterChain 이며 이 클래스를 사용하여 스프링 컨테이너 안에 있는 
스프링 시큐리티 필터를 우리 입맛대로 수정을 할 수 있다.

 

[ 2. 사용해야 하는 이유 ]

그냥 블로그 검색하니 jwt, OAuth2 는 SpringSecurity 랑 같이 쓰던데요? 

같은 생각 말고 사용을 해야하는지 알아야지 잘 사용할 수 있다고 생각합니다.

 

1) 보안 기능의 표준화

Spring Security는 인증(Authentication)과 인가(Authorization) 기능을 표준화된 방식으로 제공합니다.
이는 우리 같은 개발자가 직접 보안 기능을 구현해야 하는 부담을 덜어주고, 보안상의 결함이 발생할 가능성을 줄입니다.

 

2) 강력한 커스터마이징 가능성

Spring Security는 높은 커스터마이징을 제공합니다.
기본적인 보안 기능 외에도, 위에서 설명했다 싶이 애플리케이션의 요구에 맞춰 필터나 핸들러를 추가하거나 수정할 수 있습니다.
이를 통해 다양한 실무 시나리오에 맞게 보안 방법을 모색하고 비교적 쉽게 구현할 수 있습니다.

 

3) 쉬운 통합

Spring Security는 스프링 프레임워크와의 높은 호환성을 자랑합니다.
대표적으로 OAuth2, JWT 등과 같은 최신 인증 방식 또한 통합하여 사용할 수 있습니다.

 

[ 3. 쉽게 사용하라고 만들었지만, 어렵게 느껴지는 이유 ]

 

1) 방대한 설정 옵션

Spring Security는 다양한 기능을 제공하기 위해 아주 많은 설정 옵션^^ 을 갖추고 있습니다.
이는 유연성을 높여주지만, 입문을 하는 사람에게는 어렵게 느껴집니다.

특히, 각 설정 옵션이 어떤 역할을 하는지 알아야 사용이 가능하기 때문에 기본적인 학습이 필요합니다.

 

2) 빈번한 업데이트와 변화

Spring Security는 최신 보안 요구 사항을 반영하기 위해 자주 업데이트됩니다.
최근 까지 5.x 버전 쓰던 사람이 6.x 버전으로 마이그레이션을 진행하면
메소드 체이닝 식으로 필터 구성이 안돼 당황하는 사람이 많을 것입니다. (저 입니다...^^)

 

 

3) 복잡한 디버깅

보안 관련 문제는 디버깅이 어렵고 시간이 많이 소요될 수 있습니다.
Spring Security는 다양한 필터와 핸들러를 통해 요청을 처리하기 때문에, 특정 요청이 왜 실패했는지 추적하는 것이 복잡합니다.

 

설정 파일에 log 설정을 debug 로 찍어 놓고 봐도 잘 모르겠기도 하며 디버깅 모드로 하면 여러 스프링 시큐리티 메소드를 타기 때문에..

아주 복잡하기 때문에 그냥 기초를 공부해서 최대한 덜 틀리면서 개발을 하는게 정신 건강에 좋을 것 같습니다.

 

 

 

본론


이제 본론으로 가 스프링 컨테이너 안에 존재하는 스프링 시큐리티 필터를 Custom 하는 방법을 알아 보겠습니다.

 

implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

 

위 의존성을 추가해두고 시작을 합니다.

 

@Configuration
@EnableWebSecurity 
public class SecurityConfig {

}

 

@EnableWebSecurity : 이 어노테이션은 스프링 시큐리티 한테도 관리를 받게 하기 위한 어노테이션이다.

 

즉 SpringSecurity 설정을 활성화하기 위한 어노테이션 입니다.

 

@Configuration
@EnableWebSecurity 
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    
    	return http;
    }

}

 

위 구조가 스프링 시큐리티 필터의 기초 입니다.

 

위 SecuriFilterChain 메소드 안에 이제 여러 필터들을 구성할 것 입니다.

 

 

[ 1. http 요청에 따른 인가 ]

	@Bean 
	public SecurityFilterChain filterChain (HttpSecurity http) throws Exception {

		http
			.authorizeHttpRequests((auth) -> auth
				.requestMatchers("/", "/login","/loginProc","/join", "/joinProc").permitAll()
				.requestMatchers("/admin").hasRole("ADMIN")
         			// "/**" 는 {id} 같은거 의미, hasAnyRole 은 여러 역할 처리함
				.requestMatchers("/my/**").hasAnyRole("ADMIN", "USER")
				.anyRequest().authenticated()
			);
            
    		return http.build();
		}

 

위 인가에는 여러가지 기능이 있습니다. 

일단 메소드 구성 방식이 메소드 체이닝 -> 람다 형식으로 변형되었기 때문에 

http요청을 수락하는 authorizeHttpRequests 메소드 안에 구현을 람다식으로 했습니다.

 

즉 특정 경로에 요청을 허용하거나 거부하거나 할 수 있게 한다.

 

  • permitAll() 모든 사용자 경로 허용
  • hasRole 특정 ROLE 이 있어야 경로에 접근 가능
  • authenticated() : 로그인만 진행하면 모든 접근 가능하게 한다.
  • denyAll() : 모든 접근 거부
  • requestMatchers : 1개의 요청을 처리하는 것.
  • anyRequest() : 여러개 요청을 처리하는 것.

 

📌 requestMatchers()

 

위 메소드 안에는 접근 가능한 경로를 적습니다.

{ "/" , "/login" , "join" , "joinProc" } 경로에

 

PermitAll() 메소드를 통해 모든 사람에 접근이 허용된다는 뜻 입니다. 

위 메소드는 이름부터 알다 싶이 모든 것을 허용하는 메소드 입니다. 

 

 

📌 hasAnyRole() , hasRole()

 

위 메소드가 붙어 있다면 위 메소드 파라미터에 있는 역할을 가진 사람만 해당 경로에 접근 할 수 있다는 뜻 입니다.

즉 "/admin" 은 Role 이 ADMIN 인 사람만 접근 가능하게 한다.

 

참고로 스프링 시큐리티는 권한을 찾을 때 'ROLE_' 을 자동으로 붙여서 찾는다.
그래서 설정을 할 때 'ROLE_' 을 붙이는 실수를 하지말자. ROLE_ 을 붙여야 한다면 설정을 잘 해야 한다.

 

.anyRequest().authenticated()

anyRequest 는 등록되지 않은 나머지 경로를 다 처리한다.

 

 

[ 2. formLogin]

formLogin 은 SSR 방식에서 form 태그를 통해 로그인을 진행할 때 사용한다.

 

		http
			.formLogin((auth) -> auth
				// 우리가 Custom 한 로그인 페이지 경로를 적는다, 자동으로 redirection 을 해준다.
				.loginPage("/login")
				// html 로그인 id,password 를 특정한 경로로 보낸다 -> Post 방식임.
				.loginProcessingUrl("/loginProc").permitAll()
			);

 

[ 3. formLogout]

		http
			.logout((auth) -> auth
				.logoutUrl("/logout")
				.logoutSuccessUrl("/")
			);

 

[ 4. CSRF]

SSR 방식을 사용한다면 csrf 공격을 방어해야 합니다.

SSR 방식에서는 csrf 검증이 필요합니다.

 

security config 클래스에서 csrf.disable() 설정을 진행하지 않으면 자동으로 enable 설정이 된다 

enable 설정시 스프링 시큐리티는 CsrfFilter 를 통해 post,put,delete 요청에 대해 토큰 검증을 진행한다 (조회는 노상관) 

 

즉 위 csrf 설정이 없으면 CUD 기능이 진행이 되지 않는다

 

그렇다면 form 에서 csrf 토큰을 서버측으로 보내줘야지 서버측에서 검증을 통해 cud 기능을 진행할 수 있다

 

<form action="/loginProc" method="post" name="loginform">
    <H2>로그인 폼</H2>
    <hr>
    <input id="username" type="text" name="username" placeholder="ID" /> <br><br>
    <input id="password" type="password" name="password" placeholder="Password" /> <br><br>
    <input type="hidden" name="csrf" th:value="${_csrf.token}" />
    <button type="submit">로그인 </button>
</form>

 

위 처럼 csrf 토큰을 요청시 심어줘야 한다

 

 

하지만 보통 API 서버의 경우 보통 세션을 Stateless 로 관리하기 때문에 스프링 시큐리티 csrf.enable 설정을 진행하지 않아도 된다.

REST API 서버는 보통 세션을 stateless 인 jwt 로 관리하므로 csrf.enable() 을 설정할 필요 없다. 

// 1)
		http
			.csrf( (csrf) -> csrf.disable()
			);
            
 // 2) 둘 중 아무 방법이나 사용하면 됨
 		http
			.csrf(AbstractHttpConfigurer::disable
			);

 

 

[ 5. Hierarchy 설정]

위 설정은 ADMIN, MASTER, USER 를 제외한 여러 등급에 따른 권한을 다르게 줘야 할 때 사용한다.

ex) 네이버 카페, oo등급 은 게시물을 볼수 없고, 댓글을 달 수 없다

 

	// Hierarchy 설정
	@Bean
	public RoleHierarchy roleHierarchy() {
		return RoleHierarchyImpl.fromHierarchy(
			"ROLE_C > ROLE_B > \n" + "ROLE_B > ROLE_A");
	}

 

	@Bean 
	public SecurityFilterChain filterChain (HttpSecurity http) throws Exception {

		http
			.authorizeHttpRequests((auth) -> auth
				// Hierarchy 설정 -> 접근 권한 A, B, C 설정
				.requestMatchers("/").hasAnyRole("A","B","C")
				.requestMatchers("/manage").hasAnyRole("B","C")
				.requestMatchers("/master").hasRole("C")
				.anyRequest().authenticated()
			);

		return http.build();
		// }

	}

 

위 설정을 통해 역할 별 권한을 줄 수 있다.

 

 

[ 6. Basic Login]

로그인 방식에는 SSR 즉 폼을 통한 로그인 과 SPA(React,Vue) 를 사용하여 API 호출을 통한 로그인이 있다.

http Basic 로그인은 후자의 방식에 해당한다.

 

- 아이디와 비밀번호를 Base64 방식으로 인코딩한 뒤 HTTP 인증 헤더에 부착하여 서버측으로 요청을 보내는 방식이다.
- 특정 페이지가 필요하지 않아, 브라우저에서 헤더에다가 아이디 비밀번호를 넣어서 하는 방식이다.

 

MSA 방식에서 많이 사용을 한다.

		// http basic 방식 로그인
		http
			.httpBasic(Customizer.withDefaults()
			);

 

 

[ 7. 세션 설정]

사용자가 로그인을 진행한 뒤 사용자 정보는 SecurityContextHolder 에 의해서 서버 세션에 관리 된다

 

		// 세션 관리 설정
		http
			.sessionManagement((auth) -> auth
				.maximumSessions(2) // 하나의 아이디에 대한 다중 로그인 허용 개수, 로그인이 되있는 상태에서 2개 기기에 더 로그인할 수 있다.
				.maxSessionsPreventsLogin(true) // true: 초과시 새로운 로그인 차단, false : 초과시 기존 세션 하나 삭제
			);
		// 세션 보호 설정
		http
			.sessionManagement((auth) -> auth
				.sessionFixation().none() // 로그인 시 세션 정보 변경 안함
				.sessionFixation().newSession() // 로그인 시 세션 새로 생성
				.sessionFixation().changeSessionId() /// 로그인 시 동일한 세션에 대한 id 변경
			);

 

 

[ 8. 비밀번호 암호화]

	@Bean
	public BCryptPasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

 

위 BCryptPasswordEncoder 를 통해서 비밀번호를 암 복호화 할 수 있다.

위 기능은 정말 편리하다

 

 

[전체 설정]

@Configuration // 이 클래스는 스프링 부트 한테 Config 클래스로 등록이 된다.
@EnableWebSecurity // 스프링 시큐리티 한테도 관리를 받게 하기 위함.
public class SecurityConfig {

	/*
	* 인가 설정
	* */
	@Bean // @Bean 꼭 등록 해야함,,
	public SecurityFilterChain filterChain (HttpSecurity http) throws Exception {

		http
			// 특정 경로에 요청을 허용하거나 거부하거나 할 수 있게 한다.
			.authorizeHttpRequests((auth) -> auth
				// "/" 및 "/login" 은 모든 사람 접근 가능하게 함
				.requestMatchers("/", "/login","/loginProc","/join", "/joinProc").permitAll()
				// Hierarchy 설정 -> 접근 권한 A, B, C 설정
				.requestMatchers("/").hasAnyRole("A","B","C")
				.requestMatchers("/manage").hasAnyRole("B","C")
				.requestMatchers("/master").hasRole("C")
				// "/admin" 관리자 페이지는 Role 이 ADMIN 인 사람만 접근 가능하게 함. -> ROLE_ADMIN
				.requestMatchers("/admin").hasRole("ADMIN")
				// "/**" 는 {id} 같은거 의미, hasAnyRole 은 여러 역할 처리함, 그리고 hasAnyRole 을 하면 접미사에 'ROLE_' 을 붙여준다.
				.requestMatchers("/my/**").hasAnyRole("ADMIN", "USER")
				// anyRequest 는 등록되지 않은 나머지 경로를 다 처리한다.
				.anyRequest().authenticated()
			);

		http
			.formLogin((auth) -> auth
				.loginPage("/login") // 우리가 Custom 한 로그인 페이지 경로를 적는다, 자동으로 redirection 을 해준다.
				.loginProcessingUrl("/loginProc") // html 로그인 id,password 를 특정한 경로로 보낸다 -> Post 방식임.
				.permitAll()
			);

		http
			.logout((auth) -> auth
				.logoutUrl("/logout")
				.logoutSuccessUrl("/")
			);
		// CSRF 설정, Post 요청시 csrf 토큰도 보내줘야 로그인이 됨.
		http
			.csrf( (csrf) -> csrf.disable()
			);

		// 세션 관리 설정
		http
			.sessionManagement((auth) -> auth
				.maximumSessions(2) // 하나의 아이디에 대한 다중 로그인 허용 개수, 로그인이 되있는 상태에서 2개 기기에 더 로그인할 수 있다.
				.maxSessionsPreventsLogin(true) // true: 초과시 새로운 로그인 차단, false : 초과시 기존 세션 하나 삭제
			);

		// 세션 보호 설정
		http
			.sessionManagement((auth) -> auth
				.sessionFixation().changeSessionId() // 로그인 시 동일한 세션에 대한 id 변경
			);

		// http basic 방식 로그인
		http
			.httpBasic(Customizer.withDefaults()
			);

		return http.build();
		// }

	}

	// Hierarchy 설정
	@Bean
	public RoleHierarchy roleHierarchy() {
		return RoleHierarchyImpl.fromHierarchy(
			"ROLE_C > ROLE_B > \n" + "ROLE_B > ROLE_A");
	}

	@Bean
	public BCryptPasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}



}

 

위 코드 그대로 가져다 붙이면 작동 안돼요...

필요한거만 사용해서 붙여야 합니다!!

 

 

 

결론


 

기존 스프링 시큐리티 5.x 버전 은 메소드 체이닝 방식을 지원했다

하지만 스프링부트가 3.x 로 업그레이드 됨에 따라 스프링 시큐리티도 6.x 로 업그레이드가 되었다.

 

그리고 기존 메소드 체이닝 방식을 Deprecated 하고 함수형(=람다) 방식을 지원한다.

 

그러므로 레거시? 까지는 아니지만

( 스프링 부트 2.x = 스프링 시큐리티 5.x ) 위 조합으로 쓰이던게 현재는

( 스프링 부트 3.x = 스프링 시큐리티 6.x) 의 조합으로 사용이 된다.

[필자가 사용하는 버전임]

 

 

더 자세한 내용은 아래 링크에 있는 공식문서를 참고하길 바랍니다.

 

+ 틀린 부분 및 지적 사항 감사히 받겠습니다. 감사합니다.

 

참조


0) Git : https://github.com/Hyeonqz/spring-security6.2/tree/master/security_6.2
1) 공식문서: https://docs.spring.io/spring-security/reference/servlet/index.html
2) 유튜브 : https://www.youtube.com/watch?v=y0PXQgrkb90&list=PLJkjrxxiBSFCKD9TRKDYn7IE96K2u3C3U

 

728x90