1. 개요
해당 프로젝트에서 회원 가입 시 발생했던 문제를 정리한다. 배포 준비를 위해 회원 entity에서 미사용 필드를 제거하고 Auditing 로직을 넣은 후 다음과 같은 오류가 발생했다.

문자열을 UserPrincipal(implements UserDetail)로 캐스팅하려 했다는 것이다. 이런 코드가 있었나? 바로 디버깅을 돌려봤다.
2. 해결 과정

소셜 로그인 클래스의 140번째 줄에서 예외가 터진 것이고

이는 회원 저장 메서드였다.

Auditor에서 생성자를 찾는 과정에서 authentication.getPrincipal()을 캐스팅하였고, 여기서 오류가 발생했다. 이 프로젝트에서는 JWT를 사용하여 인증 및 인가를 진행한다. 회원 가입시에는 토큰을 발급받지 않은 상태이므로 위 조건문에서 걸렸어야 하는데 이것이 이상하여 확인해본 결과

authentication은 null도 아닌데다 principal에 문자열 "anonymousUser"가 담겨있었다. 어떻게 된 일일까?
3. 분석
스프링 시큐리티는 여러 필터를 거쳐 토큰을 받은 후 이를 AuthenticationManager에 보내 Authentication 객체를 받는다. 그 중 하나를 살펴보자.
public class AnonymousAuthenticationFilter extends GenericFilterBean implements InitializingBean {
private SecurityContextHolderStrategy securityContextHolderStrategy;
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource;
private String key;
private Object principal;
private List<GrantedAuthority> authorities;
public AnonymousAuthenticationFilter(String key) {
this(key, "anonymousUser", AuthorityUtils.createAuthorityList(new String[]{"ROLE_ANONYMOUS"}));
}
public AnonymousAuthenticationFilter(String key, Object principal, List<GrantedAuthority> authorities) {
this.securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
this.authenticationDetailsSource = new WebAuthenticationDetailsSource();
Assert.hasLength(key, "key cannot be null or empty");
Assert.notNull(principal, "Anonymous authentication principal must be set");
Assert.notNull(authorities, "Anonymous authorities must be set");
this.key = key;
this.principal = principal;
this.authorities = authorities;
}
public void afterPropertiesSet() {
Assert.hasLength(this.key, "key must have length");
Assert.notNull(this.principal, "Anonymous authentication principal must be set");
Assert.notNull(this.authorities, "Anonymous authorities must be set");
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
Supplier<SecurityContext> deferredContext = this.securityContextHolderStrategy.getDeferredContext();
this.securityContextHolderStrategy.setDeferredContext(this.defaultWithAnonymous((HttpServletRequest)req, deferredContext));
chain.doFilter(req, res);
}
private Supplier<SecurityContext> defaultWithAnonymous(HttpServletRequest request, Supplier<SecurityContext> currentDeferredContext) {
return SingletonSupplier.of(() -> {
SecurityContext currentContext = (SecurityContext)currentDeferredContext.get();
return this.defaultWithAnonymous(request, currentContext);
});
}
private SecurityContext defaultWithAnonymous(HttpServletRequest request, SecurityContext currentContext) {
Authentication currentAuthentication = currentContext.getAuthentication();
if (currentAuthentication == null) {
Authentication anonymous = this.createAuthentication(request);
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.of(() -> {
return "Set SecurityContextHolder to " + anonymous;
}));
} else {
this.logger.debug("Set SecurityContextHolder to anonymous SecurityContext");
}
SecurityContext anonymousContext = this.securityContextHolderStrategy.createEmptyContext();
anonymousContext.setAuthentication(anonymous);
return anonymousContext;
} else {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.of(() -> {
return "Did not set SecurityContextHolder since already authenticated " + currentAuthentication;
}));
}
return currentContext;
}
}
protected Authentication createAuthentication(HttpServletRequest request) {
AnonymousAuthenticationToken token = new AnonymousAuthenticationToken(this.key, this.principal, this.authorities);
token.setDetails(this.authenticationDetailsSource.buildDetails(request));
return token;
}
public void setAuthenticationDetailsSource(AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
this.authenticationDetailsSource = authenticationDetailsSource;
}
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
Assert.notNull(securityContextHolderStrategy, "securityContextHolderStrategy cannot be null");
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
public Object getPrincipal() {
return this.principal;
}
public List<GrantedAuthority> getAuthorities() {
return this.authorities;
}
}
AnonymousAuthenticationFilter의 코드이다. 인증 시 실 동작은 doFilter에서 진행하므로 이를 살펴보자면
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
Supplier<SecurityContext> deferredContext = this.securityContextHolderStrategy.getDeferredContext();
this.securityContextHolderStrategy.setDeferredContext(this.defaultWithAnonymous((HttpServletRequest)req, deferredContext));
chain.doFilter(req, res);
}
request와 deferredContext를 가지고 defaultWithAnonymous 메서드를 실행하는 것을 볼 수 있다.
private SecurityContext defaultWithAnonymous(HttpServletRequest request, SecurityContext currentContext) {
Authentication currentAuthentication = currentContext.getAuthentication();
if (currentAuthentication == null) {
Authentication anonymous = this.createAuthentication(request);
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.of(() -> {
return "Set SecurityContextHolder to " + anonymous;
}));
} else {
this.logger.debug("Set SecurityContextHolder to anonymous SecurityContext");
}
SecurityContext anonymousContext = this.securityContextHolderStrategy.createEmptyContext();
anonymousContext.setAuthentication(anonymous);
return anonymousContext;
} else {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.of(() -> {
return "Did not set SecurityContextHolder since already authenticated " + currentAuthentication;
}));
}
return currentContext;
}
}
살펴 보면 현재 필터에 오기까지 Authenticaion이 createAuthentication으로 새로 생성하여 SecurityContext에 이를 set한다.
protected Authentication createAuthentication(HttpServletRequest request) {
AnonymousAuthenticationToken token = new AnonymousAuthenticationToken(this.key, this.principal, this.authorities);
token.setDetails(this.authenticationDetailsSource.buildDetails(request));
return token;
}
AnonymousAuthenticationToken을 만들고, 이를 리턴한다.
private AnonymousAuthenticationToken(Integer keyHash, Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
Assert.isTrue(principal != null && !"".equals(principal), "principal cannot be null or empty");
Assert.notEmpty(authorities, "authorities cannot be null or empty");
this.keyHash = keyHash;
this.principal = principal;
this.setAuthenticated(true);
}
토큰 생성자이다. 이 때 authenticated를 true로 set한다.
public AnonymousAuthenticationFilter(String key) {
this(key, "anonymousUser", AuthorityUtils.createAuthorityList(new String[]{"ROLE_ANONYMOUS"}));
}
public AnonymousAuthenticationFilter(String key, Object principal, List<GrantedAuthority> authorities) {
this.securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
this.authenticationDetailsSource = new WebAuthenticationDetailsSource();
Assert.hasLength(key, "key cannot be null or empty");
Assert.notNull(principal, "Anonymous authentication principal must be set");
Assert.notNull(authorities, "Anonymous authorities must be set");
this.key = key;
this.principal = principal;
this.authorities = authorities;
}
지금까지의 모든 과정을 principal에 anonymousUser을 넣고 진행된다.
위 과정을 거쳐 Principal에 anonymousUser라는 문자열이 저장되었고, authenticated는 true이다. 즉, permitAll에서는 익명 사용자가 인가되었다고 설정 후 로직을 실행하게 된다. 이로 인해 캐스팅에 실패하게 된 것이다.
4. 해결

해당 문제를 해결하기 위해 Principal이 UserDetails가 아니라 캐스팅을 할 수 없다면 걸릴 조건을 추가했다.

정상적으로 반환되는 걸 확인할 수 있었다.
'Proj > Tripot' 카테고리의 다른 글
| 앱 버전 저장 및 관리하기 (0) | 2025.03.02 |
|---|---|
| 소셜 로그인 - 애플 identity_token에서 사용자 정보 가져오기 (0) | 2025.02.14 |
| Trouble Shooting - docker compose vs docker-compose (0) | 2025.02.10 |
| 스프링부트 테스트코드 작성하기: 통합 테스트 (1) | 2025.01.10 |
| 신고 기능 구현 (0) | 2025.01.04 |