JwtAuthenticationFilter 구현 vs SecurityContextRepository 구현(custom)
JwtAuthenticationFilter(간단, 신경 쓸 부분이 줄어듦)
HTTP 헤더에서 JWT 토큰을 추출하고, 검증하여 SecurityContext를 생성할 수 있음
Security Filter 체인 상에서 어디에 위치하는 지가 중요함
SecurityContextPersistenceFilter 바로 뒤에 또는 UsernamePasswordAuthenticationFilter 필터 전후로 위치하면 적당함
SecurityContextRepository 커스텀 구현
기본적으로 JwtAuthenticationFilter 구현과 유사함
그러나 SecurityContextRepository 인터페이스에 맞추어 부수적인 메소드 구현이 필요함
saveContext, containsContext 메소드
SecurityContextPersistenceFilter, SessionManagementFilter 2개의 필터에서 SecurityContextRepository 구현이 어떻게 사용되는지 잘 알고 있어야함
public class SecurityContextPersistenceFilter extends GenericFilterBean
public class SessionManagementFilter extends GenericFilterBean
SecurityContextRepository 인터페이스 커스텀 구현 방식이 추가적으로 고려할 내용이 많고, Spring Security 전반에 걸쳐 끼치는 영향이 더 큼
특히 SessionManagementFilter를 사용할 경우 SecurityContextRepository 메소드 구현 방법에 따라 적절한 설정이 필요함
JwtAuthenticationToken.java
JwtAuthentication.java
userservice.java
jwtauthenticationprovider.java
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// ensure that filter is only applied once per request
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
if (this.forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (this.logger.isDebugEnabled() && session.isNew()) {
this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));
}
}
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
//securitycontext를 가져오는 부분
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
try {
SecurityContextHolder.setContext(contextBeforeChainExecution);
if (contextBeforeChainExecution.getAuthentication() == null) {
logger.debug("Set SecurityContextHolder to empty SecurityContext");
}
else {
if (this.logger.isDebugEnabled()) {
this.logger
.debug(LogMessage.format("Set SecurityContextHolder to %s", contextBeforeChainExecution));
}
}
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
// Crucial removal of SecurityContextHolder contents before anything else.
SecurityContextHolder.clearContext();
//모든 처리가 끝나고 securityContext를 저장하는 부분
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute(FILTER_APPLIED);
this.logger.debug("Cleared SecurityContextHolder to complete request");
}
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
//해당부분에서 containsContext가 false를 리턴하게 되면
if (!this.securityContextRepository.containsContext(request)) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && !this.trustResolver.isAnonymous(authentication)) {
// The user has been authenticated during the current request, so call the
// session strategy
try {
this.sessionAuthenticationStrategy.onAuthentication(authentication, request, response);
}
catch (SessionAuthenticationException ex) {
// The session strategy can reject the authentication
this.logger.debug("SessionAuthenticationStrategy rejected the authentication object", ex);
SecurityContextHolder.clearContext();
this.failureHandler.onAuthenticationFailure(request, response, ex);
return;
}
// Eagerly save the security context to make it available for any possible
// re-entrant requests which may occur before the current request
// completes. SEC-1396.
this.securityContextRepository.saveContext(SecurityContextHolder.getContext(), request, response);
}
else {
// No security context or authentication present. Check for a session
// timeout
if (request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid()) {
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Request requested invalid session id %s",
request.getRequestedSessionId()));
}
if (this.invalidSessionStrategy != null) {
this.invalidSessionStrategy.onInvalidSessionDetected(request, response);
return;
}
}
}
}
chain.doFilter(request, response);
}
/**
* Http 요청 헤더에 jwt 토큰이 잇는 지 확인
* JWT 토큰이 있다면, 주어진 토큰을 디코딩하고,
* payload 부분의 username, roles 데이터를 추출 하고, UsernamePasswordAuthenticationToken 생성
* 그리고 이렇게 만들어진 Token을 SecurityContext에 넣어줌
*/
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private String credentials;
public JwtAuthenticationToken(String principal, String credentials) {
super(null);
super.setAuthenticated(false);
this.principal = principal;
this.credentials = credentials;
}
JwtAuthenticationToken(Object principal, String credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
super.setAuthenticated(true);
this.principal = principal;
this.credentials = credentials;
}
@Override
public Object getPrincipal() {
return principal;
}
@Override
public String getCredentials() {
return credentials;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
credentials = null;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
.append("principal", principal)
.append("credentials", "[PROTECTED]")
.toString();
}
}
public class JwtAuthentication {
// JWTAuthentication 객체는 불면 객체로 만들 것이기 떄문에 필드 접근을 public으로 한다.
public final String token;
public final String username;
JwtAuthentication(String token, String username) {
checkArgument(isNotEmpty(token), "token must be provided.");
checkArgument(isNotEmpty(username), "username must be provided.");
this.token = token;
this.username = username;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
.append("token", token)
.append("username", username)
.toString();
}
}
@Service
public class UserService {
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
public UserService(PasswordEncoder passwordEncoder, UserRepository userRepository) {
this.passwordEncoder = passwordEncoder;
this.userRepository = userRepository;
}
@Transactional(readOnly = true)
public User login(String principal, String credentials) {
checkArgument(isNotEmpty(principal), "principal must be provided.");
checkArgument(isNotEmpty(credentials), "credentials must be provided.");
User user = userRepository.findByLoginId(principal)
.orElseThrow(() -> new UsernameNotFoundException("Could not found user for " + principal));
user.checkPassword(passwordEncoder, credentials);
return user;
}
@Transactional(readOnly = true)
public Optional<User> findByLoginId(String loginId) {
checkArgument(isNotEmpty(loginId), "loginId must be provided.");
return userRepository.findByLoginId(loginId);
}
}