Security 6.x
버전을 기점으로 SecurityContextHolder
를 사용했을 때 동작이 달라졌습니다.
특히, SecurityContextHolder.getContext().setAuthentication(..)
를 사용하고 그 인증 객체
를 획득하려고 할 때 해당 인증 객체
를 저장하는 시점 외에 다른 요청이 발생했을 경우 인증 객체
가 유실되는 상황이 발생하는데요.
만약 JWT
를 사용하고 Presentation Layer
에서 로그인을 직접 구현한 방법으로 동작을 만든 상황이라면 더욱 난감할 것입니다.
해결 방법은 현재 Deprecated
된 SecurityContextPersistenceFilter
와 6.x 버전부터 사용중인 SecurityContextHolderFilter
클래스를 보면 이해가 빨리 됩니다.
SecurityContextPersistenceFilter
@Deprecated
public class SecurityContextPersistenceFilter extends GenericFilterBean {
static final String FILTER_APPLIED = "__spring_security_scpf_applied";
private SecurityContextRepository repo;
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
private boolean forceEagerSessionCreation = false;
public SecurityContextPersistenceFilter() {
this(new HttpSessionSecurityContextRepository());
}
public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
this.repo = repo;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
}
SecurityContextHolderFilter
public class SecurityContextHolderFilter extends GenericFilterBean {
private static final String FILTER_APPLIED = SecurityContextHolderFilter.class.getName() + ".APPLIED";
private final SecurityContextRepository securityContextRepository;
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
.getContextHolderStrategy();
/**
* Creates a new instance.
* @param securityContextRepository the repository to use. Cannot be null.
*/
public SecurityContextHolderFilter(SecurityContextRepository securityContextRepository) {
Assert.notNull(securityContextRepository, "securityContextRepository cannot be null");
this.securityContextRepository = securityContextRepository;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
}
가장 중요한 포인트는 SecurityContextRepository
입니다.
SecurityContextRepository
인터페이스의 구현체를 SecurityContextPersistenceFilter
에서는 생성자를 통해서 HttpSessionSecurityContextRepository
클래스를 선언하여 쓰고 있지만,
SecurityContextHolderFilter
는 사용하고 있지 않습니다.
HttpSessionSecurityContextRepository
는 클래스 이름에서 유추해볼 수 있듯 HttpSession
을 생성하는 로직을 담은 클래스입니다.
이 클래스를 Security
에 적용시켜서 기존 Security
가 동작했다고 볼 수 있습니다.
제가 REST API
형식으로 Security Filter
를 거쳐서 동작하도록 구현한 코드입니다.
JSON
형식으로 요청을 하면 로그인이 동작하게 됩니다.
여기서 로그인이 성공하게 되면 아래의 코드와 같이 SecurityContextHolder
에 인증객체가 들어가는 것을 확인이 가능하게 됩니다.
public class LoginAuthenticationFilterV2 extends AbstractAuthenticationProcessingFilter {
public LoginAuthenticationFilterV2(
final String defaultFilterProcessesUrl,
final AuthenticationManager authenticationManager,
final AuthenticationSuccessHandler authenticationSuccessHandler
) {
super(defaultFilterProcessesUrl, authenticationManager);
setAuthenticationSuccessHandler(authenticationSuccessHandler);
}
@Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response
) throws AuthenticationException, IOException {
String method = request.getMethod();
if (!method.equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
ServletInputStream inputStream = request.getInputStream();
LoginRequestDtoV2 loginRequestDtoV2 = new ObjectMapper()
.readValue(inputStream, LoginRequestDtoV2.class);
return this.getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
loginRequestDtoV2.username(),
loginRequestDtoV2.password()
)
);
}
}
@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication
) throws IOException, ServletException {
// SampleController의 로그인 구현 로직과 비슷하게 들어간다.
System.out.println("로그인 성공");
AuthenticationDetail principal = (AuthenticationDetail) authentication.getPrincipal();
Token jwtToken = jwtTokenProvider.createJwtToken(principal);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().println(this.objectMapper.writeValueAsString(jwtToken));
System.out.println("SecurityContextHolder.getContext().getAuthentication() = " + SecurityContextHolder.getContext().getAuthentication());
}
@GetMapping("/")
public String index(HttpServletRequest request) {
System.out.println("SecurityContextHolder.getContext().getAuthentication() = " + SecurityContextHolder.getContext().getAuthentication());
System.out.println("ss : " +request.getSession().getAttribute("SPRING_SECURITY_CONTEXT"));
return "index";
}
로그인 성공
Hibernate:
select
u1_0.user_id,
u1_0.password,
u1_0.username
from
user_t u1_0
where
u1_0.username=?
SecurityContextHolder.getContext().getAuthentication() =
UsernamePasswordAuthenticationToken [Principal=io.keede.bootlateststarter.domains.user.dto.AuthenticationDetail [Username=tester, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_MEMBER]], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_MEMBER]]
로그인을 성공하면 위와 같이 성공 핸들러
로직이 동작하게 됩니다,
하지만 문제는 다른 요청을 호출하면 아래와 같이 확인이 됩니다.
SecurityContextHolder.getContext().getAuthentication() =
AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[ROLE_ANONYMOUS]]
ss : null
SecurityContextHolder
의 인증 객체가 저장되지도 않았고, HttpSession
에도 저장되지 않은 결과가 나오게 됩니다.
공식문서에도 해결방법이 나와 있긴 하지만, 결국 저희가 신경써서 설정해야 하는 부분은 SecurityContextRepository
구현체를 등록해줘야 한다는 점이 됩니다.
그래야 인증객체도 정상적으로 유지가 되면서, HttpSession
을 이용할 수 있게 됩니다.
public LoginAuthenticationFilterV2(
final String defaultFilterProcessesUrl,
final AuthenticationManager authenticationManager,
final AuthenticationSuccessHandler authenticationSuccessHandler
) {
super(defaultFilterProcessesUrl, authenticationManager);
setAuthenticationSuccessHandler(authenticationSuccessHandler);
// 추가된 코드
// 로그인 이후 Context 생성 전략 설정
setSecurityContextRepository(
new DelegatingSecurityContextRepository(
new HttpSessionSecurityContextRepository(),
new RequestAttributeSecurityContextRepository()
)
);
}
이제 동작을 해보면,,?
로그인 성공
Hibernate:
select
u1_0.user_id,
u1_0.password,
u1_0.username
from
user_t u1_0
where
u1_0.username=?
SecurityContextHolder.getContext().getAuthentication() =
UsernamePasswordAuthenticationToken [Principal=io.keede.bootlateststarter.domains.user.dto.AuthenticationDetail [Username=tester, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_MEMBER]], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_MEMBER]]
SecurityContextHolder.getContext().getAuthentication() = UsernamePasswordAuthenticationToken [Principal=io.keede.bootlateststarter.domains.user.dto.AuthenticationDetail [Username=tester, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_MEMBER]], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_MEMBER]]
ss : SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=io.keede.bootlateststarter.domains.user.dto.AuthenticationDetail [Username=tester, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_MEMBER]], Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_MEMBER]]]
이처럼 Security 6.x
버전 이상부터는 Security
의 기본 로그인을 사용하지 않고 구현하여 사용할 경우나 커스텀한 로그인 로직에 SecurityContextHolder
에 인증 객체를 직접 넣어 쓸 경우에는 SecurityContextRepository
를 설정해줘야 하게 됐습니다.
만약 제 방식 처럼 Security
의 로그인 동작 필터를 구현하여 쓰지않고 Presentation Layer
에서 로그인 API를 사용하여 쓸 경우에는 SecurityContext
, SecurityContextRepository
, SecurityContextHolderFilter
를 직접 Bean 등록을 하고 Filter
등록을 해야 했습니다.
이후 아래와 같이 기존에 Security
에서 동작했던 Context를 저장하는 작업을 추가하여 사용했습니다.
this.securityContext.setAuthentication(loginRequestDtoV2.toAuthenticationToken());
SecurityContextHolder.setContext(this.securityContext);
this.securityContextRepository.saveContext(this.securityContext, request, response);
위 코드는
Presentation Layer
에서JWT
로그인으로 구현할 떄 쓰던 예시 코드입니다.