[Spring Security6] Authorization과 Filter
Authentication 은 사용자가 누구인지 확인하는 과정이다.
로그인 과정에서 아이디와 비밀번호를 통해 사용자를 확인하는 작업은 Authentication 이다.
Authorization 은 사용자의 권한을 의미한다.
인증된 사용자가 어떤 리소스에 접근할 수 있는지 결정한다.
Roles 는 권한의 집합을 의미한다.
관리자와 사용자가 Roles라면 사용자 관리, 글 열람은 Authorization 이다.
시큐리티는 GrantedAuthroity 인터페이스를 사용해 사용자에게 부여된 권한을 나타낸다.
public final class SimpleGrantedAuthority implements GrantedAuthority {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final String role;
public SimpleGrantedAuthority(String role) {
Assert.hasText(role, "A granted authority textual representation is required");
this.role = role;
}
@Override
public String getAuthority() {
return this.role;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof SimpleGrantedAuthority) {
return this.role.equals(((SimpleGrantedAuthority) obj).role);
}
return false;
}
@Override
public int hashCode() {
return this.role.hashCode();
}
@Override
public String toString() {
return this.role;
}
}
스프링이 항상 그렇듯 인터페이스와 그 구현체를 함께 제공한다.
권한과 역할을 표현하는 가장 기본적인 구현을 보여준다.
기본적인 경우나 단순한 설정에서는 그대로 사용해도 좋지만 요구사항에 따라서 다른 구현체를 사용하자.
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String pwd = authentication.getCredentials().toString();
List<Customer> customer = customerRepository.findByEmail(username);
if (customer.size() > 0) {
if (passwordEncoder.matches(pwd, customer.get(0).getPwd())) {
return new UsernamePasswordAuthenticationToken(username, pwd, getGrantedAuthorities(customer.get(0).getAuthorities()));
} else {
throw new BadCredentialsException("Invalid password!");
}
}else {
throw new BadCredentialsException("No user registered.");
}
}
private List<GrantedAuthority> getGrantedAuthorities(Set<Authority> authorities) {
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
for (Authority authority : authorities) {
grantedAuthorities.add(new SimpleGrantedAuthority(authority.getName()));
}
return grantedAuthorities;
}
AuthenticationProvider의 authenticate 메서드를 오버라이드 할 때 권한도 함께 반환하도록 설정하자.
.authorizeHttpRequests((requests)->requests
.requestMatchers("/myAccount").hasAuthority("VIEWACCOUNT")
.requestMatchers("/myBalance").hasAnyAuthority("VIEWACCOUNT","VIEWBALANCE")
.requestMatchers("/myLoans").hasAuthority("VIEWLOANS")
.requestMatchers("/myCards").hasAuthority("VIEWCARDS")
.requestMatchers("/user").authenticated()
.requestMatchers("/notices","/contact","/register").permitAll())
시큐리티 설정 클래스에서 URL마다 필요한 권한을 명시해 줄 수 있다.
hasAuthority : 특정 권한을 가진 사용자만 요청을 허용한다.
hasAnyAuthority : 권한을 리스트로 주고 주어진 권한 중 하나라도 가진 사용자에게 요청을 허용한다.
access : 접근을 좀 더 세밀하게 설정할 수 있다. 주어진 표현식이 참인 경우 요청을 허용한다.
이처럼 권한 단위로 사용자가 수행할 수 있는 작업을 제어할 수 있지만 권한이 너무 많은 경우 더 큰 범주인 ROLE을 사용한다.
.authorizeHttpRequests((requests)->requests
.requestMatchers("/myAccount").hasRole("USER")
.requestMatchers("/myBalance").hasAnyRole("USER","ADMIN")
.requestMatchers("/myLoans").hasRole("USER")
.requestMatchers("/myCards").hasRole("USER")
.requestMatchers("/user").authenticated()
.requestMatchers("/notices","/contact","/register").permitAll())
데이터베이스에 저장할 때는 ROLE_USER, ROLE_ADMIN 처럼 접두사를 붙인다. (권한과 역할을 구분하기 위한 규칙이다)
시큐리티 설정에서는 접두사를 명시적으로 사용하지 않아도 된다.
ROLES에 대해서도 Authority와 비슷하게 hasRole, hasAnyRole, access 메서드를 제공한다.
저렇게 authenticate 메서드를 적당히 오버라이드해서 특별한 인증 매커니즘을 처리할 수 있지만, 직접 만든 필터를 시큐리티 필터에 추가하는 방법으로도 인증 매커니즘을 처리할 수 있다.
필터를 추가하면 인증 매커니즘 처리 외에도 로깅 작업, 추가 보안 헤더 적용, 요청 내용 수정 등 추가 작업을 수행할 수 있다.
public interface Filter {
default void init(FilterConfig filterConfig) throws ServletException {
}
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException;
default void destroy() {
}
}
필터를 작성할 때는 jakarta.servlet이 제공하는 Filter 인터페이스를 구현해야 한다.
init : 필터가 처음 생성될 때 한 번만 호출되고, 초기화 작업을 수행한다.
doFilter : 클라이언트의 모든 요청에 대해 메서드가 호출된다. 실제 로직을 구현하는 부분이다.
요청, 응답, 다음에 수행될 필터에 접근하는 객체를 인자로 받아 비즈니스 로직을 처리한다.
destroy : 서블릿 컨테이너에서 필터가 제거될 때 한 번만 호출된다. 마무리 작업을 수행한다.
@Slf4j
public class AuthoritiesLoggingAtFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
log.info("Authentication ...");
chain.doFilter(request, response);
}
}
Filter 인터페이스를 구현해 doFilter 메서드를 작성해 애플리케이션의 요구사항에 맞는 필터를 구현하자.
위의 예시는 클라이언트의 요청이 있을 때 마다 로그를 출력한다.
로그 출력 말고도 다양한 비즈니스 로직을 구현할 수 있다.
@Configuration
public class ProjectSecurityConfig {
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
requestHandler.setCsrfRequestAttributeName("_csrf");
http.securityContext((context) -> context.requireExplicitSave(false))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
.cors(corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() {
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("http://localhost:4200"));
config.setAllowedMethods(Collections.singletonList("*"));
config.setAllowCredentials(true);
config.setAllowedHeaders(Collections.singletonList("*"));
config.setMaxAge(3600L);
return config;
}
})).csrf((csrf) -> csrf.csrfTokenRequestHandler(requestHandler).ignoringRequestMatchers("/contact","/register")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class)
.addFilterBefore(new RequestValidationBeforeFilter(), BasicAuthenticationFilter.class)
.addFilterAt(new AuthoritiesLoggingAtFilter(), BasicAuthenticationFilter.class)
.addFilterAfter(new AuthoritiesLoggingAfterFilter(), BasicAuthenticationFilter.class)
.authorizeHttpRequests((requests)->requests
.requestMatchers("/myAccount").hasRole("USER")
.requestMatchers("/myBalance").hasAnyRole("USER","ADMIN")
.requestMatchers("/myLoans").hasRole("USER")
.requestMatchers("/myCards").hasRole("USER")
.requestMatchers("/user").authenticated()
.requestMatchers("/notices","/contact","/register").permitAll())
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
추후 자세히 다루겠지만 위의 예시에서는 sessionCreationPolicy() 메서드로 클라이언트에게 JSESSIONID 쿠키를 전송하고 있다.
필터를 구현했으면 시큐리티 설정 클래스에서 구현한 필터를 시큐리티 필터에 추가해 줘야 한다.
addFilterAfter : 구현한 필터와 기존 필터를 매개변수로 받아서 구현한 필터를 기존 필터 후에 추가한다.
addFilterBefore : 똑같다. 기존 필터 전에 추가한다.
addFilterAt : 지정한 필터를 특정 필터 위치에 추가한다. 위치에 이미 필터가 있는 경우 실행 순서가 보장되지 않는다.
위처럼 필터를 직접 작성할 경우 Filter 인터페이스를 구현하는데, 스프링 시큐리티는 좀 더 편하게 작성할 수 있도록 몇 가지 추상 클래스와 인터페이스를 제공한다.
GenericFilterBean
스프링에서 제공하는 추상 클래스로 필터를 빈으로 관리할 때 사용된다.
스프링 빈의 생명주기나 환경 변수를 사용하는 커스텀 필터를 작성할 때 사용한다.
OncePerRequestFilter
스프링에서 제공하는 추상 클래스로 한 번의 요청에서 doFilterInternal 메서드가 한 번 만 실행되도록 보장한다.
한 번의 요청에 필터 로직이 중복으로 실행됨을 방지할 때 사용한다.
필터를 구현할 때 doFilter 메서드를 오버라이드하는데, OncePerRequestFilter에서 실제 필터 로직을 구현할 때 사용되는 메서드는 doFilterInternal 이다.
서블릿 컨테이너의 내부 구현에서 하나의 요청 처리에서 필터가 여러 번 호출될 가능성이 있는데, OncePerRequestFilter를 사용하면 로직이 중복으로 실행되지 않음을 보장할 수 있다.
'Spring > Spring Security' 카테고리의 다른 글
[Spring Security 6] 메서드 단위 권한 요청 (0) | 2023.08.12 |
---|---|
[Spring Security6] Json Web Token (0) | 2023.08.11 |
[Spring Security6] CORS, CSRF (0) | 2023.08.08 |
[Spring Security6] 사용자 인증 (0) | 2023.08.06 |
[Spring Security6] Password Encoder (0) | 2023.08.05 |
댓글
이 글 공유하기
다른 글
-
[Spring Security 6] 메서드 단위 권한 요청
[Spring Security 6] 메서드 단위 권한 요청
2023.08.12 -
[Spring Security6] Json Web Token
[Spring Security6] Json Web Token
2023.08.11 -
[Spring Security6] CORS, CSRF
[Spring Security6] CORS, CSRF
2023.08.08 -
[Spring Security6] 사용자 인증
[Spring Security6] 사용자 인증
2023.08.06