스프링 시큐리티(Spring Security)에 대해서 공부한 내용을 정리하며 진행 이해한 내용을 바탕으로 풀어서 정리하였다.
먼저 스프링 시큐리티라고 하면 제일먼저 등장하는 흐름도이다.
아래는 스프링 필터체인의 세부화된 그림으로 이 두가지 그림을 같이 보면서 정리하려고 한다.
스프링상 보안에는 필터/체인 이 존재하는데 요청(Request)가 오게될 경우에는 인증 및 권한 부여를 위해 필터를 하나씩 통과하는 과정을 거치게 된다. 그리고 각 기능별로 관련필터를 찾을 때까지 체인을 통과하는 과정을 겪는다.
예시
- HTTP 기본 인증 요청은 BasicAuthenticationFilter에 도달할때까지 필터 체인을 통과한다.
- HTTP Digest 인증 요청은 DigestAuthenticationFilter에 도달할때까지 필터 체인을 통과한다.
- 사용자는 로그인 화면을 통해 아이디와 패스워드를 입력하고 양식을 제출하려고 요청한다.
- 1번의 상황에서 시큐리티 필터체인은 보라색으로 표시한 UsernamePasswordAuthenticationFilter에 도달할 때까지 필터체인을 통과하게 된다.
- 사용자는 로그인 인증 요청을 하게되고 이것을 AuthenticationFilter가 수신하고 여기서 사용자 이름과 비밀번호를 추출한다. 그리고 이것을 바탕으로 인증객체인 UsernamePasswordAuthenticationToken을 생성한다.
- 이 생성한 인증토큰(UsernamePasswordAuthenticationToken) 을 AuthenticationManager 에게 역할을 위임한다.
- 이 AuthenticationManager는 인터페이스로 실제 구현체는 ProviderManager 이다.
- 실제 구현체 ProviderManager는 인증시 필요한 목록(AuthenticationProvider (s) ) 들을 들고 있는데 살펴보고 좀전에 만든 인증객체(UsernamePasswordAuthenticationToken)을 기반으로 사용자 인증을 시도한다.
- 사용자의 좀더 세부정보를 검색하기 위해 AuthenticationProvider는 UserDetailsService를 찾아보게 되고 UserDetailsService는 사용자 이름을 기반으로 UserDetails를 뒤적이게 된다.
- UserDetails는 인터페이스로 실제 구현은 User에 구현되어있다. 그래서 User를 검색한다.
- AuthenticationProvider는 인증이 성공할 경우 인증 개체를 반환하고 그렇지않고 실패할 경우 AuthenticationException을 던져준다.
- AuthenticationManager는 인증 개체를 받아서 다시 인증 필터로 돌려준다.
- 그리고 필터는 향후 필터 사용을 위해 획득한 인증 개체를 SecurityContext에 저장해둔다.
여기까지 스프링 시큐리티의 흐름에 대해서 간단히 정리해봤다.
이제 코드를 바탕으로 시큐리티를 만들어보자!
여기서는 CSRF 토큰 기반으로 시큐리티를 적용했다는점 참고 하길 바란다.
역시 제일 먼저 할일은 Build.Gradle에 추가하여 의존성 주입하는 것!
// spring security
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
두 가지를 추가했다.
- 위에는 스프링 시큐리티를 사용하겠다고 알려주는 것
- 타임리프에서 스프링 시큐리티 관련 태그들과 인증관련 기능들을 사용하겠다는 것이다.
설정파일(Config : SecurityConfig.java) 파일
package com.bootproj.pmcweb.Config;
import lombok.AllArgsConstructor;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
public void configure(WebSecurity web) throws Exception
{
// static 디렉터리 하위 목록은 무시
// csrf에 의해서 보호하지 않음
web.ignoring().antMatchers("/css/**", "/img/**","/vendor/**","/js/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception{
http.authorizeRequests()
// 페이지 권한 설정
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/userInfo/**").hasRole("NORMAL")
.antMatchers("/", "/home", "/user/signup","/user/sendSignUpEmail").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/user/login")
.defaultSuccessUrl("/user/loginSuccess")
// .defaultSuccessUrl("/")
.permitAll()
.and() // 로그아웃 설정
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.permitAll();
}
}
제일 먼저 설명해야 하는 부분은 스프링한테 알려줘야하는 일이다.
@EnableWebSecurity 가 이 역할을 대신 해준다.
스프링이 처음에 딱 실행되면 @Configuration 이 붙은 것을 찾아가는데 여기에 @EnableWebSecurity를 쓰면 스프링은 아 여기가 스프링 시큐리티가 설정되어있는 파일이구나 하고 인식을 한다.
다음으로 WebSecurityConfigurerAdapter를 상속 받음으로써 본격적으로 스프링 시큐리티를 쓰겠다, 사용하겠다 라는 의미
WebSecurityConfigurerAdapter를 상속받게되면 아래의 세 가지 메소드가 오버라이딩 된다.
- configure(WebSecurity web)
- configure(HttpSecurity http)
- configure(AuthenticationMangerBuilder auth)
configure(WebSecurity web)
- 서비스 전체에 영향을 미치는 설정으로 CSRF의 보안절차를 건너뛰거나, 디버그모드 세팅, 방화벽 설정 등을 통해 특정 리퀘스트를 거부할 수 있다. 여기서는 web.ignoring 메서드를 통해 공통적인 css, js, fonts, vendor 등을 생략
configure(HttpSecurity http)
- HttpSecurity 클래스를 주입함으로써 웹 기반 보안을 구성 할 수 있도록 도와주는 메소드 이다. http 취약성에 대한 보안이 필요한 경우에 여기서 지정할 수 있다.
- HttpSecurity에서 지원하는 authorizeRequest 메소드는 URL 패턴을 통해 HttpServletRequest 접근에 대한 제한 권한을 획득한다.
- 시큐리티는 기본적인 로그인(/login) 페이지와 로그인 실패(/login?error) 페이지를 HttpSecurity 상속받은 FormLoginConfigurer에서 제공해준다. 사용자가 지정 경로를 커스터마이징해서 사용할 수 있다.
- HttpSecurity에서 제공하는 CsrfCOnfigurer는 requireCsrfProtectionMatcher에 지정한 방법에 대해 CSRF 보호기능 활성화와 ignoringAntMatchers() 메소드의 인자로 등록한 url에 대해서는 CSRF 보호를 하지않게 설정할 수 있다.
- LogoutConfigurer를 통해 logoutRequestMatcher 클래스의 인자(url)로 들어오는 RequestMatcher 객체로 로그아웃을 진행한다.
configure(AuthenticationMangerBuilder auth)
- 이 메소드는 AuthenticationManager() 구현에 사용되는 메소드인데 대게 UserDetailsService를 받는 서비스를 지정해서 사용할 수 있다.
UserDetailsService를 받은 AccountSecurityService.java 파일
package com.bootproj.pmcweb.Service;
import com.bootproj.pmcweb.Domain.Account;
import com.bootproj.pmcweb.Domain.enumclass.UserRole;
import com.bootproj.pmcweb.Network.Exception.DuplicateEmailException;
import com.bootproj.pmcweb.Network.Exception.NoMatchingAcountException;
import com.bootproj.pmcweb.Network.Exception.SendEmailException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Slf4j
@Service
public class AccountSecurityService implements UserDetailsService {
@Autowired
private AccountServiceImpl accountServiceimpl;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = accountServiceimpl.getUserByEmail(username);
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(account.getRole()));
return new User(account.getEmail(), account.getPassword(), authorities);
}
public Account save(Account account) throws SendEmailException, DuplicateEmailException {
account.setPassword(passwordEncoder.encode(account.getPassword()));
return accountServiceimpl.sendSignUpEmail(account);
}
}
UserDetailsService를 받게되면 자동적으로 loadUserByUsername을 상속받아줘야 한다.
이 메서드를 통해서 실제 로그인할 때 사용자 정보를 비교해서 인증과정을 거쳐서 로그인하게 도와준다.
Return 값에있는 User는 실제 UserDetails의 구현체로 데이터베이스에서 조회한 username을 받아서 User에 던져줌으로써 두개를 비교하여 인증 절차를 진행하는 것이다.
→ 여기서 주의할 점은 Security에서 username으로 인식할 값을 폼에서 name값으로 지정해줘야한다는 것이다.
→ 필자는 여기서 email의 name값을 username으로 주었다. 그리고 form으로 말아서 submit으로 보내주었다
<input type="email" name="username" id="inputEmail" class="form-control" placeholder="Email address" required autofocus>
<label for="inputPassword" class="sr-only">Password</label>
<input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required>
스프링 시큐리티에서는 별다른 설정을 하지 않더라도 기본적으로 CSRF라는 기능이 활성화 된다.
Cross-site request forgery의 약자로 다른 곳에서 form 데이터를 통해(로그인 데이터) 공격하려고 할때 방지해주기 위해 사용하는 토큰값이다.
타임리프 형태로 form을 구성하게 되면 자동적으로 csrf token 정보를 header 정보에 포함하여 서버 요청을 하게 한다.
이상으로 스프링 시큐리티 설정에 대한 핵심부분에 대한 내용을 정리해봤다. 하지만 보완할 점이 많은 것 같다.
다음에는 csrf 토큰 값이 아닌 jwt 토큰을 사용하여 변경해보려고 한다.
참고사이트
cheese10yun.github.io/spring-csrf/
springbootdev.com/2017/08/23/spring-security-authentication-architecture/
'Dev > Spring Boot' 카테고리의 다른 글
스프링부트 공통 설정 그놈 Logback (0) | 2021.04.07 |
---|---|
스프링 시큐리티 로그인 인증 그 후... (0) | 2020.12.02 |
@Valid로 유효성 체크하기 (0) | 2020.11.15 |
스프링 빈 주입방법과 템플릿 우선순위 (3) | 2020.10.15 |
테스트에 사용하는 어노테이션 (0) | 2020.10.03 |