Security로 설정을 구성하다 보면 항상 자연스럽게 Authentication 객체에 Principal은 당연히 UserDetails였다. 물론 지원해주는 거니 문제 없게 사용하지만 한 번 바꿔보고 싶은 마음에 인증 절차 Provider랑 Token이랑 바꿔봤다.
애초에 인증 절차는 http.formLogin()을 활성화할 경우 UsernamePasswordAuthenticationFilter가 작동한다. 그리고 SSR 방식에서 세션을 사용하는 경우라면 거의 웬만하면 해당 FIlter로 처리하는게 적당하다고 생각하기 때문에 해당 필터를 사용한 인증방식을 그대로 사용한다.
간단한 인증 절차는 이렇다.application/x-www-form-urlencoded
1. 사용자가 application/x-www-form-urlencoded 타입으로 데이터를 날린다. (username, password)
2. UsernamePasswrodAuthenticationFilter에 지정된 defaltUrl과 메소드 타입은 POST (물론 바꿀 수 있지만 그대로 해줌)
3.attemptAuthentication()메소드를 통해 username, password로 Authentication을 구현한 Token을 만들어 AuthenticationManager에게 넘긴다.
4. AuthenticationManager의 구현체인 ProviderManager가 가지고 있는 메소드
에서
토큰을 처리할 수 있는 Provider를 모두 조회하는데
기본적으로는 두 개가 존재한다 DaoAuthenticationProvider는 UsernamePasswordAuthenticationToken 타입을 처리하는 Provider이고 AnonymousAuthenticationProvider는
AnonymousAuthenticationToken을 처리하는데 로그인을 안 한 사용자이다. 로그인을 안 한다 해도 SecurityContext에는 해당 토큰이 담긴다.
결국 해당 Provider를 찾아 인증을 위임하고 해당 Provider는 인증절차를 진행하고 Authentication에 authenticated값을 true로 설정하여 지금까지 온 길 다시 돌아가면서 마지막 Filter에서 SecurityContextHolder에 Authentication을 담고 인증 절차가 마무리된다. (이 사이에도 많은 과정이 있지만 생략)
이 과정을 통해 알 수 있는 것은 Provider와 Token만 만든다면 Authentication에 Principal값은 Userdetails가 아닌 값을 만들 수 있다는 것이다.
코드구현
UsernamePasswordAuthenticationFilter 방식을 그대로 사용한다.
1. AuthenticationToken
여기서 Token은 Authentication을 구현해야한다. 그러면서 자동으로 principal, credentials, details, authenticated 값들의 getter, setter들을 오버라이딩 해야한다. 여기서는 롬복을 통해 해당 메소드를 만들었다. 그리고 여기서 봐야할 건 principal이다. 나는 여기에 Userdetails가 아닌 내가 만든 CustomUser를 넣으려고 이렇게 구성했다. CustomUser는 아무것도 상속받지 않으며 아무것도 구현하지 않는다. 나머지 생성자랑 unauthenticated는 UsernamPasswordAuthenticationToken을 참고했다.
위에 두 메소드가 UsernamePasswordAuthentiocationToken의 메소드와 생성자다. unauthenticated는 UsernamePasswordAuthenticationFilter에서 그대로 사용된다.
아래는 UsernamePasswordAuthenticationFilter를 대신할 CustomFilter이다.
package com.example.back.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class CustomFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
public CustomFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
super(defaultFilterProcessesUrl);
setAuthenticationManager(authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if ( !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
System.out.println("FILTER 요청처리 ");
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
CustomToken authRequest = CustomToken.unauthenticated(username,password);
log.debug("FILTER에 들어온 인증 토큰 ->> {}",authRequest);
// Allow subclasses to set the "details" property
return this.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.SPRING_SECURITY_FORM_USERNAME_KEY);
}
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.SPRING_SECURITY_FORM_PASSWORD_KEY);
}
}
해당 코드와 UsernamePasswordAuthenticationFilter는 거의 비슷하다.
여기서 상속받는 AbstractAuthenticationProcessingFilter는 인증을 처리하는 필터 중 하나이고, 해당 필터는 사용자가 인증을 시도할 때 호출되며, 사용자가 제공한 인증 정보를 검증하고 인증에 성공하면 인증된 사용자에게 인가 정보를 부여한다.
해당 Filter는 attemptAuthentication() 메서드를 통해 사용자가 제공한 인증 정보를 검증하고 인증 객체를 반환한다. 해당 인증 객체 즉 Authentication 객체는 Authenticationmanager를 통해 검증되고 인증이 성공하면 인가 정보를 생성하여 사용자에게 반환한다. UsernamePasswordAuthenticationFIlter도 해당 fIlter를 상속받는다.
아래 메소드는 UsernamePasswardAuthenticationFIlter 가 오버라이딩한 메소드이다. 해당 메소드는 POST로 요청이 /login으로 들어오면 getParameter로 Request 객체에서 username,password를 뽑아서 해당 값들로 Authentication을 만들어 넘긴다.
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
근데 여기서 문제점이 있었는데 바로 AuthenticationManager였다. AbstractAuthenticationProcessingFilter의 getAuthenticationManager를 가져와서 쓰려했는데 null이어서 사용을 못했다.
그렇기 때문에 SecurityConfig(설정클래스) 에서 AuthenticationManager를 주입받아 사용했다.
설정클래스에서는 AuthenticationManager를 가져오기 위해
AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
를 사용한다 getSharedObject는 객체에서 특정한 객체를 가져오는 메소드이다. AuthenticationManagerBuilder를 가져온다.
HttpSecurity 안에
이렇게 사용하는 메소드가 있지만 접근제어자가 private이라 사용하지 못한다. AuthenticationManager에 각종 설정을 한 뒤에 다시 해당 AuthenticationManager를 등록해주어야 한다.
이렇게 해서 Filter를 추가하면서 해당 AuthenticationManager를 Filter에 넘겨준다.
http.addFilterAt(new CustomFilter("/login",authenticationManager), UsernamePasswordAuthenticationFilter.class);
이제 Provider이다. 일단 먼저 AuthenticationManager 에 Provider 를 설정해줘야한다.
AuthenticationManager authenticationManager = authenticationManagerBuilder.build();
authenticationManagerBuilder.authenticationProvider(new CustomProvider());
Provider는 기본적으로 AuthenticationProvider를 구현하고 authenticate() 메소드와 supports() 메소드를 가지는데 authenticate()는 원래는 인증을 진행하고, support는 인증을 처리 할 token타입을 명시한다.
기존 DaoAuthenticationProvider였다면 UserDetailsService에 loadByusername()을 통해 비밀번호랑 제대로 비교를 했겠지만 여기서는 DB보다 SecurityContext에 들어가는 타입을 바꿔보는 거기에 그렇게 까지 안 했다. 그냥 바로 user랑 1234면 인증 되도록 구현했다.
package com.example.back.security;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
public class CustomProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
CustomToken customToken = (CustomToken)authentication;
if(customToken.getPrincipal().getUsername().equals("user") && customToken.getPrincipal().getPassword().equals("1234")){
System.out.println("Provider에 들어온 토큰 "+customToken);
return CustomToken.builder()
.authenticated(true)
.credentials(null)
.principal(CustomUser.builder()
.username(customToken.getPrincipal().getUsername())
.password(customToken.getPrincipal().getPassword())
.authority(customToken.getPrincipal().getAuthority())
.build())
.build();
}
return null;
}
@Override
public boolean supports(Class<?> authentication) {
return authentication == CustomToken.class;
}
}
결과확인
폼로그인 방식으로 보낸다.
여기에 접근하여 현재 SecurityContext 안에 그리고 Authentication안에 있는 CustomUser를 꺼내온다.
글이 약간 횡설수설하지만 결국 나는 코드를 짜다가 @AuthenticationPrincipal에 주입해주는 값은 UserDetails를 구현을 꼭 해야하는지 의문이 들어 한 번 코드로 테스트 해본 걸 기록으로 남긴 것이다.
'Spring Security' 카테고리의 다른 글
Spring Security - JWT 설정 (0) | 2023.03.03 |
---|---|
SpringSecurity - UsernamePasswordAuthenticationFilter (0) | 2023.02.15 |
Spring Security - 2.7 이후 SecurityFilterChain (0) | 2023.01.02 |