2023. 1. 12. 19:16ㆍ기술 창고/Spring
프로젝트를 진행하면서 구현한 로그인과 같은 보안과 직결된 기능들을 구현을 할 때 Spring Security와 jwt를 사용하여 구현을 했다.
아무래도 초보 개발자이기도 하고 직접적으로 모든 것을 구현하기에는 실력이 부족한 것이 사실이기 때문에 Spring 에서 제공해주는 기능을 활용해보기로 한 것이다.
하지만 Spring Security를 활용해서 구현을 하긴 했지만 처음 보는 내용들이 너무나 많기 때문에 두루뭉술하게 알거나 정확한 용도를 알지 못한 채 구현에만 초점을 두고 사용을 하여 정말로 이해하고 사용했는가 라고 물어본다면 당연히 아니라고 할 것 같다.
따라서 Spring Security에 대해 정리해보는 시간을 가져보도록 하자.
jwt 에 대한 내용은 따로 정리하도록 해야겠다.
Spring Security
Spring Security는 Spring 기반의 애플리케이션의 보안(인증, 권한, 인가 등)을 담당하는 Spring 하위 프레임워크이다.
Spring Security는 인증과 권한에 대한 작업을 Filter 흐름에 따라 처리하고 있다.
Filter는 Dispatcher Servlet으로 가기 이전에 적용되므로 가장 먼저 URL 요청을 받지만, interceptor는 Dispatcher Servlet 과 Controller 사이에 위치한다는 점에서 적용 시기에 차이가 있다.
# Spring Security 아키텍쳐 구조의 처리과정에 대해서 자세하고 잘 정리해주신 블로그가 있다. 참고하도록 하자.
https://mangkyu.tistory.com/77
Spring Security Architecture
인증 / 인가
Spring Security는 인증관 권한에 대한 작업을 Spring에서 Filter의 흐름에 따라 처리해준다고 하였다.
여기서 인증과 인가가 무엇인가에 대해서부터 알아보자.
- 인증 (Authentication)
- 해당 사용자가 본인이 맞는지 확인하는 절차
- 인가 (Authorization)
- 인증된 사용자가 요청한 자원에 접근 가능한 권한을 가지고 있는지 확인하는 절차
즉, Spring Security는 우선 인증 작업이 정상적으로 완료가 된 후에 인가 절차를 수행한다.
이러한 인증 / 인가 절차를 위해 Principal 을 아이디, Credential 을 비밀번호로 사용하는 Credential 기반의 인증 방식을 사용한다.
Spring Security 모듈
▶ SecurityContextHolder
SecurityContextHolder는 보안 주체의 세부 정보(예: 사용자 정보)를 포함하여 응용 프로그램의 현재 보안 Context에 대한 세부 정보(어떠한 보안 방식이 적용되었는지)가 저장된다.
▶ SecurityContext
Authentication(인증 정보) 을 저장하여 보관하는 모듈이다.
SecurityContext를 통해 Authentication 객체를 불러올 수 있다.
▶ Authentication
Authentication은 현재 접근하는 주체의 정보와 권한(Authorization)을 담는 인터페이스이다.
Authentication 객체는 SecurityContext에 저장되며, SecurityContextHolder에서 SecurityContext에 접근해어 불러올 수 있다.
public interface Authentication extends Principal, Serializable {
// 현재 사용자의 권한 목록을 가져옴
Collection<? extends GrantedAuthority> getAuthorities();
// credentials(비밀번호)을 가져옴
Object getCredentials();
Object getDetails();
// Principal(아이디) 객체를 가져옴.
Object getPrincipal();
// 인증 여부를 가져옴
boolean isAuthenticated();
// 인증 여부를 설정함
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
▶ UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken 은 Authentication을 implements한 AbstractAuthenticationToken의 하위 클래스로, 사용자의 아이디가 Principal, 비밀번호가 Credential 역할을 한다.
UsernamePasswordAuthenticationToken의 첫번째 생성자는 인증 전의 객체,
UsernamePasswordAuthenticationToken의 두번째 생성자는 인증 후의 객체를 생성한다.
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
// 사용자의 ID에 해당함
private final Object principal;
// 사용자의 PW에 해당함
private Object credentials;
// 인증 전의 객체 생성
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
// 인증 후의 객체 생성
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // true
}
}
public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {
}
▶ AuthenticationManager
인증은 Spring Security의 AuthenticationManager를 통하여 처리하는데 사실상 AuthenticationManager에 등록된 AuthenticationProvider 에서 처리한다.
인증이 성공하게 되면 UsernamePasswordAuthenticationToken 두번째 생성자인 인증 후의 생성자가 객체를 생성하여 SecurityContext에 저장되게 된다.
그리고 인증 상태(Authentication)을 유지하기 위해 session에 보관하며, 인증이 실패한 경우에는 AuthenticationException 이 발생된다.
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
▶ AuthenticationProvider
실제 인증 처리가 진행되는 모듈이다.
인증 전의 Authentication 객체를 받아서 인증이 완료된 객체를 반환하는 역할을 한다.
AuthenticationProvider 인터페이스는 개발자가 자체적으로 Custom하여 구현해서 AuthenticationManager에 등록해주면 Custom한 대로 인증 처리를 진행한다.
public interface AuthenticationProvider {
// 인증 전의 Authenticaion 객체를 받아서 인증된 Authentication 객체를 반환
Authentication authenticate(Authentication var1) throws AuthenticationException;
boolean supports(Class<?> var1);
}
AuthenticationManager를 implements 한 Providermanager 는 실제 인증 과정에 대한 로직이 있는 AuthenticationProvider를 List로 가지면서, for문을 통해 모든 Provider를 조회하면서 Authenticate 처리를 진행한다.
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
public List<AuthenticationProvider> getProviders() {
return providers;
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
//for문으로 모든 provider를 순회하여 처리하고 result가 나올 때까지 반복한다.
for (AuthenticationProvider provider : getProviders()) {
....
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
....
}
throw lastException;
}
}
여기서 AuthenticationProvider를 List로 가지는 이유는 인증해야할 부분이 여러가지가 있거나, 인증 처리가 실패했을 경우 그 다음 인증 처리 작업을 통해 완료하기 위해 List로 여러개 가지는 것이다.
앞서 AuthenticationlProvider를 개발자가 직접 Custom 하여 구현 후 등록하려면 WebSecurityConfigurerAdapter를 상속받은 SecurityConfig 에서 할 수 있다.
WebSecurityConfigurerAdapter의 상위 클래스에는 AuthenticationManager 를 가지고 있기 때문에 우리가 직접 만든 AuthenticationProvider를 등록할 수 있다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public AuthenticationManager getAuthenticationManager() throws Exception {
return super.authenticationManagerBean();
}
// 직접 만든 AuthenticationProvider Bean 객체로 생성
@Bean
public CustomAuthenticationProvider customAuthenticationProvider() throws Exception {
return new CustomAuthenticationProvider();
}
// 직접 만든 AuthenticationProvider 등록
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider());
}
}
▶ UserDetails
인증에 성공하면 UserDetails 객체가 생성된다.
UserDetails 객체는 Authentication 객체를 구현한 UsernamePasswordAuthenticaionToken을 생성하기 위해 사용된다.
주로 사용자의 아이디(username(principal)), 비밀번호(password) 같은 정보를 반환하는 get 메소드를 가지고 있다.
본인은 UserDetails를 상속받는 구현체 클래스를 만들어서 처리하였다.
<UserDetails 인터페이스>
public interface UserDetails extends Serializable {
// 가진 권한들
Collection<? extends GrantedAuthority> getAuthorities();
// 비밀번호 / Credential
String getPassword();
// 아이디 / Principal
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
<UserDetails 를 상속받은 UserDetailsImpl 구현 클래스>
@Data
@RequiredArgsConstructor
public class UserDetailsImpl implements UserDetails, OAuth2User { // UserDetails 상속
private final Member member;
private final Map<String, Object> attributes;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(Authority.ROLE_MEMBER.toString());
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(authority);
return authorities;
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return member.getEmail();
}
@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 Map<String, Object> getAttributes(){
return attributes;
}
@Override
public String getName(){
return null;
}
}
▶ UserDetailsService
UserDetailsService 인터페이스는 UserDetails 객체를 반환하는 하나의 메소드를 가지고있다.
UserDetailsService를 상속받은 구현체 클래스를 만들어서 해당 구현체 클래스에 오버라이딩된 메소드 내부에 Repository를 활용해 DB 와 연결하여 사용한다.
<UserDetailsService 인터페이스>
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
<인터페이스를 상속받은 UserDetailsServiceImpl 구현체 클래스>
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService{ // UserDetailsService 상속
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("Can't find " + email));
return new UserDetailsImpl(member);
}
}
▶ GrantedAuthority
GrantedAuthority는 현재 사용자가 가지고 있는 권한이다.
ROLE_ADMIN, ROLE_USER 와 같은 ROLE_* 형식을 사용하며, UserDetailsService 구현 객체를 통해 UserDetails에 접근하여 불러올 수 있다.
접근 권한이 있는지 확인하고 허용 여부를 결정한다.
public interface GrantedAuthority extends Serializable {
String getAuthority();
}
▶ Password Encoding
Password Encoder 기능을 통해 패스워드 암호화를 진행한다.
(나는 Password Encoder 기능은 나중에 다룰 JWT 보안 설정 단계에서 같이 설정해주었다.)
@Bean
public PasswordEncoder passwordEncoder() {
log.info("회원관리 기능 절차(jwt) -> SecurityConfiguration - passwordEncoder 메소드");
return new BCryptPasswordEncoder();
}
일단 Spring Security가 무엇인지 간단히 알아보고, 주요 모듈들에 대해서 임시 코드를 보면서 정리하였다.
내가 만든 프로젝트에 구현된 Spring Security는 jwt의 등장으로 다른 점이 있기 때문에 나중에 jwt를 정리하면서 같이 보면 좋을 것 같다.
# 많은 도움을 받은 블로그
'기술 창고 > Spring' 카테고리의 다른 글
[Spring Boot] xml 설정 파일을 이용한 Bean 관리 및 사용 (0) | 2023.05.31 |
---|---|
[Spring Boot] Spring Boot Session 사용 (Spring Bean Scope) (0) | 2023.03.15 |
[Spring Boot] JWT (Json Web Token) (0) | 2023.01.13 |
[Spring Boot] Dispatcher Servlet (0) | 2023.01.13 |
[Spring Boot] Spring Bean (0) | 2023.01.06 |