스프링 시큐리티 자동-구성
build.gradle 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-security' // 스프링 부트 보안 스타터 의존성
implementation 'org.springframework.security:spring-security-test' // 보안 테스트 의존성
스프링 부트 보안 스타터
- 애플리케이션이 시작되면 스프링이 프로젝트의 classpath에 있는 라이브러리를 찾아 기본적인 보안 구성을 설정해 준다.
- 이 후, 웹 브라우저에서 홈페이지에 접속하면 스프링 시큐리티에서 제공하는 HTTP 기본 인증 대화상자가 나타난다.
- Username: user를 입력
- Password: 무작위로 자동 생성되어 애플리케이션 로그 파일에 수록되며, 콘솔 창에서 확인할 수 있다.
Using generated security password: 0a4374a0-240c-4da1-8af4-9e085e9b7ddf
- 해당 36자리의 비밀번호를 Password 필드에 붙여넣기 하면, 애플리케이션을 사용할 수 있는 권한이 부여되어 접속한 페이지가 나타난다.
위와 같이 보안 스타터를 프로젝트 빌드 파일에 추가만 했을 때는 아래와 같은 보안 구성이 제공된다.
- 모든 HTTP 요청 경로는 인증되어야 한다.
- 어떤 특정 역할이나 권한이 없다.
- 로그인 페이지가 따로 없고, 스프링 시큐리티 HTTP 기본 인증을 사용해서 인증된다.
- 사용자는 하나만 있으며, 이름은 user, 비밀번호는 암호화해 준다.
애플리케이션의 보안을 제대로 구축하려면 최소한 아래 기능을 할 수 있도록 스프링 시큐리티를 구성해야 한다.
- 다수의 사용자를 제공하며, 새로운 고객이 사용자로 등록할 수 있는 페이지가 있어야 한다.
- 서로 다른 HTTP 요청 경로마다 서로 다른 보안 규칙을 적용한다.
- 스프링 시큐리티의 HTTP 인증 대화상자 대신 우리의 로그인 페이지로 인증한다.
더보기
※ 참고
보안을 테스트할 때는 웹 브라우저를 private 또는 incognito 모드로 설정하는 것이 좋다.
이렇게 하면 사용자의 검색 세션에 관한 데이터인 쿠키, 임시 인터넷 파일, 열어 본 페이지 목록 및 기타 데이터를 저장하지 못하도록 하기 때문에, 브라우저의 창을 열 때마다 이전 세션의 사용 기록이 반영되지 않는 새로운 세션으로 시작할 수 있다.
커스텀 사용자 스토리지 정의
- 한 명 이상의 사용자를 처리할 수 있도록 사용자 정보를 유지, 관리하는 사용자 스터리지를 구성한다.
- 스프링 시큐리티에서는 여러 가지의 사용자 스토리지 구성 방법을 제공한다.
- 인메모리 사용자 스토리지
- JDBC 기반 사용자 스토리지
- LDAP 기반 사용자 스토리지
- 커스텀 사용자 명세 서비스
- 우선 사용자의 HTTP 요청 경로에 대해 접근 제한과 같은 보안 관련 처리를 할 수 있는 SecurityConfig 설정 파일을 작성한다.
SecurityConfig
- SecurityConfig 클래스는 보안 구성 클래스인 WebSecurityConfigurerAdapter의 서브 클래스로, 두 개의 configure() 메서드를 오버라이딩 한다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 생략
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
// 생략
}
}
configure(AuthenticationManagerBuilder)
- 사용자 인증 정보를 구성하는 메서드로, 사용자 스토리지 중 어떤 것을 선택하든 이 메서드에서 구성한다.
- 인증을 하기 위해 사용잘르 찾는 방법을 지정하는 코드를 작성해야 한며, 이때 인자로 전달된 AuthenticationManagerBuilder를 사용한다.
- AuthenticationManagerBuilder는 인증 명세를 구성하기 위해 빌더 형태의 API를 사용한다.
1. 인메모리 사용자 스토리지
만약 변경이 필요 없는 사용자만 미리 정해 놓고 애플리케이션을 사용하는 경우, 아예 보안 구성 코드 내부에 정의할 수 있다.
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user1")
.password("password1")
.authorities("ROLE_USER")
.and()
.withUser("user2")
.password("password2")
.authorities("ROLE_USER");
}
- inMemoryAuthentication() 메서드를 사용하면 보안 구성 자체에 사용자 정보를 직접 저장할 수 있다.
- withUser()를 호출하면 해당 사용자의 구성이 시작되며, 이때 사용자 이름을 인자로 전달한다.
- 비밀번호와 부여 권한은 각각 password()와 authorities() 메서드의 인자로 전달하여 호출한다.
- and() 메서드를 연속해서 withUser()를 호출하여 여러 사용자를 지정할 수 있다.
※ 주의: 스프링 5 부터는 반드시 비밀번호를 암호화해야 하므로 만약 password() 메서드를 호출하여 암호화하지 않으면, 접근 거부 (HTTP 403) 또는 서버 내부 오류 (HTTP 500) 가 발생한다.
- 인메모리 사용자 스토리지는 테스트 목적이나 간단한 애플리케이션에는 편리하지만, 사용자의 추가, 삭제, 변경 시 보안 구성 코드를 변경한 후 애플리케이션을 다시 빌드하고 배포, 설치해야 한다.
- 따라서, 실제 운영되는 애플리케이션에는 적합하지 않다.
2. JDBC 기반의 사용자 스토리지
- 관계형 데이터베이스에 유지되는 사용자 정보를 인증하기 위해 JDBC를 사용하여 스프링 시큐리티를 구성하는 방법
- AuthenticationManagerBuiler의 jdbcAuthentication()을 호출한다.
- 이때, 데이터베이스를 액세스하는 방법을 알 수 있도록 dataSource() 메서드를 호출하여 DataSource를 설정한다.
@Autowired
DataSource dataSource;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.jdbcAuthentication()
.dataSource(dataSource);
}
- 사용자 정보를 저장하는 테이블과 칼럼의 스키마와 쿼리가 미리 정해져 있기 때문에, 위와 같이 설정해주면 스프링 시큐리티가 내부적으로 쿼리를 수행해준다.
- 사용자 정보는 users 테이블에, 권한은 authorities 테이블에, 그룹의 사용자는 group_members 테이블에, 그룹의 권한은 group_authorities 테이블에 저장된다.
// 해당 사용자의 이름, 비밀번호, 사용 가능한 사용자인지를 나타내는 활성화 여부 조회
// 사용자 인증에 사용된다.
public static final String DEF_USERS_BY_USERNAME_QUERY =
"select username, password, enabled " +
"from users " +
"where username = ?";
// 해당 사용자에게 부여된 권한 조회
public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY =
"select username, authority " +
"from authorities " +
"where username = ?";
// 해당 사용자가 속한 그룹과 그룹 권한 조회
public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY =
"select g.id, g.group_name, ga.authority " +
"from authorities g, group_members gm, group_authorities ga " +
"where gm.username = ? " +
"and g.id = ga.group_id " +
"and g.id = gm.group_id";
- 스프링 시큐리티와 다른 데이터베이스 스키마를 사용한다면, 스프링 시큐리티의 SQL 쿼리를 내가 사용하는 SQL 쿼리로 대체할 수 있다.
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery(
"select username, password, enabled from users " +
"where username=?")
.authoritiesByUsernameQuery(
"select username, authority from authorities " +
"where username=?");
}
※ 단, 아래와 같은 규칙을 지켜야 한다.
- 매개변수는 하나이며, username이어야 한다.
- 사용자 정보 인증 쿼리에서는 username, password, enabled 열의 값을 반환해야 한다.
- 사용자 권한 쿼리에서는 해당 사용자 이름과 부여한 권한을 포함하는 0 또는 다수의 행을 반환할 수 있다.
- 그룹 권한 쿼리에서는 각각 그룹 id, 그룹 이름, 권한 칼럼을 갖는 0 또는 다수의 행을 반환할 수 있다.
비밀번호 암호화
- 비밀번호를 데이터베이스에 저장할 때와 사용자가 입력한 비밀번호는 모두 같은 암호화 알고리즘을 사용해서 암호화해야 한다.
- 비밀번호을 암호화할 때는 passwordEncoder() 메서드를 호출하여 비밀번호 인코더로 지정한다.
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery(
"select username, password, enabled from users " +
"where username=?")
.authoritiesByUsernameQuery(
"select username, authority from authorities " +
"where username=?")
.passwordEncoder(new BCryptPasswordEncoder());
}
- passwordEncoder() 메서드는 스프링 시큐리티의 PasswordEncoder 인터페이스를 구현하는 어떤 객체도 인자로 받을 수 있다.
- 암호화 알고리즘을 구현한 스프링 시큐리티의 모듈에는 아래와 같은 구현 클래스가 포함되어 있다.
BCryptPasswordEncoder | bcrypt를 해싱 암호화한다. |
NoOpPasswordEncoder | 암호화하지 않는다. |
Pbkdf2PasswordEncoder | PBKDF2를 암호화한다. |
SCryptPasswordEncoder | scrypt를 해싱 암호화한다. |
StandardPasswwordEncoder | SHA-256을 해싱 암호화한다. |
- 어떤 비밀번호 인코더를 사용하든, 일단 암호화되어 데이터베이스에 저장된 비밀번호의 암호는 해독하지 않는다.
- 대신 로그인 시에 사용자가 입력한 비밀번호에 동일한 알고리즘을 사용해서 암호화한 후, 데이터베이스의 암호화된 비밀번호와 비교한다. (PasswordEncoder의 matches() 메서드)
3. LDAP 기반 사용자 스토어
- LDAP 기반 인증으로 스프링 시큐리티를 구성하기 위해서는 ldapAuthentication() 메서드를 사용한다.
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.userSearchBase("ou=people)
.userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups")
.groupSearchFilter("member={0}");
}
- userSearchFilter()와 groupSearchFilter() 메서드는 LDAP 기본 쿼리의 필터를 제공하기 위해 사용되며, 여기서는 사용자와 그룹을 검색하기 위해 사용
- userSearchBase() 메서드는 사용자를 찾기 위한 기준점 쿼리를 제공하며, groupSearchBase() 메서드에는 그룹을 찾기 위한 기준점 쿼리를 지정한다.
- 위의 코드는, 사용자는 people의 구성 단위부터, 그룹은 groups의 구성 단위부터 검색 시작
비밀번호 비교
- LDAP의 기본 인증 전략은 사용자가 직접 LDAP 서버에서 인증받도록 하는 것이지만, 비밀번호를 비교하는 방법도 있다.
- 입력된 비밀번호를 LDAP 디렉터리에 전송한 후, 이 비밀번호를 사용자의 비밀번호 값과 비교하도록 LDAP 서버에 요청한다.
- 이때, 비밀번호 비교는 LDAP 서버에서 수행되므로 실제 비밀번호는 노출되지 않는다.
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.userSearchBase("ou=people)
.userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups")
.groupSearchFilter("member={0}")
.passwordCompare()
.passwordEncoder(new BCryptPasswordEncoder())
.passwordAttribute("userPassword");
}
- 비밀번호를 비교하는 방법으로 LDAP 인증을 하고자 할 때는 passwordCompare() 메서드를 사용한다.
- 로그인 폼에 입력된 비밀번호는 사용자의 LDAP 서버에 있는 userPassword 속성값과 비교되기 때문에, 만약 비밀번호가 다른 속성명을 가진다면 passwordAttribute()를 사용해서 속성명을 맞춰주어야 한다.
- LDAP를 이용하면 실제 비밀번호가 서버에 유지된다는 장점이 있지만, 비교되는 비밀번호는 여전히 LDAP 서버에 전달되어야 하므로 중간에 가로채질 위험성이 존재한다.
- 따라서, 이를 방지하기 위해 passwordEncoder() 메서드를 통한 암호화가 필요하다.
원격 LDAP 서버 참조하기
- 기본적으로 스프링 시큐리티의 LDAP 인증에서는 로컬 호스트의 33389 포트로 LDAP 서버가 접소괸다고 간주한다.
- 그러나, 만약 LDAP 서버가 다른 컴퓨터에서 실행 중이라면 contextSource() 메서드를 사용해서 해당 서버의 위치를 구성할 수 있다.
- contextSource() 메서드는 ContextSourceBuilder를 반환하고, ContextSourceBuilder는 url() 메서드 제공
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.ldapAuthentication()
.userSearchBase("ou=people)
.userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups")
.groupSearchFilter("member={0}")
.passwordCompare()
.passwordEncoder(new BCryptPasswordEncoder())
.passwordAttribute("userPassword")
.contextSource().url("{지정할 LDAP 서버 URL}")
}
내장된 LDAP 서버 구성하기
implementation 'org.springframework.boot:spring-boot-starter-data-ldap'
implementation 'org.springframework.ldap:spring-ldap-core'
implementation 'org.springframework.security:spring-security-ldap'
- 인증을 기다리는 LDAP 서버가 없는 경우에는 스프링 시큐리티에서 제공하는 내장 LDAP 서버를 사용할 수 있다.
- 내장된 LDAP 서버를 사용할 때는 원격 LDAP 서버의 URL을 설정하는 대신 root() 메서드를 사용해서 내장 LDAP 서버의 루트 경로를 지정한다.
- LDAP 서버가 시작될 때는 classpath에서 찾을 수 있는 LDIF 파일로부터 데이터를 로드한다.
- LDIF는 일반 텍스트 파일에 LDAP 데이터를 나타내는 표준화된 방법
- 각 레코드는 하나 이상의 줄로 구성되며, 각 줄은 한 쌍으로 된 name:value를 포함한다.
- 각 레코드는 빈줄로 구분된다.
- 만약 스프링이 classpath를 검색하지 않고 LDIF 파일을 찾도록 하려면, ldif() 메서드를 사용해서 LDIF 파일을 찾을 수 있는 경로를 지정할 수 있다.
4. 사용자 인증 커스터마이징
- 만약 스프링에 내장된 사용자 스토어가 요구를 충족하지 못할 때는 커스텀 사용자 명세 서비스를 사용할 수 있다.
User 엔티티
@RequiredArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class User implements UserDetails {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private final String username;
private final String password;
private final String fullname;
private final String street;
private final String city;
private final String state;
private final String zip;
private final String phoneNumber;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- User 클래스는 스프링 시큐리티의 UserDetails 인터페이스를 구현한다.
- UserDetails를 구현한 클래스는 기본 사용자 정볼르 프레임워크에 제공한다.
- 해당 사용자에게 부여된 권한과 해당 사용자 계정을 사용할 수 있는 지의 여부 등
- getAuthorities() 메서드는 해당 사용자에게 부여된 권한을 저장한 컬렉션을 반환한다.
사용자 명세 서비스
@RequiredArgsConstructor
@Service
public class UserRepositoryUserDetailsService implements UserDetailsService {
private final UserRepository userRepo;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepo.findByUsername(username);
if (user != null) {
return user;
}
throw new UsernameNotFoundException("User " + username + " not founded");
}
}
- UserDetailService 인터페이스를 구현하는 클래스의 메서드에는 사용자 이름이 인자로 전달되며, 메서드 실행 후 UserDetails 객체가 반환되거나, 해당 사용자 이름이 없으면 UsernameNotFoundException을 발생시킨다.
- @Service 어노테이션을 사용하면 스프링이 자동으로 이 클래스를 찾아 빈으로 생성해주기 때문에, 해당 클래스를 빈으로 선언하지 않아도 된다.
- But, 커스텀 명세 서비스를 스프링 시큐리티(SecurityConfig)에 구성해주어야 한다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/design", "/orders")
.access("hasRole('ROLE_USER')")
.antMatchers("/", "/**").access("permitAll")
.and()
.httpBasic();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(encoder());
}
}
PasswordEncoder
- encoder() 메서드에는 @Bean 어노테이션 지정
- encoder() 메서드가 생성한 BCryptPasswordEncoder 인스턴스가 스프링 애플리케이션 컨텍스트에 등록, 관리되며, 이 인스턴스가 애플리케이션 컨텍스트로부터 주입되어 반환된다.
- 따라서, 우리가 원하는 종류의 PasswordEncoder 빈 객체를 스프링의 관리하에 사용할 수 있다.
- 이는 클래스와 클래스의 인스턴스 생성 및 주입의 전 과정을 스프링이 관리하는 @Component 어노테이션과는 의미가 다르다.
웹 보안 요청 처리
- 보안 규칙을 구성하려면 SecurityConfig 클래스에 configure(HttpSecurity) 메서드를 오버라이딩 해야 한다.
- configure 메서드는 HttpSecurity 객체를 인자로 받으며, 웹 수준에서 보안을 처리하는 방법을 구성하는데 사용된다.
- HttpSecurity를 사용해서 구성할 수 있는 것은 아래와 같다.
- HTTP 요청 처리를 허용하기 전에 충족되어야 할 특정 보안 조건을 구성한다.
- 커스텀 로그인 페이지를 구성한다.
- 사용자가 애플리케이션의 로그아웃을 할 수 있도록 한다.
- CSRF 공격으로부터 보호하도록 구성한다.
- 특히, 해당 요청 시에 사용자가 합당한 권한을 갖는지 확인하는 것이 HttpSecurity 구성에서 가장 많이 하는 것 중 하나
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/design", "/orders")
.hasRole("ROLE_USER")
.antMatchers("/", "/**").access("permitAll");
}
- authorizeRequests() 는 ExpressionInterceptUrlRegistry 객체를 반환한다.
- 이 객체를 사용하면 URL 경로와 패턴 및 해당 경로의 보안 요구사항을 구성할 수 있다.
- antMatchers()에서 지정된 경로의 패턴 일치를 검사하므로, 먼저 지정된 보안 규칙이 우선적으로 처리된다.
- hasRole()과 permitAll()은 요청 경로의 보안 요구를 선언하는 메서드이다.
- 대부분의 메서드는 요청 처리의 기본적인 보안 규칙을 제공하지만, 각 메서드에 정의된 보안 규칙만 사용된다는 제약
- 따라서, 이의 대안으로 access() 메서드를 통해 SpEL을 사용할 수 있다.
- 스프링 시큐리티에서는 SpEL을 확장하여 보안 관련 특정 값과 함수를 제공한다.
커스텀 로그인 페이지 생성
- 기본 로그인 페이지를 교체하려면 우선 커스텀 로그인 페이지가 있는 경로를 스프링 시큐리티에 알려주어야 한다.
- 이는 HttpSecurity 객체의 formLogin()을 호출하면 된다.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/design", "/orders")
.hasRole("ROLE_USER")
.antMatchers("/", "/**")
.access("permitAll")
.and()
.formLogin()
.loginPage("/login");
}
- and() 메서드는 인증 구성이 끝나서 추가적인 HTTP 구성을 적용할 준비가 되었다는 것을 나타내며, 새로운 구성을 시작할 때마다 사용할 수 있다.
- formLogin()에 커스텀 로그인 페이지의 경로를 지정하면, 사용자가 인증되지 않아 로그인이 필요하다고 스프링 시큐리티가 판단할 때 해당 경로로 연결해 준다.
- 기본적으로 스프링 시큐리티는 /login 경로로 로그인 요청을 처리하며, 사용자 이름과 비밀번호 필드명은 username과 password로 간주한다.
- 로그인 경로와 필드 이름을 변경하려면 아래와 같이 작성한다.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/design", "/orders")
.hasRole("ROLE_USER")
.antMatchers("/", "/**")
.access("permitAll")
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("{지정할 경로}")
.usernameParameter("user")
.passwordParameter("pwt");
}
- 로그인하면 해당 사용자의 로그인이 필요하다고 스프링 시큐리티가 판단했을 당시에 사용자가 머물던 페이지로 바로 이동하지만, 사용자가 직접 로그인 페이지로 이동했을 경우는 로그인한 후 루트 경로로 이동한다.
- 이를 변경하고 싶으면 아래와 같이 작성한다.
- 만약 사용자가 로그인 전에 어떤 페이지에 있었는지와는 무관하게 로그인 후에 무조건 이동시키고 싶은 경우에는 defaultSuccessUrl의 두 번재 인자로 true를 지정한다.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/design", "/orders")
.hasRole("ROLE_USER")
.antMatchers("/", "/**")
.access("permitAll")
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("{이동할 페이지}")
}
로그아웃
로그아웃을 하기 위해서는 HttpSecurity 객체의 logout을 호출해야 한다.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/design", "/orders")
.hasRole("ROLE_USER")
.antMatchers("/", "/**")
.access("permitAll")
.and()
.formLogin()
.loginPage("/login")
.and()
.logout() // /logout의 POST 요청을 가로채는 보안 필터 설정
.logoutSuccessUrl("/");
}
뷰 템플릿
<form method="POST" th:action="@{/logout}">
<input type="submit" value="Logout"/>
</form>
- 사용자가 로그아웃 버튼을 클릭하면 세션이 종료되고 애플리케이션에서 로그아웃된다.
- 이때 사용자는 기본적으로 로그인 페이지로 다시 이동되지만, 다른 페이지로 이동시키고 싶다면 로그아웃 이후에 이동할 페이지를 지정하여 logoutSucessUrl()을 호출하면 된다.
CSRF 공격 방어
더보기
CSRF란 사용자가 웹 사이트에 로그인한 상태에서 악의적인 코드가 삽입된 페이지를 열면 공격 대상이 되는 웹 사이트에 자동으로 폼이 제출되고, 이 사이트는 위조된 공격 명령이 믿을 수 있는 사용자로부터 제출된 것으로 판단하게 되어 공격에 노출된다.
- CSRF 공격을 막기 위해 애플리케이션에서는 폼의 숨김 필드에 넣을 CSRF 토큰을 생성할 수 있다.
- 해당 폼이 제출될 때 폼의 다른 데이터와 함께 토큰도 서버로 전송된다.
- 서버에서는 이 토큰을 원래 생성했던 토큰과 비교하며, 토큰이 일치하면 해당 요청의 처리가 허용된다.
- 일치하지 않는다면 해당 폼은 토큰이 있다는 사실을 모르는 악의적인 웹사이트에서 제출된 것으로 간주한다.
- 스프링 시큐리티에는 내장된 CSRF 방어 기능이 있으며, 기본적으로 활성화되어 있어 별도로 구성할 필요가 없다.
- 단지 CSRF 토큰을 넣을 _csrf라는 이름의 필드를 애플리케이션이 제출하는 폼에 포함시키면 된다.
<input type="hidden" name="_csrf" th:value="${_csrf.token}"/>
※ 만약 스프링 MVC의 JSP 태그 라이브러리 또는 Thymeleaf를 스프링 시큐리티 dialect와 함께 사용 중이라면 숨김 필드도 자동으로 생성되므로 지정할 필요가 없다.
'Spring > Spring Security' 카테고리의 다른 글
OAuth2 로그인 인증 (0) | 2022.01.21 |
---|---|
JWT 인증 구현 (0) | 2022.01.21 |