πŸš€ Spring Security μ˜μ‘΄μ„± νƒˆμΆœκΈ°

유건우·2025λ…„ 1μ›” 7일

ν”„λ‘œμ νŠΈ

λͺ©λ‘ 보기
8/9
post-thumbnail

β“κ°œμš”

Spring Security λ₯Ό μ‚¬μš©ν•˜μ—¬ JWT 토큰 λ°©μ‹μ˜ 인증/인가λ₯Ό κ΅¬ν˜„ν•˜κ²Œλ˜λ©΄ μ‚¬μš©ν•˜κ²Œ λ˜λŠ” UserDetatilsService, UserDetails κ°€ μžˆμŠ΅λ‹ˆλ‹€. 이 λ‘˜μ„ μ‚¬μš©ν•΄ JWT 토큰 λ°©μ‹μœΌλ‘œ κ΅¬ν˜„ν•˜κ²Œ 되면 Spring Security κ°€ λ¬΄μŠ¨μΌμ„ ν•˜κ³  μ–΄λ– ν•œ λ¬Έμ œκ°€ λ°œμƒν•˜λŠ”μ§€ μ•Œμ•„λ³΄λ„λ‘ ν•˜κ² μŠ΅λ‹ˆλ‹€





πŸ“Ž 각자의 μ±…μž„

UserDetails

package org.springframework.security.core.userdetails;

import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities(); // κΆŒν•œ 

    String getPassword(); // λΉ„λ°€λ²ˆν˜Έ 

    String getUsername(); // 식별 정보 

    default boolean isAccountNonExpired() { // 계정 만료 μ—¬λΆ€ 
        return true;
    }

    default boolean isAccountNonLocked() { // 계정 잠금 μ—¬λΆ€ 
        return true;
    }

    default boolean isCredentialsNonExpired() { // λΉ„λ°€λ²ˆν˜Έ 만료 μ—¬λΆ€ 
        return true;
    }

    default boolean isEnabled() { // 계정 ν™œμ„±ν™” μ—¬λΆ€ 
        return true;
    }
}
  • UserDeatils 의 역할은 Spring Security μ—μ„œ μ‚¬μš©μžμ˜ 정보λ₯Ό λ‚˜νƒ€λ‚΄λŠ”λ°μ— μ‚¬μš©λ©λ‹ˆλ‹€.
  • μ‚¬μš©μžμ˜ 인증 μƒνƒœμ™€ κΆŒν•œμ„ μ’…ν•©μ μœΌλ‘œ ν™•μΈν•˜κ³  관리할 수 μžˆλ„λ‘ ν•˜λŠ” 역할을 κ°€μ§€κ³  μžˆμŠ΅λ‹ˆλ‹€.

UserDeatilsService

package org.springframework.security.core.userdetails;

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
  • Spring Security μ—μ„œ μ‚¬μš©μž 정보λ₯Ό λ‘œλ“œν•˜λŠ” μΈν„°νŽ˜μ΄μŠ€μž…λ‹ˆλ‹€.
  • μ‹€μ œ μ‚¬μš©μž 정보가 μ €μž₯된 κ³³(DB, μ™ΈλΆ€ μ‹œμŠ€ν…œ λ“±)μ—μ„œ μ‚¬μš©μž 정보λ₯Ό 가져와 UserDetails 객체둜 λ³€ν™˜ν•˜μ—¬ λ°˜ν™˜ν•˜λŠ” 역할을 κ°€μ§€κ³  μžˆμŠ΅λ‹ˆλ‹€.



ν•„ν„°λ₯Ό μ œμ™Έν•˜κ³  μœ„μ— λ‘˜μ„ κ΅¬ν˜„ν•˜κ²Œ 되면 JWT 토큰 인증/인가 방식을 μ‚¬μš©ν•  수 μžˆκ²Œλ©λ‹ˆλ‹€. Spring Security κ°€ λ§Žμ€ μ±…μž„μ„ κ°€μ§€κ³  인증/인가λ₯Ό λŒ€μ‹  μˆ˜ν–‰ν•˜κ²Œλ©λ‹ˆλ‹€. ν•˜μ§€λ§Œ 이 λ‘˜μ„ μ΄μš©ν•˜κ²Œ 되면 μƒκΈ°λŠ” 문제점이 μžˆμŠ΅λ‹ˆλ‹€. 이 뢀뢄을 닀루기 전에 λ’€μ—μ„œ μ„€λͺ…ν•˜κ²Œ 될 AuthenticationProvider μ±…μž„μ— λŒ€ν•΄ 닀루고 λ„˜μ–΄κ°€λ„λ‘ ν•˜κ² μŠ΅λ‹ˆλ‹€.


AuthencationProvider

package org.springframework.security.authentication;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;

    boolean supports(Class<?> authentication);
}
  • authenticate()
    • μ‹€μ œ 인증을 μ²˜λ¦¬ν•˜λŠ” λ©”μ„œλ“œμž…λ‹ˆλ‹€.
    • UserDetailsServiceλ₯Ό μ΄μš©ν•˜μ—¬ 인증 λ‘œμ§μ— λŒ€ν•œ λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ„ κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • supports()
    • ν•΄λ‹Ή Providerκ°€ νŠΉμ • Authentication νƒ€μž…μ„ μ§€μ›ν•˜λŠ”μ§€ μ—¬λΆ€λ₯Ό ν™•μΈν•˜κ²Œ λ©λ‹ˆλ‹€.
    • λΉ„μ¦ˆλ‹ˆμŠ€λ‘œμ§μ— 따라 인증 λ‘œμ§μ„ λ‹¬λ¦¬ν•˜κ³  μ‹Άμ„λ•Œ supports λ₯Ό 톡해 ν•΄λ‹Ή AuthenticationProvider κ°€ 인증을 μˆ˜ν–‰ν•΄μ•Όν•˜λŠ”μ§€ μ—¬λΆ€λ₯Ό νŒλ‹¨ν•˜κ²Œ λ©λ‹ˆλ‹€.



Spring Security μ—μ„œμ˜ 각자 μ±…μž„μ„ μ‚΄νŽ΄λ³΄μ•˜μŠ΅λ‹ˆλ‹€. Spring Securityλ₯Ό μ΄μš©ν•˜κ²Œ 되면 인증/인가λ₯Ό μˆ˜ν–‰ν•˜κΈ° νŽΈλ¦¬ν•˜κ³  객체지ν–₯적으둜 ꡬ쑰λ₯Ό 섀계할 수 μžˆκ²Œλ©λ‹ˆλ‹€. λ°˜λŒ€λ‘œ 단점도 μ‘΄μž¬ν•©λ‹ˆλ‹€. Spring Security κ°€ 이미 λ§Žμ€κ²ƒμ„ ν•΄μ£Όκ³  μžˆκΈ°λ•Œλ¬Έμ— 디버깅이 νž˜λ“€λ‹€λŠ” 점이 μƒκΈ°κ²Œ λ©λ‹ˆλ‹€. λ˜ν•œ μ—¬λŸ¬κ°€μ§€ 문제점이 μƒκΈ°κ²Œ λ˜λŠ”λ° 이 λΆ€λΆ„μ˜ λŒ€ν•΄ ν•œλ²ˆ μ•Œμ•„λ³΄λ„λ‘ ν•˜κ² μŠ΅λ‹ˆλ‹€.





πŸ‘• λ§žμ§€ μ•ŠλŠ” 옷

UserDeatilsServiceλ₯Ό μ‚¬μš©ν—ˆκ²Œ 되면 μƒκΈ°λŠ” λ¬Έμ œμ μž…λ‹ˆλ‹€. JWT ν† ν°λ°©μ‹μœΌλ‘œ 인증을 μˆ˜ν–‰ν•  λ•Œ μ‹λ³„μž 정보λ₯Ό ID λ˜λŠ” Email 을 μ‚¬μš©μ„ ν•  경우λ₯Ό 이야기해보도둝 ν•˜κ² μŠ΅λ‹ˆλ‹€.

μ²«λ²ˆμ§Έλ‘œλŠ” JWT 토큰 인증방식을 μ„ νƒν•˜μ—¬ κ΅¬ν˜„ν•˜λŠ” 경우 UserDetailsService λŠ” 둜그인 μ‹œ μ‚¬μš©ν•˜κ²Œ λ©λ‹ˆλ‹€. ν•˜μ§€λ§Œ 둜그인 ν• λ•Œ ID λ˜λŠ” Email 을 μ‚¬μš©ν•˜κ²Œ 되면 Naming Miss Match κ°€ λ°œμƒν•˜κ²Œ λ©λ‹ˆλ‹€. UserDeatilsService λŠ” username 으둜 μ‚¬μš©μžλ₯Ό μ°ΎλŠ” 것에 관심이 μžˆμ§€λ§Œ JWT 인증/인가 방식은 Claim 정보에 μ’€ 더 관심이 μžˆμŠ΅λ‹ˆλ‹€.

λ‘λ²ˆμ§Έλ‘œλŠ” JWT 토큰 인증 방식을 μ΄μš©ν•˜μ—¬ κ΅¬ν˜„ν•  경우 UserDetailsService λŠ” 둜그인 μ‹œμ—λ§Œ μ΄μš©ν•˜κ²Œ λ©λ‹ˆλ‹€. UserDetailsService λŠ” Spring Security 의 μ „λ°˜μ μΈ 인증 과정을 μœ„ν•΄ μ„€κ³„λ˜μ—ˆμ§€λ§Œ 둜그인 μ‹œμ—λ§Œ μ΄μš©ν•˜λŠ”κ²ƒμœΌλ‘œ μ œν•œν•˜κ²Œ λœλ‹€λ©΄ 섀계 μ˜λ„μ— μ–΄κΈ‹λ‚˜κ²Œ λ©λ‹ˆλ‹€.




🧳 λΆˆν•„μš”ν•œ μˆ˜ν™”λ¬Ό

UserDetailsλ₯Ό μ‚¬μš©ν•˜κ²Œ 되면 κ΅¬ν˜„ν•΄μ•Ό ν•˜λŠ” λ©”μ„œλ“œλ“€μ΄ μ—¬λŸ¬κ°œ μ‘΄μž¬ν•˜κ²Œ λ©λ‹ˆλ‹€. 이 λ©”μ„œλ“œλ“€μ˜ λ¬Έμ œμ μ„ 닀뀄보도둝 ν•˜κ² μŠ΅λ‹ˆλ‹€.



μ²«λ²ˆμ§Έλ‘œλŠ” getPassword() λ©”μ„œλ“œμž…λ‹ˆλ‹€. JWT 토큰 μΈμ¦λ°©μ‹μ—λŠ” μ‚¬μš©ν•˜μ§€ μ•ŠλŠ” getPassword() μž…λ‹ˆλ‹€. ν•΄λ‹Ή λ©”μ„œλ“œλ₯Ό κ΅¬ν˜„μ„ κ°•μ œν•˜λ„λ‘ ν•˜κ³  μžˆμœΌλ‹ˆ κ΅¬ν˜„ν•˜κ²Œ 되면 return κ°’μ—λŠ” null 을 λ°˜ν™˜ν•˜λ„λ‘ κ΅¬ν˜„ν•˜κ²Œ λ˜λ―€λ‘œ μ˜λ―Έμ—†λŠ” κ΅¬ν˜„μ„ ν•˜κ²Œλ©λ‹ˆλ‹€.



λ‘λ²ˆμ§Έ λ˜ν•œ μ˜λ―Έμ—†λŠ” κ΅¬ν˜„μ΄ 될 μˆ˜λ„ μžˆλŠ” λ©”μ„œλ“œμž…λ‹ˆλ‹€. ν•΄λ‹Ή λ©”μ„œλ“œλ“€μ€ JWT λΆˆν•„μš”ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

JWTλŠ” 토큰 자체의 λ§Œλ£Œμ‹œκ°„κ³Ό 검증을 μ‚¬μš©ν•˜λŠ”λ° UserDetailsλŠ” 계정 μƒνƒœ 체크 λ©”μ„œλ“œλ“€μ„ κ°•μ œλ‘œ κ΅¬ν˜„ν•΄μ•ΌλŠ” λ¬Έμ œμ μ„ μ•ˆκ³  μžˆμŠ΅λ‹ˆλ‹€.



μΆ”κ°€λ‘œ SecurityContext 에 μ €μž₯λ˜λŠ” Authentication 객체와 UserDetails정보이 생λͺ…μ£ΌκΈ° λΆˆμΌμΉ˜ν•˜λŠ” λ¬Έμ œκ°€ λ°œμƒν•  수 λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€. 둜그인 후에 λ°œμƒν•˜λŠ” μ‚¬μš©μž μƒνƒœ 변화에 λŒ€ν•΄ UserDetails의 정보 갱신이 어렀움이 생길 수 μžˆμŠ΅λ‹ˆλ‹€. UserDeatils, UserDetailsService 에 λŒ€ν•œ μ—¬λŸ¬ λ¬Έμ œμ μ„ μ‚΄νŽ΄λ³΄μ•˜μŠ΅λ‹ˆλ‹€. μ΄λŸ¬ν•œ λ¬Έμ œμ μ„ 해결해보도둝 ν•˜κ² μŠ΅λ‹ˆλ‹€.




πŸ“Œ Spring Security λ‚΄λΆ€λ‘œ λ“€μ–΄κ°€κΈ°

userDetails λ₯Ό μ΄μš©ν•˜μ—¬ κ΅¬ν˜„ν•˜κ²Œ 되면 AuthenticationProvider λŠ” Authentication μ΄λΌλŠ” 인증 객체λ₯Ό λ§Œλ“€μ–΄ SecurityContextHolder 에 μ €μž₯ν•˜κ²Œ λ©λ‹ˆλ‹€. κ·Έλ ‡λ‹€λ©΄ UserDetails 을 μ‚¬μš©ν•˜μ§€ μ•Šκ³  Authentication μ΄λΌλŠ” μΈν„°νŽ˜μ΄μŠ€λ₯Ό 직접 λ§Œλ“€μ–΄ SecurityContextHolder 에 μ €μž₯ν•˜κ²Œ 되면 Spring Security의 μž₯점을 κ°€μ Έκ°€λ©΄μ„œ 객체지ν–₯적으둜 μ½”λ“œλ₯Ό κ΅¬ν˜„ν•  수 있게 λ©λ‹ˆλ‹€. 흐름도λ₯Ό λ¨Όμ € μ„€λͺ…ν•˜κΈ° 전에 각자 μ±…μž„μ„ κ°–κ³  κ΅¬ν˜„ν•΄μ•Όν•˜λŠ” 필터에 λŒ€ν•΄μ„œ μ„€λͺ…ν•˜κ² μŠ΅λ‹ˆλ‹€.


AbstractAuthenticationProcessingFilter

package org.springframework.security.web.authentication;

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {

    public abstract Authentication attemptAuthentication
    (
		    HttpServletRequest request, 
		    HttpServletResponse response
    ) throws AuthenticationException, IOException, ServletException;
}

JWT 토큰 인증방식을 μ΄μš©ν•΄ 인증 ν•„ν„°λ₯Ό κ΅¬ν˜„ν• λ•Œ λŒ€ν‘œμ μΈ ν•„ν„°κ°€ UsernamePasswordAuthenticationFilter κ°€ μžˆμŠ΅λ‹ˆλ‹€. UsernamePasswordAuthenticationFilter 에 μƒμœ„ κ΅¬ν˜„μ²΄κ°€ AbstractAuthenticationProcessingFilter μž…λ‹ˆλ‹€. ν•΄λ‹Ή ν•„ν„°λ₯Ό 상속받아 쑰금 더 Custom ν•˜κ²Œ 인증 ν•„ν„°λ₯Ό κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ GenericFilterBean 을 상속받기 λ•Œλ¬Έμ— ν•œλ²ˆ μš”μ²­μ— μ€‘λ³΅λ˜λŠ” 인증을 μˆ˜ν–‰ν•  수 μžˆλŠ” 문제점이 λ°œμƒν•˜λŠ”λ° SecurityContext λ₯Ό ν™•μΈν•˜μ—¬ 이미 인증된 μ‚¬μš©μžμΈμ§€ μ²΄ν¬ν•˜λŠ” 방법이 μ‘΄μž¬ν•©λ‹ˆλ§Œ ν•΄λ‹Ή μ£Όμ œμ—μ„œ λ²—μ–΄λ‚œ μ£Όμ œμ΄κΈ°μ— 닀루지 μ•Šλ„λ‘ ν•˜κ² μŠ΅λ‹ˆλ‹€.


인가 ν•„ν„°λŠ” OncePerRequestFilter λ₯Ό μ΄μš©ν•˜μ—¬ κ΅¬ν˜„ν•˜λ©΄ λ©λ‹ˆλ‹€. ν•΄λ‹Ή ν•„ν„°λ₯Ό μ΄μš©ν•˜μ—¬ 인가 흐름은

  1. URIλ₯Ό ν™•μΈν•˜μ—¬ 인가가 ν•„μš”ν•œμ§€ μ—¬λΆ€λ₯Ό 확인
  2. JWT 토큰을 헀더 λ˜λŠ” μΏ ν‚€μ—μ„œ 정보 κ°€μ Έμ˜€κΈ°
  3. JWT 토큰 μœ νš¨μ„± 검사 (JWT Provider μ—κ²Œ μ±…μž„ μœ„μž„)
  4. 인증이 μ •μƒμ μœΌλ‘œ μˆ˜ν–‰λ˜μ—ˆλ‹€λ©΄ SecurityContextHolder 에 μ €μž₯

ν•΄λ‹Ή νλ¦„λŒ€λ‘œ μ§„ν–‰ν•œλ‹€λ©΄ 관심사와 μ±…μž„μ„ 잘 κ°€μ§€κ³  μžˆλŠ” ν•„ν„°κ°€ 될 κ²ƒμž…λ‹ˆλ‹€.

μ½”λ“œμ μΈ 뢀뢀은 λ§ˆμ§€λ§‰ λΆ€λΆ„μ—μ„œ 닀루도둝 ν•˜κ³  흐름도에 λŒ€ν•΄μ„œ μ€‘μ μ μœΌλ‘œ μ„€λͺ…ν•˜κ² μŠ΅λ‹ˆλ‹€.




🎨 그림으둜 이해해보기

둜그인 인증 흐름

둜그인 인증 흐름은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  • ID 와 Passwordλ₯Ό μž…λ ₯λ°›μ•˜μ„λ•Œ AbstractAuthenticationProcessingFilter λ₯Ό κ΅¬ν˜„ν•œ LoginAutehnticationFilter 둜 λ„μ°©ν•˜κ²Œ λ©λ‹ˆλ‹€.
  • LoginAuthenticationFilter λŠ” Json 을 νŒŒμ‹±ν•˜μ—¬ μΈμ¦λ˜μ§€ μ•Šμ€ Authentication 을 λ§Œλ“€μ–΄ AuthenticationProvider μ—κ²Œ 인증을 μœ„μž„ν•©λ‹ˆλ‹€.
  • AuthenticationProvider λ₯Ό κ΅¬ν˜„ν•œ LoginAuthenticationProvider λŠ” λΉ„μ¦ˆλ‹ˆμŠ€ λ‘œμ§μ— 따라 인증을 μ‹€μ§ˆ 적으둜 μˆ˜ν–‰ν•˜κ²Œ λ©λ‹ˆλ‹€. μ—¬κΈ°μ„œλŠ” DBλ₯Ό 톡해 μ‚¬μš©μž 정보λ₯Ό ν™•μΈν•˜μ—¬ 인증된 Authentication 을 λ°œκΈ‰ν•©λ‹ˆλ‹€.
  • LoginAuthenticationProvider 둜 λΆ€ν„° 인증된 Authentication 이 LoginAuthenticationFilter 에 λ„μ°©ν•˜λ©΄ successfulAuthentication μ—μ„œ 토큰을 헀더 λ˜λŠ” 쿠킀에 λ‹΄μ•„μ„œ ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ μ‘λ‹΅ν•˜λ©΄λ©λ‹ˆλ‹€.
  • λ§Œμ•½ 인증에 μ‹€νŒ¨ν•˜μ˜€λ‹€λ©΄ unsuccessfulAuthentication λ₯Ό 톡해 인증 μ‹€νŒ¨μ— λŒ€ν•œ 정보λ₯Ό ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ μ‘λ‹΅ν•˜λ©΄ λ©λ‹ˆλ‹€.



JWT 인가 흐름

JWT 토큰 인증 흐름은 λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  • ν΄λΌμ΄μ–ΈνŠΈλ‘œ λΆ€ν„° JWT 토큰 정보λ₯Ό μš”μ²­λ°›κ²Œ 되면 OncePerRequestFilter λ₯Ό κ΅¬ν˜„ν•œ JwtAuthorizationFilter 에 λ„μ°©ν•˜κ²Œ λ©λ‹ˆλ‹€.
  • JwtAuthorizationFilter λŠ” JwtToken을 μΆ”μΆœμ„ JwtProvider μ—κ²Œ μœ„μž„ν•˜μ—¬ JwtToken 정보λ₯Ό ν™•μΈν•©λ‹ˆλ‹€.
  • JwtToken 정보λ₯Ό ν™•μΈν•˜μ˜€λ‹€λ©΄ Authentication 인증 객체λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.
  • 인증 된 Authentication 객체λ₯Ό SecurityContext 에 μ €μž₯ν•˜μ—¬ ν΄λΌμ΄μ–ΈνŠΈκ°€ μ„œλΉ„μŠ€λ₯Ό μ΄μš©ν•  수 μžˆλ„λ‘ ν•©λ‹ˆλ‹€.





πŸ§‘β€πŸ’» 전체적인 μ½”λ“œ

Principal

@Getter
public class AuthUser {

    private final Long id;
    private final String email;
    private final Collection<? extends GrantedAuthority> authorities;

    public AuthUser(Long id, String email, Role role) {
        this.id = id;
        this.email = email;
        this.authorities = List.of(new SimpleGrantedAuthority("ROLE_" + role));
    }

    public static AuthUser from(User user) {
        return new AuthUser(user.getId(), user.getEmail(), user.getRole());
    }
}
  • User 정보λ₯Ό λŒ€μ²΄ν•  κ°μ²΄μž…λ‹ˆλ‹€.



Authentication

package com.my.relink.config.security;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;
import java.util.Collections;

public class LoginAuthentication implements Authentication {
    private final String email;
    private final String password;
    private boolean authenticated;
    private AuthUser principal;

    public LoginAuthentication(String email, String password) { // 인증 μ „ 
        this.email = email;
        this.password = password;
        this.authenticated = false;
    }

    public LoginAuthentication(AuthUser authUser) { // 인증 ν›„ 
        this.email = authUser.getEmail();
        this.password = null;
        this.authenticated = true;
        this.principal = authUser;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return principal != null ? principal.getAuthorities() : Collections.emptyList();
    }

    @Override
    public Object getCredentials() {
        return password;
    }

    @Override
    public Object getDetails() {
        return principal;
    }

    @Override
    public Object getPrincipal() {
        return authenticated ? principal : email;
    }

    @Override
    public boolean isAuthenticated() {
        return authenticated;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        this.authenticated = isAuthenticated;
    }

    @Override
    public String getName() {
        return email;
    }
}
  • Authentication μƒνƒœλŠ” 인증 μ „/인증 ν›„ 두 κ°€μ§€ μƒνƒœλ₯Ό κ°€μ§€κ³  μžˆμŠ΅λ‹ˆλ‹€.



AuthenticationProvider

package com.my.relink.config.security;

import com.my.relink.domain.user.User;
import com.my.relink.domain.user.repository.UserRepository;
import com.my.relink.ex.ErrorCode;
import com.my.relink.ex.SecurityFilterChainException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class LoginAuthenticationProvider implements AuthenticationProvider {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        LoginAuthentication loginAuthentication = (LoginAuthentication) authentication;
        String email = loginAuthentication.getName();
        String password = (String) loginAuthentication.getCredentials();

        User user = userRepository.findByEmailActiveUser(email)
                .orElseThrow(() -> new SecurityFilterChainException(ErrorCode.USER_NOT_FOUND));

        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new SecurityFilterChainException(ErrorCode.MISS_MATCHER_PASSWORD);
        }

        return new LoginAuthentication(AuthUser.from(user));
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return LoginAuthentication.class.isAssignableFrom(authentication);
    }
}
  • DB μ—μ„œ μ‚¬μš©μž 정보λ₯Ό 가져와 인증을 μˆ˜ν–‰ν•˜κ²Œλ©λ‹ˆλ‹€.
  • AuthenticationProvider λŠ” 인증이 μ–΄λ–»κ²Œ μˆ˜ν–‰λ˜λŠ”μ§€μ— λŒ€ν•œ μžμ„Έν•œ 이야기λ₯Ό μ•Œκ³  μžˆμŠ΅λ‹ˆλ‹€.



AbstractAuthenticationProcessingFilter

package com.my.relink.config.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.my.relink.config.security.dto.req.LoginRepDto;
import com.my.relink.config.security.dto.resp.LoginRespDto;
import com.my.relink.config.security.jwt.JwtProvider;
import com.my.relink.domain.user.User;
import com.my.relink.domain.user.repository.UserRepository;
import com.my.relink.ex.ErrorCode;
import com.my.relink.ex.SecurityFilterChainException;
import com.my.relink.util.api.ApiResult;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    private final ObjectMapper objectMapper;
    private final JwtProvider jwtProvider;
    private final UserRepository userRepository;

    public LoginAuthenticationFilter(AuthenticationManager authenticationManager, ObjectMapper objectMapper, JwtProvider jwtProvider, UserRepository userRepository) {
        super(new AntPathRequestMatcher("/auth/login", HttpMethod.POST.name()));
        this.setAuthenticationManager(authenticationManager);
        this.objectMapper = objectMapper;
        this.jwtProvider = jwtProvider;
        this.userRepository = userRepository;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            LoginRepDto loginRequest = objectMapper.readValue(request.getInputStream(), LoginRepDto.class);

            LoginAuthentication loginAuthentication = new LoginAuthentication(
                    loginRequest.getEmail(),
                    loginRequest.getPassword()
            );
            return this.getAuthenticationManager().authenticate(loginAuthentication);
        } catch (IOException ex) {
            throw new SecurityFilterChainException(ErrorCode.JSON_PARSE_ERROR, ex);
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        String token = jwtProvider.generateToken(authResult);
        User user = userRepository.findByEmail(authResult.getName())
                .orElseThrow(() -> new SecurityFilterChainException(ErrorCode.USER_NOT_FOUND));

        response.addHeader("Authorization", token);
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.getWriter().write(objectMapper.writeValueAsString(ApiResult.success(new LoginRespDto(user))));
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.getWriter().write(objectMapper.writeValueAsString(ApiResult.error(ErrorCode.INVALID_CREDENTIALS)));
    }
}
  • ν•΄λ‹Ή 필터에 μ±…μž„μ€ 인증 μ „ Authentication을 λ§Œλ“€μ–΄ 인증을 μœ„μž„ν•˜κ³  인증이 성곡 λ˜λŠ” μ‹€νŒ¨μ— λŒ€ν•œ μ±…μž„μ„ κ°€μ§€κ³  μžˆμŠ΅λ‹ˆλ‹€.



AuthorizationFilter

package com.my.relink.config.security;

import com.my.relink.config.security.jwt.JwtProvider;
import com.my.relink.ex.ErrorCode;
import com.my.relink.ex.SecurityFilterChainException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.List;

@Component
@RequiredArgsConstructor
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    private final JwtProvider jwtProvider;

    private final List<String> publicPaths;

    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        if (isPublicPath(request.getRequestURI())) {
            filterChain.doFilter(request, response);
            return;
        }

        String tokenValue = request.getHeader(JwtProvider.AUTHENTICATION_HEADER_PREFIX);
        if (tokenValue == null) {
            throw new SecurityFilterChainException(ErrorCode.TOKEN_NOT_FOUND);
        }
        String token = tokenValue.replace(JwtProvider.TOKEN_PREFIX, "");
        jwtProvider.validateToken(token);

        AuthUser authUser = jwtProvider.getAuthUserForToken(token);

        LoginAuthentication loginAuthentication = new LoginAuthentication(authUser);
        SecurityContextHolder.getContext().setAuthentication(loginAuthentication);
        filterChain.doFilter(request, response);
    }

    private boolean isPublicPath(String uri) {
        return publicPaths.stream()
                .anyMatch(path -> antPathMatcher.match(path, uri));
    }
}
  • ν•΄λ‹Ή ν•„ν„°λŠ” 인가λ₯Ό μˆ˜ν–‰ν•˜λŠ” 필터이기에 인가 ν•„ν„°μ—μ„œ μˆ˜ν–‰ν•˜λŠ” URI 인지λ₯Ό ν™•μΈν•œ ν›„ 토큰 μœ νš¨μ„±μ„ κ²€μ¦ν•˜μ—¬ SecurityContextHolder 에 μ €μž₯ν•˜λŠ” μ±…μž„μ„ κ°€μ§€κ³  μžˆμŠ΅λ‹ˆλ‹€.








πŸ“– 톺아보기

  • λΆˆν•„μš”ν•œ λ©”μ„œλ“œ, 넀이밍 뢈일치, 생λͺ…μ£ΌκΈ° 뢈일치 λ“± μ—¬λŸ¬ λ¬Έμ œμ μ„ ν™•μΈν•΄λ³΄μ•˜μŠ΅λ‹ˆλ‹€.

  • 인증 객체λ₯Ό 직접 κ΅¬ν˜„ν•¨μœΌλ‘œμ¨ μ—¬λŸ¬ 단점듀을 ν•΄μ†Œν–ˆμŠ΅λ‹ˆλ‹€.
  • Spring Security 에 ꡬ쑰적인 μž₯점을 살릴 수 μžˆμ—ˆμœΌλ©° 객체지ν–₯적으둜 섀계할 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.
  • Spring Security λŠ” 맀우 κ°•λ ₯ν•œ ν”„λ ˆμž„μ›Œν¬μ΄λ©° κ°œλ°œμžκ°€ 인증/인가 κ΅¬ν˜„μ„ νŽΈν•˜κ²Œ ν•  수 있게 ν•˜λŠ” 반면 λͺ¨λ“ κ²ƒμ„ Spring Security 에 μ˜μ‘΄ν•˜κ²Œ 되면 디버깅과 μ—¬λŸ¬ 아킀텍쳐적인 문제점이 λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  • νŽΈλ¦¬ν•œ ν”„λ ˆμž„μ›Œν¬λ₯Ό μ΄μš©ν•˜κΈ°μ— μ•žμ„œ ν”„λ ˆμž„μ›Œν¬μ— 내뢀적인 λ™μž‘λ°©μ‹μ—λŒ€ν•΄ 잘 μ΄ν•΄ν•˜κ³  μ‚¬μš©ν•˜κ²Œ λœλ‹€λ©΄ μ—¬λŸ¬ λ¬Έμ œμ μ„ ν•΄μ†Œν•  수 μžˆμŠ΅λ‹ˆλ‹€.
profile
βœ…Β μ λ‹Ήν•œ 좔상화λ₯Ό μ°Ύμ•„κ°€λŠ” κ°œλ°œμžμž…λ‹ˆλ‹€.

0개의 λŒ“κΈ€