쌩으로 하는게 아니라 해당 추상클래스 사용

- 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
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"); } }
public class SessionManagementFilter extends GenericFilterBean
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); }
- SecurityContextRepository 인터페이스 커스텀 구현 방식이 추가적으로 고려할 내용이 많고, Spring Security 전반에 걸쳐 끼치는 영향이 더 큼
- 특히 SessionManagementFilter를 사용할 경우 SecurityContextRepository 메소드 구현 방법에 따라 적절한 설정이 필요함
/** * Http 요청 헤더에 jwt 토큰이 잇는 지 확인 * JWT 토큰이 있다면, 주어진 토큰을 디코딩하고, * payload 부분의 username, roles 데이터를 추출 하고, UsernamePasswordAuthenticationToken 생성 * 그리고 이렇게 만들어진 Token을 SecurityContext에 넣어줌 */
public class JwtAuthenticationFilter extends GenericFilterBean { private final Logger log = LoggerFactory.getLogger(getClass()); private final String headerKey; private final Jwt jwt; public JwtAuthenticationFilter(String headerKey, Jwt jwt) { this.headerKey = headerKey; this.jwt = jwt; } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req;//request HttpServletResponse response = (HttpServletResponse)res; if (SecurityContextHolder.getContext().getAuthentication() == null) { String token = getToken(request); if (token != null) { try { Jwt.Claims claims = verify(token);//JwtVerificationException 가능성이 있음 log.debug("Jwt parse result: {}", claims); String username = claims.username; List<GrantedAuthority> authorities = getAuthorities(claims); if (isNotEmpty(username) && authorities.size() > 0) { JwtAuthenticationToken authentication = new JwtAuthenticationToken(new JwtAuthentication(token, username), null, authorities); authentication.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { log.warn("Jwt processing failed: {}", e.getMessage()); } } } else { //SecurityContext에 무엇인가 있다는것은 인증이 이미 되었다는것! log.debug("SecurityContextHolder not populated with security token, as it already contained: '{}'", SecurityContextHolder.getContext().getAuthentication()); } chain.doFilter(request, response);//다음 필터체인 } private String getToken(HttpServletRequest request) { String token = request.getHeader(headerKey); if (isNotEmpty(token)) { log.debug("Jwt authorization api detected: {}", token); try { return URLDecoder.decode(token, "UTF-8"); } catch (UnsupportedEncodingException e) { log.error(e.getMessage(), e); } } return null; } private Jwt.Claims verify(String token) { return jwt.verify(token); } private List<GrantedAuthority> getAuthorities(Jwt.Claims claims) { String[] roles = claims.roles; return roles == null || roles.length == 0 ? emptyList() : Arrays.stream(roles).map(SimpleGrantedAuthority::new).collect(toList()); } } @Override protected void configure(HttpSecurity http) throws Exception { http. . . . .addFilterAfter(JwtAuthenticationFilter(), SecurityContextPersistenceFilter.class) ; }
JwtAuthenticationToken.java
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(); } }
JwtAuthentication.java
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(); } }
userservice.java
@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); } }
jwtauthenticationprovider.java