📝 스프링 시큐리티란?
- 스프링 시큐리티는 스프링 기반 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크입니다.
- 주로 서블릿 필터와 이들로 구성된 필터체인으로의 위임모델을 사용하며, 보안과 관련하여 체계적으로 많은 옵션을 제공해주기 때문에 개발자 입장에서는 일일이 보안관련 로직을 작성하지 않아도 됩니다.
스프링 시큐리티를 접하기 전, 밑에 나열한 보안 용어들을 숙지해놓으면 도움이 될 것입니다.
- 접근 주체(
Principal
) : 보호된 리소스에 접근하는 대상 - 인증(
Authentication
) : 보호된 리소스에 접근한 대상(Principal
)에 대해 이 사용자가 누구인지, 애플리케이션의 작업을 수행해도 되는 주체인지 등을 확인하는 과정 - 인가(
Authorize
) : 해당 리소스에 대해 접근 가능한 권한을 가지고 있는지 확인하는 과정(인증 이후의 과정) - 권한 : 어떤 리소스에 대한 접근 제한, 모든 리소스는 접근 제어 권한이 걸려있다. 즉, 인가 과정에서 해당 리소스에 대해 제한된 최소한의 권한을 가졌는지 확인하는 것
📝 스프링 시큐리티 인증 아키텍쳐
- 스프링 시큐리티의 인증 절차는 아래와 같이 이루어집니다.
- 사용자가
Form
을 통해 로그인 정보를 입력하고 인증 요청을 보냅니다. HttpServletRequest
에서 사용자가 보낸 아이디와 패스워드를AuthenticationFilter
가 인터셉트하여 유효성 검사를 수행한 후, 인증용 객체(UsernamePasswordAuthenticationToken
)를 생성하여AuthenticationManager
인터페이스(구현체 -ProviderManager
)에게 위임합니다.AuthenticationFilter
에게 인증용 객체(UsernamePasswordAuthenticationToken
)를 전달 받습니다.- 실제 인증 처리를 수행할
AuthenticationProvider
에게 인증용 객체(UsernamePasswordAuthenticationToken
)를 다시 전달합니다. - DB에서 사용자 인증 정보를 가져올
UserDetailsService
객체에게 사용자 아이디를 넘겨주고 DB에서 인증에 사용할 사용자 정보를UserDetails
객체로 전달 받습니다. AuthenticationProvider
는UserDetails
객체를 전달 받은 이후 실제 사용자의 입력 정보와UserDetails
객체와의 비교를 통해 인증을 시도합니다.- 인증이 완료되면 사용자 정보를 가진
Authentication
객체를SecurityContextHolder
에 담은 이후AuthenticationSuccessHandler
를 실행합니다(실패시AuthenticationFailureHandler
를 실행합니다).
📝 스프링 시큐리티 필터
- 스프링 시큐리티는 다음과 같은 서블릿 필터들을 제공하고 있습니다.
필터 | 역할 |
---|---|
SecurityContextPersistenceFilter |
SecurityContextRepository 에서 SecurityContext 를 가져오거나 저장하는 역할을 수행한다. |
LogoutFilter |
설정된 로그아웃 URL로 오는 요청을 감시하며, 해당 유저를 로그아웃 처리한다. |
UsernamePasswordAuthenticationFilter |
아이디와 비밀번호를 사용하는 Form 기반 인증 설정된 로그인 URL로 오는 요청을 감시하며, 유저 인증 처리를 수행한다. |
DefaultLoginPageGeneratingFilter |
인증을 위한 로그인 폼 URL을 감시한다. |
BasicAuthenticationFilter |
HTTP 기본 인증 헤더를 감시하여 처리한다. |
RequestCacheAwareFilter |
로그인 성공 후, 원래 요청 정보를 재구성하기 위해 사용된다. |
SecurityContextHolderAwareRequestFilter |
HttpServletRequestWrapper 를 상속한 SecurityContextHolderAwareRequestWrapper 클래스로 HttpServletRequest 정보를 감싼다. SecurityContextHolderAwareRequestWrapper 클래스는 필터 체인상의 다음 필터들에게 부가정보를 제공한다. |
AnonymousAuthenticationFilter |
해당 필터가 호출되는 시점까지 사용자 정보가 인증되지 않았다면 인증 토큰에 사용자가 익명 사용자로 나타난다. |
SessionManagementFilter |
인증된 사용자와 관련된 모든 세션을 추적한다. |
ExceptionTranslationFilter |
보호된 요청을 처리하는 중에 발생할 수 있는 예외를 위임하거나 전달하는 역할을 한다. |
FilterSecurityInterceptor |
AccessDecisionManager 로 권한부여 처리를 위임함으로써 접근 제어 결정을 쉽게 해준다. |
- 위처럼 많은 필터들이 제공되고 있기 때문에 우리는 필요한 필터들만 구현하여 사용할 수 있습니다.
📝 AuthenticationManager
- 사용자의 요청을
AuthenticationFilter
에서Authentication
객체로 변환하여AuthenticationManager
(ProviderManager
)에게 넘겨주고AuthenticationProvider
(DaoAuthenticationProvider
)가 실제 인증을 수행한 이후에 인증이 완료되면Authentication
객체를 반환하여 저장한다고 하였습니다. - 위 과정을 좀 더 자세히 살펴보겠습니다.
1. AbstractAuthenticationProcessingFilter
는 웹 기반 인증요청에서 사용되는 컴포넌트로 POST 폼 데이터를 포함하는 요청을 처리합니다. 사용자 비밀번호를 다른 필터로 전달하기 위해 Authentication
객체를 생성하고 일부 프로퍼티를 설정하게 됩니다.
2. AuthenticationManager
는 인증 요청을 받은 뒤, Authentication
객체를 채워줍니다.
3. AuthenticationProvider
는 실제 인증을 수행하고 만약 인증에 성공할 시 Authentication
객체의 authenticated
값을 true
로 설정합니다.
4. DaoAuthenticationProvider
는 UserDetailsService
타입 오브젝트로 인증용 객체를 위임합니다.
5. UserDetailsService
는 UserDetails
구현체를 리턴하는 역할을 합니다. 여기서 UserDetails
인터페이스는 이전에 설명한 Authentication
인터페이스와 상당히 유사하지만 서로 다른 목적을 가진 인터페이스이므로 확실히 구분해야 합니다.
Authentication
: 사용자 ID, 패스워드와 인증 요청 컨텍스트에 대한 정보를 가지고 있다. 인증 이후의 사용자 상세정보와 같은 UserDetails 타입 오브젝트를 포함할 수도 있다.UserDetails
: 이름, 이메일, 전화번호와 같은 사용자 프로필 정보를 저장하기 위한 용도로 사용한다.
📝 Authentication Exception
- 인증과 관련된 모든 예외는
AuthenticationException
을 상속하며,AuthenticationException
은 개발자에게 상세한 디버깅 정보를 제공하기 위한 두 개의 멤버 필드를 가지고 있습니다.authentication
: 인증 요청 관련Authentication
객체를 저장하고 있다.extraInformation
: 인증 예외 관련 부가 정보를 저장한다. 예를 들어UsernameNotFoundException
예외는 인증에 실패한 유저의 ID 정보를 저장하고 있다.
📝 스프링 시큐리티 사용해보기
- 스프링 시큐리티를 활용한 예제를 구현해보겠습니다.
📜 의존성 추가
- 스프링 시큐리티를 사용하기 위해 의존성 추가를 수행합니다.
1. Maven Project - pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2. Gradle Project - build.gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
📜 Security Config 설정
- Security Config 설정 클래스를 구현합니다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin() //Form 로그인 인증 기능이 작동함
.loginPage("/login.html") //사용자 정의 로그인 페이지
.defaultSuccessUrl("/home") //로그인 성공 후 이동 페이지
.failureUrl("/login.html?error=true") // 로그인 실패 후 이동 페이지
.usernameParameter("username") //아이디 파라미터명 설정
.passwordParameter("password") //패스워드 파라미터명 설정
.loginProcessingUrl("/login") //로그인 Form Action Url
.successHandler(loginSuccessHandler()) //로그인 성공 후 핸들러 (해당 핸들러를 생성하여 핸들링 해준다.)
.failureHandler(loginFailureHandler()) //로그인 실패 후 핸들러 (해당 핸들러를 생성하여 핸들링 해준다.)
.permitAll(); //사용자 정의 로그인 페이지 접근 권한 승인
}
}
@EnableWebSecurity
어노테이션을WebSecurityconfigurerAdapter
를 상속하는 설정 객체에 붙혀주면SpringSecurityFilterChain
에 등록됩니다.
@EnableWebSecurity는 스프링 MVC에서 웹 보안을 활성화하기 위한 어노테이션으로 핸들러 메소드에서 @AuthenticationPrincipal 어노테이션이 붙은 매개변수를 이용해 인증처리를 수행합니다. 그리고 자동으로 CSRF 토큰을 스프링의 form binding tag library를 사용해 추가하는 빈을 설정합니다.
📜 로그인 요청
- 사용자는 로그인 하기 위해 아래와 같은 화면에서 아이디와 비밀번호를 입력하여 로그인 요청을 수행하게 됩니다.
📜 UserDetails 객체 생성
- 인증 처리 후 로그인 한 사용자를 세션에 등록하기 위해
UserDetails
인터페이스를 구현하는 클래스를 생성합니다.
@Getter
public class PrincipalDetail implements UserDetails {
private User user;
public PrincipalDetail(User user) {
this.user = user;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
// 계정이 만료되었는가?
@Override
public boolean isAccountNonExpired() {
return true;
}
// 계정이 잠겨있는가?
@Override
public boolean isAccountNonLocked() {
return true;
}
// 비밀번호가 만료되었는가?
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 사용 가능한 계정인가?
@Override
public boolean isEnabled() {
return true;
}
//계정이 갖고 있는 권한 목록 반환
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collectors = new ArrayList<>();
collectors.add(() -> {
return "ROLE_" + user.getRole();
});
return collectors;
}
}
- 스프링 시큐리티가 인증을 수행하여 인증이 성공한다면 로그인을 진행하고 위 클래스의 객체를 스프링 시큐리티 고유 세션 저장소에 저장할 것입니다.
📜 패스워드 인코딩
- DB에 저장되어 있는 사용자 패스워드와 로그인 화면에서 사용자가 입력한 패스워드를 서로 비교하기 위해 DB에 저장되어 있는 패스워드가 어떤 방식으로 인코딩되어 저장되어 있는지 스프링 시큐리티에게 알려줍니다.
@Configuration
@EnableWebSecurity
public class SecurityConfig implements WebMvcConfigurer {
@Bean
public BCryptPasswordEncoder encodePWD() {
return new BCryptPasswordEncoder();
}
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(principalDetailService).passwordEncoder(encodePWD());
}
}
- 이때
UserDetailsService
객체를 매개변수로 전달해주어야 하는데, 이를 위해 아래와 같이UserDetailsService
인터페이스를 구현하는 클래스를 생성해줍니다.
@Service
@RequiredArgsConstructor
public class PrincipalDetailService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User principal = userRepository.findByUsername(username)
.orElseThrow(() -> {
return new UsernameNotFoundException("해당 사용자를 찾을 수 없습니다.");
});
return new PrincipalDetail(principal);
}
}
- 패스워드와 관련된 부분은 스프링 시큐리티에서 알아서 처리해주기 때문에 사용자 아이디와 관련된 부분만 처리해주었습니다.
- 만약 위 클래스를 구현하지 않는다면, 인증에 성공했을 때 현재 DB에 저장되어 있는 사용자에 대한 정보가
UserDetails
객체에 저장되지 않습니다.
'🥑 Web Technoloy' 카테고리의 다른 글
Lombok이란? (1) | 2023.06.04 |
---|---|
Spring AOP(Aspect Oriented Programming) (0) | 2023.06.03 |
SpringBoot에서 SMTP를 활용한 메일 전송 구현하기 (0) | 2022.11.16 |
Spring Interceptor 개념 정리, 적용법 (0) | 2022.11.15 |
OAuth 2.0 개념 정리 (0) | 2022.11.11 |