spring security를 사용해봅시다.
spring security가 뭔지 궁금했는데 이번에 사이드 프로젝트를 진행하면서 한번 사용해보기로 했습니다.
장점
- 다양한 인증 매커니즘 지원 : Spring Security는 기본 인증, 양식 기반 인증, OAuth2, OpenID Connect 등 다양한 인증 메커니즘을 지원합니다.
- Java Config 지원 : 보안 설정을 XML 파일 또는 Java Config를 통해 구성할 수 있습니다.
- 기본적인 보안 기능 제공 : 암호화된 비밀번호 저장을 지원하며, 다양한 암호화 알고리즘을 쉽게 사용할 수 있습니다. CSRF 보호: CSRF 공격을 방지하기 위한 기본적인 보호 메커니즘을 제공합니다.
spring secuirty 설정
권한 별 접근 허용할 때 6.1.0 미만의 버전에서는 아래와 같이 사용했었습니다.
우선 Spring Security를 사용하기 위해서는 먼저 의존성을 추가해줘야 합니다.
maven
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
gradle
dependencies {
implementation "org.springframework.boot:spring-boot-starter-security"
}
위와같이 의존성을 추가시키고 문제가 없다면 Spring Boot Application 실행시켰을때 콘솔 화면에 아래와 같은 문구가 출력됩니다.

이후 간단한 Controller를 추가하고, “/” 또는 “” url을 매핑한 다음 해당 페이지로 들어가게 되면
Spring Security에서 기본적으로 제공하는 로그인 페이지로 이동하게 된다.
@GetMapping({"","/"})
public String index() {
return "index"; // src/main/resources/templates/index.mustache
}

처음 사용자 계정은 User가 기본값이며 비밀번호는 Application을 실행할 때 나오는 Log에 출력되는 비밀번호를 입력하면 로그인이 가능합니다.
Spring Security에서 기본적으로 제공하는 페이지를 사용하지 않거나, OAuth2 등 다른 로그인을 원한다면 따로 설정이 필요한데 이러한 설정 내용을 SecurityConfig 클래스에서 작성해줍니다.
제가 사용한 6.1.0 버전에서는 메서드 체이닝의 사용을 지양하고, 람다식을 통해 함수형으로 설정을 지향하고 있는데 이에 따라서 코드 작성 방식이 조금 달라졌습니다.
기존 코드
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable().headers().frameOptions().disable().and()
.authorizeHttpRequests()
.requestMatchers(PathRequest.toH2Console()).permitAll()
.requestMatchers("/", "/login/**").permitAll()
.requestMatchers("/posts/**", "/api/v1/posts/**").hasRole(Role.USER.name())
.requestMatchers("/admins/**", "/api/v1/admins/**").hasRole(Role.ADMIN.name())
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login/login")
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/login/login-proc")
.defaultSuccessUrl("/", true)
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.userDetailsService(myUserDetailsService);
return http.build();
}
새로운 코드
package com.example.springsecuritypractice.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// 해당 메서드의 리턴되는 오브젝트를 IOC로 등록해준다.
@Bean
public BCryptPasswordEncoder encodePwd() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf((csrfConfig) ->
csrfConfig.disable()
)
.authorizeHttpRequests((authorizeRequests) ->
authorizeRequests
.requestMatchers("/user/**").authenticated() // 로그인 한 사람만 접근 가능, 인증만 되면 들어갈 수 있는 주소
.requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER") // admin 또는 manager
.requestMatchers("/admin/**").hasRole("ADMIN") // admin만
.anyRequest().permitAll() // 나머지 url은 모든 접근 허용
).formLogin((formLogin) ->
formLogin.loginPage("/loginForm") // formLogin 활성화, 권한이 없는 경우 403 페이지가 아닌 login 페이지로 이동
.loginProcessingUrl("/login") // /login 주소가 호출이 되면 시큐리티가 낚아채서 대신 로그인을 진행해줌.
.defaultSuccessUrl("/")// 로그인 성공시 url 만약 다른 Url 이 있으면 자동으로 거기로 보내주고, 없으면 defauilt url로 이동
// .usernameParameter("username2") principalDetailsService와 Model의 필드명이 다를 때 usernameParameter로 매핑시켜준다.
);
return http.build();
}
}
우선 이 클래스를 Bean으로 등록해서 사용하기 위해 @Configuration 어노테이션을 붙여주고,
시큐리티 필터가 (SecurityConfig) 가 스프링 필터 체인에 등록될 수 있도록 @EnableWebSecurity 어노테이션을 작성해줍니다.
| CSRF(Cross Site Request Forgery) CSRF란 사이트간 위조 요청을 말하는데 인증된 사용자가 웹 애플리케이션에 특정 요청을 보내도록 유도하는 공격 행위를 말합니다.
크로스 사이트 요청 위조는 사용자가 인증한 세션에서 웹 애플리케이션이 정상적인 요청과 비정상적인 요청을 구분하지 못하는 점을 악용하는 공격 방식으로,
웹 애플리케이션이 사용자의 요청이 실제 사용자가 전송한 것인지 확인하지 않는 경우에 자주 발생합니다.
이러한 것들을 방지하기 위해 Spring Security에서는 CSRF Protection 이 기본값으로 설정되어 있고,
아래와 같이 명시해줄 수도 있습니다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf(Customizer.withDefaults());
return http.build();
}
}
이러한 설정이 활성화 되어 있으면 protection을 통해 GET 방식의 요청을 제외한 상태를 변화시킬 수 있는 POST, PUT, DELETE등의 요청으로부터 보호하게 되는데 <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> 이러한 토큰값이 있어야 요청을 받아들이게 됩니다.
또한 특정 엔드포인트는 CSRF 요청을 무시하도록 설정할 수도 있습니다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.ignoringRequestMatchers("/api/*")
);
return http.build();
}
만약 화면을 Vue나 React 등을 사용한다면 서버는 일반적으로 RestApi를 구성하게 될 텐데
이러한 경우 Session 기반 인증과는 다르게 보통 Token을 이용한 인증 방식을 사용하기 때문에 Stateless하고, 서버에 인증정보를 보관하지 않습니다.
따라서 클라이언트가 권한이 필요한 요청을 할 경우 요청에 필요한 인증 정보(OAuth2, JWT)를 포함시켜 요청하기 때문에 불필요한 CSRF 코드들을 작성할 필요가 없습니다.
이러한 경우에는 아래와 같이 disable 처리를 해주면 됩니다.
.csrf((csrfConfig) ->
csrfConfig.disable()
)
세션 기반 인증과 토큰 기반 인증에 대해서 조금만 더 알아봅시다.
세션 기반 인증
1.사용자가 로그인을 하게되면 서버는 사용자의 인증 정보를 서버 메모리에 저장하고, 사용자에게 세션 Id를 쿠키로 발급해줍니다. 2. 사용자가 다른 요청을 할 때마다 쿠키에 들어있는 세션 Id를 서버에 전송하면 이 세션Id를 이용해 사용자 인증 정보를 조회합니다.
토근 기반 인증
- 사용자가 로그인을 하게 되면 서버는 토큰(ex:JWT)를 발급해 사용자에게 제공해줍니다.(이때 발급된 토큰은 인증정보를 암호화 해서 포함하고 있으며, 민감한 정보는 담지 않습니다.)
- 사용자가 서버로 요청을 보낼 때 이 토큰을 포함시켜 전송하고, 서버는 토큰의 유효성을 확인하고, 토큰에 들어있는 정보를 사용해 사용자를 인식합니다.
세션 기반 인증과 토큰 기반 인증의 차이는 세션 기반 인증의 경우 브라우저가 자동으로 쿠키를 전송하기 때문에 공격자가 악의적인 요청을 만들어 사용자가 해당 요청을 실행하도록 함으로써 CSRF 공격을 할 수 있지만,
토큰 기반 인증 방식의 경우 사용자가 직접 토큰을 요청 헤더에 포함시켜야 하고, 브라우저에서 토큰을 자동으로 전송하지 않기 때문에 공격자가 사용자의 브라우저를 통해 자동으로 토큰을 포함한 요청을 만들기가 어렵습니다.
이러한 이유로 토큰 기반 인증 방식을 사용하게 되면 CSRF 공격에 대한 노출이 크게 감소하므로 CSRF 보호 기능을 비활성화 하는 것이 일반적입니다.
다음은 authorizeHttpRequests 설정에 대한 부분입니다.
여기서는 어떤 URL에 어떤 권한을 가진 사용자를 접근허용할지 설정하는 부분입니다.
예를들어 .requestMatchers("/user/**).authenticated() 와 같이 작성하면 로그인 한 사람만 접근이 가능합니다. 즉 인증만 되면 해당 URL에 접근할 수 있게 됩니다.
.requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER") 와 같이 작성하면 admin 권한 또는 manager 권한을 가진 사용자만 접근 가능하게 됩니다.
마지막으로 .anyRequest().permitAll() 와 같이 작성하면 위에 작성한 url 을 제외한 나머지 url은 모든 접근을 허용하게 됩니다.
전체적인 설정 코드는 아래와 같습니다.
.authorizeHttpRequests((authorizeRequests) ->
authorizeRequests
.requestMatchers("/user/**").authenticated()
.requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER")
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().permitAll()
)
6.1.0 미만 버전에서는 아래 코드와 같이 .antMatchers("/manager").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER'))") 와 같은 형태로 권한 별 접근 URL을 지정했지만
6.1.0 이상 버전에서부터는 여러 권한에 대해 접근을 허용할 때는 hasAnyRole(”권한1”,”권한2”) 와 같이 부여하고, 한개의 권한에 대해 접근을 허용할 때는 hasRole(”권한1”) 과 같이 사용하도록 변경되었습니다.
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/user/**").authenticated()
.antMatchers("/manager").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER'))")
마지막으로 Spring Security에서 제공하는 기본 로그인 폼을 비활성화 시키기 위해서는 아래와 같은 설정이 필요합니다.
.formLogin(login -> login
.loginPage("/view/login") // [A] 커스텀 로그인 페이지 지정
.loginProcessingUrl("/login-process") // [B] submit 받을 url
.usernameParameter("userid") // [C] submit할 아이디
.passwordParameter("pw") // [D] submit할 비밀번호
.defaultSuccessUrl("/view/dashboard", true)
.permitAll()
)
.logout(withDefaults());
다음번에는 Spring Security의 암호화에 대해서 알아보겠습니다.