JWT = ์ํ

์๊พธ ๊ณต๋ถํ๋ฉด ๊ณต๋ถํ ์๋ก ๋ญ ์ํ๋ง๋ฅ ์์์ผ ํ ๊ฒ ์๊พธ ๋์จ๋ค.. ๋ฏธ์ณ๋ฒ๋ฆด์ง๊ฒฝ
์ด๋ป๊ฒ ๊ตฌํํ ๊ฑฐ๋๋ฉด
- ์ฌ์ฉ์ ๊ฒ์ฆ UserDetailsService
- ๋น๋ฐ๋ฒํธ ๋น๊ต PasswordEncoder
- Authentication ๊ฐ์ฒด ์์ฑ
- JWT ๋ฐ๊ธ
- ํด๋ผ์ด์ธํธ์ ํ ํฐ ์ ๋ฌ
- Filter ๊ตฌํ
์์๋๋ก ์์ฑํ ์์ ์ด๋ค.
์์ฑ ์์
@Service
@RequiredArgsConstructor
@Slf4j
public class AuthService {
private final AuthenticateHandler authenticateHandler;
private final JwtProvider jwtProvider;
private final UserDetailsServiceImpl userDetailsServiceImpl;
public TokenDTO login(LoginRequestDTO request) {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
/*
* (1) ์ฌ์ฉ์ ๊ฒ์ฆ(UserDetailsService)
* */
UserDetailsDTO userDetails = (UserDetailsDTO) userDetailsServiceImpl.loadUserByUsername(request.getUserId());
/*
* (2) ๋น๋ฐ๋ฒํธ ๋น๊ต(PasswordEncoder)
* */
if (!passwordEncoder.matches(request.getPassword(), userDetails.getPassword())) {
throw new BadCredentialsException("๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์!(T_T)");
}
/*
* (3) Authentication ๊ฐ์ฒด ์์ฑ
* */
Authentication authentication = authenticateHandler.authenticate(request);
// (4) JWT ๋ฐ๊ธ
String accessToken = jwtProvider.createAccessToken(authentication);
// (5) ํด๋ผ์ด์ธํธ์ ํ ํฐ ์ ๋ฌ
return new TokenDTO(accessToken);
}
}
1. ์ฌ์ฉ์ ๊ฒ์ฆ
DB์์ ์ ์ ์ ๋ณด๋ฅผ ์กฐํํ๊ณ UserDetails ๊ฐ์ฒด๋ฅผ ๋ง๋ ๋ค. ex) ์ด username์ ๊ฐ์ง ์ ์ ๊ฐ ์๋์? → UserDetailsImpl ๋ง๋ค์ด์ ์คํ๋ง ์ํ๋ฆฌํฐ์ ์ ๊ณตํ๋ค.
๋ก๊ทธ์ธ์ 2๋จ๊ณ๋ก ์ด๋ฃจ์ด์ง๋๋ฐ, ์ฒซ๋ฒ์งธ๋ ์ฌ์ฉ์ ๊ฒ์ฆ(Authentication ์ค๋น), ๋๋ฒ์งธ๋ ๋น๋ฐ๋ฒํธ ๊ฒ์ฆ(Authentication ์๋ฃ) ๋จ๊ณ์ด๋ค.
์ฌ์ฉ์ ๊ฒ์ฆ์ "๋๊ฐ ๋๊ตฐ์ง", ์ฆ ์ด userId๋ฅผ ๊ฐ์ง ์ฌ๋์ด ์ค์ ๋ก ์กด์ฌํ๋์ง, ๊ทธ๋ฆฌ๊ณ DB์ ๋ฑ๋ก๋ ์ฌ์ฉ์์ธ์ง ๊ฒ์ฆ์ ํ๋ค.
๋น๋ฐ๋ฒํธ ๊ฒ์ฆ์ "์ง์ง ๋๊ฐ ๋ง๋์ง"๋ฅผ ํ์ธํ๋ ๋จ๊ณ์ด๋ค.
์ด 2๋จ๊ณ๊ฐ ์์ด๋ฉด ์ํํ๊ธฐ ๋๋ฌธ์ ์ํ๋ฆฌํฐ๋ ์ผ๋ถ๋ฌ ๋๋ ๋จ๋ค.
| ์ญํ | ์ฑ ์ |
| UserDetailsService | ์ด ์ฌ์ฉ์๊ฐ ์กด์ฌํ๋์ง |
| UserDetails | ์ด ์ฌ์ฉ์์ ์ ๋ณด๋ ๋ฌด์์ธ์ง |
| AuthenticationProvider | ๋น๋ฐ๋ฒํธ๊ฐ ๋ง๋์ง |
| SecurityContext | ๋ก๊ทธ์ธ ์๋ฃ ํ ์ํ ์ ์ฅ |
๊ทธ๋ฆฌํ์ฌ ์ฌ์ฉ์ ๊ฒ์ฆ ๋จ๊ณ์์
- โ ๋ก๊ทธ์ธ ์ฑ๊ณต ์ฒ๋ฆฌ ์ ํจ
- โ ์ธ์ /JWT ์ ๋ง๋ฆ
- โญ “์ด ์ฌ๋์ด ๋๊ตฌ์ธ์ง ์ค๋ช ์๋ง ์ ๋ฌ”
๋ฅผ ์ฒ๋ฆฌํ๋ฉด ๋๋ค.
์ฝ๋์ ๋ํ ์ค๋ช ์ ๋๋ฌด ๊ธธ์ด์ ธ์ ๋ฐ๋ก ๋ถ๋ฆฌํ๋ค.
2. ๋น๋ฐ๋ฒํธ ๋น๊ต
์ ๋ ฅํ ๋น๋ฐ๋ฒํธ๋ฅผ ์ํธํํ์ ๋ DB์ ์ํธ๊ฐ๊ณ ๋์ผํ์ง ํ์ธํ๋ค.
์ฌ์ค ์ด ๊ณผ์ ์ Authentication ๊ณผ์ ์ด ์์ผ๋ฉด ๊ตณ์ด ํ์ํ์ง ์๋ค๊ณ ํ๋๋ฐ.. ๊ทธ๋ฅ ๊ณต๋ถ์ฐจ ์์ฑํ๋ค.
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
if (!passwordEncoder.matches(request.getPassword(), userDetails.getPassword())) {
throw new BadCredentialsException("๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์!");
}
(1) PasswordEncoder
๋น๋ฐ๋ฒํธ๋ฅผ ์์ ํ๊ฒ ์ํธํํ๊ณ , ์ ๋ ฅ๋ ๋น๋ฐ๋ฒํธ์ ์ ์ฅ๋ ์ํธํ๋ ๋น๋ฐ๋ฒํธ๋ฅผ ๋น๊ตํ๋ ์ธํฐํ์ด์ค
public interface PasswordEncoder {
ใใ// ๋น๋ฐ๋ฒํธ๋ฅผ ๋จ๋ฐฉํฅ ์ํธํ
ใใString encode(CharSequence rawPassword);
ใใ// ์ํธํ๋์ง ์์ ๋น๋ฐ๋ฒํธ(raw-)์ ์ํธํ๋ ๋น๋ฐ๋ฒํธ(encoded-)๊ฐ ์ผ์นํ๋์ง ๋น๊ต
ใใboolean matches(CharSequence rawPassword, String encodedPassword);
ใใ// ์ํธํ๋ ๋น๋ฐ๋ฒํธ๋ฅผ ๋ค์ ์ํธํํ๊ณ ์ ํ ๊ฒฝ์ฐ true๋ฅผ returnํ๊ฒ ์ค์
ใใdefault boolean upgradeEncoding(String encodedPassword) { return false; };
}
์ด๋ฐ์์ผ๋ก ๊ตฌ์ฑ๋์ด ์๊ณ , PasswordEncoder ๊ตฌํ ํด๋์ค๋ 4๊ฐ๊ฐ ์๋ค.
- BcryptPasswordEncoder : BCrypt ํด์ ํจ์๋ฅผ ์ฌ์ฉํ์ฌ ๋น๋ฐ๋ฒํธ๋ฅผ ์ํธํ
- Argon2PasswordEncoder: Argon2 ํด์ ํจ์๋ฅผ ์ฌ์ฉํด ๋น๋ฐ๋ฒํธ๋ฅผ ์ํธํ
- Pbkdf2PasswordEncoder : PBKDF2 ํด์ ํจ์๋ฅผ ์ฌ์ฉํ์ฌ ๋น๋ฐ๋ฒํธ๋ฅผ ์ํธํ
- SCryptPasswordEncoder : SCrypt ํด์ ํจ์๋ฅผ ์ฌ์ฉํด์ ๋น๋ฐ๋ฒํธ ์ํธํ
์ฌ๊ธฐ์ BCryptPasswordEncoder๋ฅผ ์ด์ฉํ๋ค. ๋จ์ํ ํด์ฑ์ด ์๋ ์ํธ(Salt)๋ผ๋ ๋ ๋ค ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐํ์ฌ ๋ณด์์ฑ์ ๋์ธ๋ค. ๋์ผํ ๋น๋ฐ๋ฒํธ๋ ์ํธํ์ ๋ฐ๋ผ ๊ฒฐ๊ณผ๊ฐ์ด ๋ฌ๋ผ์ง๊ธฐ ๋๋ฌธ์ ์๋ ๋น๋ฐ๋ฒํธ๋ฅผ ์์๋ด๊ธฐ ๋งค์ฐ ์ด๋ ต๋ค.
(2) matches(ํ๋ฌธ, ์ํธ๋ฌธ)
์ ์ฅ๋ ์ํธ๋ฌธ(userDetails.getPassword())์์ ์ฌ์ฉ๋ฌ๋ ์ํธ(Salt)๊ฐ์ ์ถ์ถํ๋ค. ์ถ์ถ๋ ์ํธ์ ์ ๋ ฅ๋ฐ์ ํ๋ฌธ ๋น๋ฐ๋ฒํธ๋ฅผ ๊ฒฐํฉํ์ฌ ๋ค์ ํด์ฑํ ํ, ๊ทธ ๊ฒฐ๊ณผ๊ฐ ์ ์ฅ๋ ์ํธ๋ฌธ๊ณผ ๊ฐ์์ง ํ์ธํ๋ค.
(3) BadCredentialsException
Spring Security์์ ์ ๊ณตํ๋ ํ์ค ์์ธ๋ก, ์๊ฒฉ์ฆ๋ช (id/pw)์ด ์ ํจํ์ง ์์ ๋ ๋ฐ์ํ๋ค. ์ฌ์ฉ์์ id์ ํด๋นํ๋ ํจ์ค์๋๊ฐ ์ผ์นํ์ง ์์ ๋, ์ฌ์ฉ์์ ์์ด๋๊ฐ ์ ๋ฌ๋์ง ์์ ๋ ๋ฑ๋ฑ!
์ด ์์ธ๊ฐ ๋ฐ์ํ๋ฉด ๋ณดํต Spring Security์ ์ธ์ฆ ํํฐ๊ฐ ์ด๋ฅผ ๊ฐ๋ก์ฑ์ ๋ก๊ทธ์ธ ์คํจ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธํ๊ฑฐ๋, 401 unauthorized ์๋ฌ ์๋ต์ ๋ณด๋ด๊ฒ ๋๋ค
3. Authentication ๊ฐ์ฒด ์์ฑ
private final AuthenticateHandler authenticateHandler;
Authentication authentication = authenticateHandler.authenticate(request);
Authentication ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ ๊ณผ์ ์ "์ด ์ฌ์ฉ์๊ฐ ์ ๋ง ์ฐ๋ฆฌ ํ์์ด ๋ง๋์ง ์์คํ ์ ์ผ๋ก ์ต์ข ์น์ธ ๋์ฅ์ ์ฐ๋ ๊ณผ์ "์ด๋ผ๊ณ ์๊ฐํ๋ฉด ๋๋ค.
Authentication ๊ฐ์ฒด๋?
Spring Security์์ Authentication์ ํ์ฌ ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ์ ์ ๋ณด์ฆ์์ด๋ค.
- Principal: ์ฌ์ฉ์์ ์์ด๋ ํน์ ์ฌ์ฉ์ ๊ฐ์ฒด
- Credentials: ๋น๋ฐ๋ฒํธ(์ธ์ฆ์ ์ฌ์ฉ๋ ์ฆ๊ฑฐ)
- Authorities: ์ฌ์ฉ์๊ฐ ๊ฐ์ง ๊ถํ
- Authenticated: ์ธ์ฆ ์ฌ๋ถ(T/F)
(1) AuthenticationManager ์๋ ์์ฑ
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
AuthenticationManager์ "์ธ์ฆ ์ผํฐ์ ์ด์ฑ ์์"์ด๋ค. ๋ง ๊ทธ๋๋ก ์ธ์ฆ(Authentication)์ ์ฒ๋ฆฌํ๋ ๊ด๋ฆฌ์์ธ๋ฐ, ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ์ ์๋ํ๋ฉด ๊ทธ ์ ๋ณด๊ฐ ๋ง๋์ง ํ์ธํ๋ค.
์์ Spring Security๋ WebSecurityConfigurerAdapter์ด๋ผ๋๊ฑธ ์์๋ฐ์ผ๋ฉด ์๋์ผ๋ก AuthenticationManager์ ์์ฑํด์คฌ๋๋ฐ, ์์ฆ์(5.7๋ฒ์ ์ด์)์ ์ปดํฌ๋ํธ ๊ธฐ๋ฐ ์ค์ ์ผ๋ก ๋ฐ๋๋ฉด์ ๋ณด์์์ ์ด์ ๋ก ์๋ Bean ์์ฑ์ ์ํด์ฃผ๊ณ ๊ฐ๋ฐ์๊ฐ ๋ช ์ํด์ผ ํ๋ค๊ณ ํ๋ค.............
๊ทธ๋์ Bean์ผ๋ก ๋ฑ๋ก์ ํด์คฌ๋ค.
- AuthenticationConfiguration : ์คํ๋ง์ด ๋ด๋ถ์ ์ผ๋ก ๋ง๋ค์ด๋ ์ธ์ฆ ์ค์ ์ ๋ณด
- .getAuthenticationManager() : ์ด๋ฏธ ์ค์ ๋ ์ ๋ณด(UserDetailsService, PasswordEncoder ๋ฑ)๋ฅผ ๋ฐํ์ผ๋ก ์์ฑํ "์ด์ฑ ์์(Manager)"๋ฅผ ๊บผ๋ด์ค๋ ๊ณผ์
(2) AuthenticationHandler
@Service
@Slf4j
@RequiredArgsConstructor
public class AuthenticateHandler {
private final AuthenticationManager authenticationManager;
public Authentication authenticate (LoginRequestDTO request) {
try {
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUserId(),
request.getPassword()
)
);
} catch (BadCredentialsException e) {
log.warn("์ธ์ฆ ์คํจ - ์ฌ์ฉ์: {} (์๋ชป๋ ๋น๋ฐ๋ฒํธ)", request.getUserId());
throw e;
} catch (UsernameNotFoundException e) {
log.warn("์ธ์ฆ ์คํจ - ์ฌ์ฉ์: {} (์กด์ฌํ์ง ์๋ ๊ณ์ )", request.getUserId());
throw e;
} catch (Exception e) {
log.error("์ธ์ฆ ์ค ์ค๋ฅ ๋ฐ์ - ์ฌ์ฉ์: {}", request.getUserId(), e);
throw e;
}
} // authenticate
}
1๏ธโฃ UsernamePasswordAuthenticationToken() ์์ฑ
์ฌ์ฉ์๊ฐ ๋ณด๋ธ ์์ด๋์ ๋น๋ฐ๋ฒํธ๋ฅผ ๋ด์ ์ธ์ฆ ์์ฒญ์ฉ ํ ํฐ์ ๋จผ์ ๋ง๋ ๋ค.
์ด๋๋ ์์ง ๊ฒ์ฆ๋์ง ์์๊ธฐ ๋๋ฌธ์ setAuthenticated(false) ์ํ์ธ ์ผ์ข ์ ๋ก๊ทธ์ธ ์ ์ฒญ์์๊ฐ๋ค.
2๏ธโฃ AuthenticationManager.authenticate() ํธ์ถ
์ด "์ ์ฒญ์"๋ฅผ AuthenticationManager์๊ฒ ์ ๋ฌํ๋ค.
๊ด๋ฆฌ์๋ ๋ด๋ถ์ ์ผ๋ก ๋ฑ๋ก๋ AuthenticationProvider๋ค์ ์ฐพ์ ์ด ์์ด๋์ ๋น๋ฐ๋ฒํธ๊ฐ ์ง์ง ๋ง๋์ง ํ์ธํ๋ค.
3๏ธโฃ ์ต์ข Authentication ๊ฐ์ฒด ๋ฐํ(์ธ์ฆ ์๋ฃ)
๋ด๋ถ ๊ฒ์ฆ(๋น๋ฐ๋ฒํธ ๋น๊ต ๋ฑ)์ด ์ฑ๊ณตํ๋ฉด, ์ฌ์ฉ์์ ๊ถํ(Roles)๊ณผ ์์ธ ์ ๋ณด๊ฐ ํฌํจ๋ ์๋ก์ด Authentication ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ค.
์ด ๊ฐ์ฒด๋ setAuthenticatted(true)์ํ์ด๋ฉฐ, ์ด์ ์์คํ ์ด๋์๋ "์ด ์ฌ๋์ ๋ฏฟ์ ์ ์๋ ์ฌ์ฉ์๋ค" ๋ผ๊ณ ์ธ์ ๋ฐ๊ฒ ๋๋ค.
4. JWT ๋ฐ๊ธ
@Component
public class JwtProvider {
private Long ACCESS_TOKEN;
private Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
public String createAccessToken (Authentication authentication) {
UserDetailsDTO principal = (UserDetailsDTO) authentication.getPrincipal();
String userId = principal.getUserId();
List<String> roles = principal.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList();
Date now = new Date();
Date expiry = new Date(now.getTime() + ACCESS_TOKEN);
return Jwts.builder()
.setSubject(userId)
.claim("role", roles)
.setIssuedAt(now)
.setExpiration(expiry)
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}
}
createAccessToken : ์๋ก์ด ํ ํฐ์ ์์ฑํ๋ ๋ฉ์๋
- ACCESS_TOKEN : application.yml ํน์ properties์์ ์ค์ ๋ ๋ง๋ฃ์๊ฐ ๊ฐ์ ๊ฐ์ ธ์จ๋ค
- secretKey : ์๋ช (Signature)์ ์ํ ํค. HS256์๊ณ ๋ฆฌ์ฆ์ ์ ํฉํ ์์ ํค๋ฅผ ์๋์ผ๋ก ์์ฑ.
- Keys.secretKeyFor๋ ์๋ฒ๊ฐ ์ฌ์์๋ ๋ ๋ง๋ค ์๋ก์ด ํค๋ฅผ ์์ฑํ๋ค.
(1) ์ฌ์ฉ์ ์ ๋ณด ์ถ์ถ(Payload ์ค๋น)
- authentication.getPrincipal() : ์์ ๋จ๊ณ์์ ์์ฑํ๋ ์ธ์ฆ ๊ฐ์ฒด์์ ์ฌ์ฉ์ ์์ธ์ ๋ณด๋ฅผ ๊บผ๋(UserDetailsDTO). ํ ํฐ ๋ด๋ถ์ ์ ์ฅํ ์ฌ์ฉ์ ID์ ๊ถํ๋ชฉ๋ก์ ์ค๋น
(2) JWT ํ ํฐ ์์ฑ
- setSubject : ํ ํฐ์ ์๋ณ์. ๋ณดํต ์ฌ์ฉ์ ๊ณ ์ ์์ด๋๋ฅผ ๋ฃ์
- claim("role", roles) : ํ์ค ํ๋ ์ธ์ ๊ฐ๋ฐ์๊ฐ ์ถ๊ฐํ๊ณ ์ถ์ ์ ๋ณด๋ฅผ ๋ฃ์
- setIssuedAt(now) : ํ ํฐ ๋ฐํ ์๊ฐ
- setExpiration(expiry) : ํ ํฐ ๋ง๋ฃ ์๊ฐ
- signWith(secretKey, SignatureAlgorithm.HS256) : ์ํธํ ์๋ช . ์ค๋นํ secretKey์ HS256์๊ณ ๋ฆฌ์ฆ์ ์ฌ์ฉํด ํ ํฐ์ด ์์กฐ๋์ง ์์์ ์ฆ๋ช ํ๋ ์๋ช . ๊ฐ์ฅ ์ค์ํ ๋ณด์ ๋จ๊ณ
- compact() : ๋ชจ๋ ์ค์ ์ ๋ง์น๊ณ ์ต์ข ์ ์ผ๋ก header.payload.signature ํํ์ ๊ธด ๋ฌธ์์ด์ ๋ฐํ
5. ํด๋ผ์ด์ธํธ์ ํ ํฐ ์ ๋ฌ
return new TokenDTO(accessToken);
6. Filter ๊ตฌํ
์ด๊ฒ๋ ์ค๋ช ๊ธธ์ด์ง ๊ฒ ๊ฐ์์ ๋ฐ๋ก ๊ฒ์๊ธ ๋ง๋ค์๋ค.
๋ง๋ฌ๋ ์ค๋ฅ ์ฌํญ
1. java.lang.StackOverflowError

๋ฉ์๋ ํธ์ถ์ด ๋๋ฌด ๊น์ด์ ธ์ ์คํ ๋ฉ๋ชจ๋ฆฌ๊ฐ ๊ฐ๋ ์ฐผ์ ๋ ๋ฐ์ํ๋ ์๋ฌ์ด๋ค.
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:216) ~[spring-aop-6.1.10.jar:6.1.10]
at jdk.proxy3/jdk.proxy3.$Proxy69.authenticate(Unknown Source) ~[na:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
์ด๊ฒ ๋ฌดํ๋ฐ๋ณต๋๋๊ฑธ ํ์ธํ๋ค.
AuthenticateHandler์ ๊ฐ์ด Authentication๊ณผ ๊ด๋ จ๋ ๊ณผ์ ์์ ๋ฌด์ธ๊ฐ๊ฐ ๋ฌดํ๋ฐ๋ณตํด์ ํธ์ถํ๋๊ฑฐ๋ผ๋๊ฑด ์ด๋ฆผ์ง์ ํ ์ ์์๋ค.
Spring AOP ํ๋ก์์ ๊ด๋ จ๋ ์ฌ๊ท๋ฌธ์ ๋ผ๊ณ ํ๋ค.
GPT์ ์ถ๊ฐ์ค๋ช ๐ฝ
1. ํ๋ก์ ๊ฐ์ฒด ($Proxy69)
- $Proxy69.authenticate๋ @Service๋ @Component์ ๊ฐ์ Spring Bean์ ํธ๋์ญ์ (@Transactional)์ด๋ ๋ณด์(AOP)๊ณผ ๊ฐ์ ๋ถ๊ฐ ๊ธฐ๋ฅ์ด ์ ์ฉ๋ ๋, **Spring์ด ์๋ณธ ๊ฐ์ฒด ๋์ ์์ฑํ์ฌ ์ฃผ์ ํ๋ ๋๋ฆฌ ๊ฐ์ฒด(Proxy)**์ ๋๋ค.
- ๋น์ ์ ์ฝ๋์์๋ AuthenticationManager ๋น์ด ์ด ํ๋ก์ ๊ฐ์ฒด๋ก ์ฃผ์ ๋ ๊ฒ์ ๋๋ค.
2. ํ๋ก์์ invoke
- JdkDynamicAopProxy.invoke๋ ์ด ํ๋ก์ ๊ฐ์ฒด์ ๋ฉ์๋(์ฌ๊ธฐ์๋ authenticate)๊ฐ ํธ์ถ๋ ๋ ํญ์ ๋จผ์ ์คํ๋๋ Spring AOP์ ํต์ฌ ๋ก์ง์ ๋๋ค. ์ด ๋ก์ง์ ๋ถ๊ฐ ๊ธฐ๋ฅ์ ์ฒ๋ฆฌํ ํ ์๋ณธ ๊ฐ์ฒด์ ๋ฉ์๋๋ฅผ ํธ์ถํฉ๋๋ค.
3. ๋ฌดํ ์ฌ๊ท (Stack Overflow)
StackOverflowError๊ฐ ๋ฐ์ํ๋ค๋ ๊ฒ์ ๋ค์๊ณผ ๊ฐ์ ์ํ ํธ์ถ ๊ตฌ์กฐ๊ฐ ๋ฐ์ํ๋ค๋ ์๋ฏธ์ ๋๋ค.
- ์์: AuthService (๋๋ ์ธ๋ถ) $\rightarrow$ ํ๋ก์ AuthenticationManager์ authenticate() ํธ์ถ
- ํ๋ก์: JdkDynamicAopProxy.invoke ์คํ
- ๋ฌธ์ : invoke ๋ก์ง์ด **์๋ณธ AuthenticationManager**๋ฅผ ํธ์ถํ๋ ๋์ , ์ด๋ค ๊ฒฝ๋ก๋ฅผ ํตํด ๋ค์ 1๋ฒ ๋จ๊ณ๋ก ๋์๊ฐ ํ๋ก์ ๊ฐ์ฒด์ authenticate()๋ฅผ ์ฌํธ์ถํฉ๋๋ค.
์ด ๋ฌดํ ๋ฃจํ์ ์์ธ์ ์์ ๋ต๋ณ์์ ์ค๋ช ๋๋ ธ๋ฏ์ด, AuthenticationManager ๋น์ ์ ์ํ๋ ๊ณผ์ (SecurityConfig)๊ณผ ์ด๋ฅผ ์ฌ์ฉํ๋ ์ปค์คํ ์ธ์ฆ ๋ก์ง(AuthenticateHandler ๋๋ AuthService) ๊ฐ์ ์์กด์ฑ ์ฃผ์ (DI) ์ถฉ๋์ด๋ ์ค์ ์ ์ค๋ณต ๋๋ฌธ์ ๋๋ค.
๊ฒฐ๋ก :
์ค๋ฅ ๋ฐ์ ์ง์ ์ AuthenticationManager์ authenticate()์ ํธ์ถ ๊ณผ์
์์ธ์ Spring Security์ ์ค์ ๊ณผ ์ปค์คํ ๋ก์ง ๊ฐ์ ์ํ ์ฐธ์กฐ(Circular Dependency)
- DB์์ ์ง์ ์ฌ์ฉ์์ ๋ณด ์กฐํ(getUserDetails)
- ๋น๋ฐ๋ฒํธ๋ฅผ ์ง์ ๋น๊ต(passwordEncoder.matches)
- authenticatHandler.authenticate(request)๋ฅผ ํตํด ๋ค์ ์ธ์ฆ ์๋
- ์ฆ, ๋ก๊ทธ์ธ ๋ก์ง ์์์ Spring Security์ ์ธ์ฆ์ '์๋' + '์๋'์ผ๋ก ๋์์ ๋๋ฆฌ๋ฉด์ ์ธ์ฆ ๊ณผ์ ์ด ์๊ธฐ ์์ ์ ๊ณ์ ํธ์ถํ์ฌ stackOverflow ๋ฐ์ (๊ธธ์ด์ ธ์ ์ ์๊ธ ์์ฑ)
authenticateHandler.authenticate() ์์์ ๋ฌด์จ ์ผ์ด ์ผ์ด๋ฌ๋?
AuthenticateHandler๋ ๋ด๋ถ์์ ๋ณดํต ์ด๋ฐ ๊ฑธ ํ๊ณ ์์ด:
์ฆ,๐ Spring Security์ ํ์ค ์ธ์ฆ ํ๋ก์ธ์ค(AuthenticationManager) ๋ฅผ ๋ค์ ํธ์ถํจ
Spring Security๋ ์ด ์ธ์ฆ์ ์ด๋ป๊ฒ ์ฒ๋ฆฌํ๋?
Spring Security๋ ์ธ์ฆ์ ์์ํ๋ฉด:
- AuthenticationManager ์คํ
- AuthenticationProvider ํธ์ถ
- UserDetailsService.loadUserByUsername() ํธ์ถ
- (AOP / Filter / Security ์ค์ ์ ๋ฐ๋ผ)
- ์ปค์คํ ๋ก๊ทธ์ธ ๋ก์ง(AuthService.login())์ ๋ค์ ํ๊ฒ ๋จ
๊ทธ๋์ ๋ฌด์จ ์ผ์ด ๋ฒ์ด์ก๋? (๋ฌดํ ๋ฃจํ)
AuthService.login()
โ authenticateHandler.authenticate()
โ AuthenticationManager.authenticate()
โ Spring Security ์ธ์ฆ ์์
โ AuthService.login() ๋ค์ ํธ์ถ
โ authenticateHandler.authenticate()
โ AuthenticationManager.authenticate()
โ ...
๐ข ๋์๋ ํ์ ๋ชฉ๋ง
โก๏ธ StackOverflowError ๋ฐ์
๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์ AuthenticationManager์ ์ฌ์ฉํ์ง ์๊ณ , ์ด๋ฏธ ์ฝ๋์์ ์ง์ ๋น๋ฐ๋ฒํธ ๊ฒ์ฆ์ ํ๊ณ ์๊ธฐ๋๋ฌธ์ ์ธ์ฆ ๊ฐ์ฒด๋ฅผ ์๋์ผ๋ก ์์ฑํ๋ผ๊ณ ํ๋๋ฐ...
public class AuthService {
private final JdbcTemplate jdbcTemplate;
private final AuthenticateHandler authenticateHandler;
private final JwtProvider jwtProvider;
public TokenDTO login(LoginRequestDTO request) {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
UserDetailsImpl userDetails = getUserDetails(request);
if (!passwordEncoder.matches(request.getPassword(), userDetails.getPassword())) {
// ๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์๋ค๋ฉด
throw new BadCredentialsException("๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์!(T_T)");
}
Authentication authentication = authenticateHandler.authenticate(request);
String accessToken = jwtProvider.createAccessToken(authentication);
return new TokenDTO(accessToken);
}
๊ทผ๋ฐ ๋ ๊ณต๋ถ์ค์ด๋ฏ๋ก AuthenticationManager์ ์ฌ์ฉํด๋ณด๊ณ ์ถ๋ค.
1๏ธโฃ UserDetailsService ๊ตฌํ ํด๋์ค ์์ฑ
@Service
@RequiredArgsConstructor
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {
public final JdbcTemplate jdbcTemplate;
@Override
public UserDetails loadUserByUsername(String userId) {
String sql = "SELECT * FROM dalmuri.\"USER\" WHERE \"USER_ID\" = ?"; // ์ฌ๊ธฐ ์๋ฌ๋ ๋ฌด์ ๊ฐ๋ฅ
UserDetailsImplDTO userImpl = new UserDetailsImplDTO();
try {
UserDTO user = jdbcTemplate.queryForObject(
sql,
new Object[]{userId},
(rs, rowNum) -> { // Mapper
UserDTO u = new UserDTO();
u.setUserCd(rs.getString("USER_CD"));
u.setUserId(rs.getString("USER_ID"));
u.setUserNm(rs.getString("USER_NM"));
u.setPassword(rs.getString("PASSWORD"));
u.setBgColor(rs.getString("BG_COLOR"));
u.setTextColor(rs.getString("TEXT_COLOR"));
u.setInsertDatetime(rs.getString("INSERT_DATETIME"));
return u;
}
);
if (user == null) {
throw new UsernameNotFoundException("์ฌ์ฉ์ ์ ๋ณด ์์: " + userId);
}
userImpl.setUserCd(user.getUserCd());
userImpl.setUserId(user.getUserId());
userImpl.setUserNm(user.getUserNm());
userImpl.setPassword(user.getPassword());
userImpl.setBgColor(user.getBgColor());
userImpl.setTextColor(user.getTextColor());
userImpl.setInsertDatetime(user.getInsertDatetime());
return userImpl;
} catch (Exception e) {
e.printStackTrace();
throw new UsernameNotFoundException("์ฌ์ฉ์ ์ ๋ณด ์์: " + userId);
}
}
}
๊ทธ๋ฆฌ๊ณ ๊ธฐ์กด AuthService.java์ ์๋ DBํธ์ถ๋ก์ง์ ์ญ์
// private UserDetailsImpl getUserDetails (LoginRequestDTO request) {
// String sql = "SELECT * FROM dalmuri.\"USER\" WHERE \"USER_ID\" = ?"; // ์ฌ๊ธฐ ์๋ฌ๋ ๋ฌด์ ๊ฐ๋ฅ
//
// User user = jdbcTemplate.queryForObject(
// sql,
// new Object[]{request.getUserId()},
// (rs, rowNum) -> { // Mapper
// User u = new User();
// u.setUserCd(rs.getString("USER_CD"));
// u.setUserId(rs.getString("USER_ID"));
// u.setUserNm(rs.getString("USER_NM"));
// u.setPassword(rs.getString("PASSWORD"));
// u.setBgColor(rs.getString("BG_COLOR"));
// u.setTextColor(rs.getString("TEXT_COLOR"));
// u.setInsertDatetime(rs.getString("INSERT_DATETIME"));
// return u;
// }
// );
//
// if (user == null) {
// throw new UsernameNotFoundException("์ฌ์ฉ์ ์ ๋ณด ์์: " + request.getUserId());
// }
//
// return new UserDetailsImpl(user);
// }
์ ์ญ์ ํ๋๋ฉด, AuthService์์ ๊ฐ์ด ์์ฑํ๊ธฐ ๋๋ฌธ์ getUserDetails ๋ฉ์๋๋ UserDetailsService ์ธํฐํ์ด์ค๋ฅผ ์์๋ฐ์ง ์๋๋ค(AuthService๊ฐ UserDetailsService๋ฅผ ์์๋ฐ์ง ์์)
2๏ธโฃ User์ UserDetails๋ก ๋ณํ
@Getter
@Setter
@ToString
public class UserDetailsDTO implements UserDetails {
private String userCd;
private String userId;
private String userNm;
private String password;
private String bgColor;
private String textColor;
private String insertDatetime;
/*
* 1. Lombok ์ด๋
ธํ
์ด์
์ ์ฌ์ฉํด๋ UserDetails์์ ์๊ตฌํ๋ ์ธํฐํ์ด์ค๋ ์ง์ ์์ฑํด์ผ ํ๋ค.
* */
private final String role = "ROLE_USER";
@Override
public String getUsername() {
return userId;
}
@Override
public String getPassword() {
return password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// ๊ณ ์ ๋ฐํ
return List.of(new SimpleGrantedAuthority(this.role));
}
/**
* ํ์์ ์ธ ์์๋ค : true๋ก ๊ณ ์
* */
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
๊ธฐ์กด์ ๋ง๋ค์๋ UserDTO
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO {
private String userCd;
private String userId;
private String userNm;
private String password;
// ์์ผ๋ ์์ผ๋
private String bgColor;
private String textColor;
private String insertDatetime;
}
3๏ธโฃ AuthService.java ๋ก์ง ์์
๊ธฐ์กด ๋ก์ง
public class AuthService {
private final JdbcTemplate jdbcTemplate;
private final AuthenticateHandler authenticateHandler;
private final JwtProvider jwtProvider;
public TokenDTO login(LoginRequestDTO request) {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
UserDetailsImpl userDetails = getUserDetails(request);
if (!passwordEncoder.matches(request.getPassword(), userDetails.getPassword())) {
// ๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์๋ค๋ฉด
throw new BadCredentialsException("๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์!(T_T)");
}
Authentication authentication = authenticateHandler.authenticate(request);
String accessToken = jwtProvider.createAccessToken(authentication);
return new TokenDTO(accessToken);
}
private UserDetailsImpl getUserDetails (LoginRequestDTO request) {
String sql = "SELECT * FROM dalmuri.\"USER\" WHERE \"USER_ID\" = ?"; // ์ฌ๊ธฐ ์๋ฌ๋ ๋ฌด์ ๊ฐ๋ฅ
User user = jdbcTemplate.queryForObject(
sql,
new Object[]{request.getUserId()},
(rs, rowNum) -> { // Mapper
User u = new User();
u.setUserCd(rs.getString("USER_CD"));
u.setUserId(rs.getString("USER_ID"));
u.setUserNm(rs.getString("USER_NM"));
u.setPassword(rs.getString("PASSWORD"));
u.setBgColor(rs.getString("BG_COLOR"));
u.setTextColor(rs.getString("TEXT_COLOR"));
u.setInsertDatetime(rs.getString("INSERT_DATETIME"));
return u;
}
);
if (user == null) {
throw new UsernameNotFoundException("์ฌ์ฉ์ ์ ๋ณด ์์: " + request.getUserId());
}
return new UserDetailsImpl(user);
}
}
์๋ก ๊ณ ์น ๋ก์ง
@Service
@RequiredArgsConstructor
@Slf4j
public class AuthService {
private final AuthenticateHandler authenticateHandler;
private final JwtProvider jwtProvider;
private final UserDetailsServiceImpl userDetailsServiceImpl;
public TokenDTO login(LoginRequestDTO request) {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
UserDetailsDTO userDetails = (UserDetailsDTO) userDetailsServiceImpl.loadUserByUsername(request.getUserId());
if (!passwordEncoder.matches(request.getPassword(), userDetails.getPassword())) {
// ๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์๋ค๋ฉด
throw new BadCredentialsException("๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ์ง ์์!(T_T)");
}
Authentication authentication = authenticateHandler.authenticate(request);
String accessToken = jwtProvider.createAccessToken(authentication);
return new TokenDTO(accessToken);
}
[์ฐธ๊ณ ์๋ฃ]
https://velog.io/@corgi/Spring-Security-PasswordEncoder%EB%9E%80-4kkyw8gi
[Spring Security] PasswordEncoder๋?
Spring Security 5.3.3 ๋ฒ์ ์์ ์ง์ํ๋ PasswordEncoder์ ๋ํด ๊ฐ๋ตํ ์์๋ด ๋๋ค.
velog.io
https://coding-start.tistory.com/153
Spring boot - Spring Security(์คํ๋ง ์ํ๋ฆฌํฐ) ๋? ์์ ํด๊ฒฐ!
์ค๋ ํฌ์คํ ํ ๋ด์ฉ์ Spring Security์ด๋ค. ์ฌ์ค ํ์๋ ๋จธ๋ฆฌ๊ฐ ๋๋น ์ ๊ทธ๋ฐ์ง ๋ชจ๋ฅด๊ฒ ์ง๋ง, ์๋ฌด๋ฆฌ ๊ตฌ๊ธ๋ง์ ํตํด ์คํ๋ง ์ํ๋ฆฌํฐ๋ฅผ ๊ฒ์ํด๋ ์ด๋ ๋คํ ๋ช ์พํ ํด๋ต์ ์ป์ง ๋ชปํ๋ค. ๋๋ถ๋ถ ์ด๋ก ์
coding-start.tistory.com

'Backend๐ฅ๏ธ > Javaโ(Spring๐)' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| ๋๋ค์์ด๋? (0) | 2026.01.29 |
|---|---|
| [Spring] JWT ์ด์ฉํด์ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ๋ง๋ค๊ธฐ - ํํฐ ์์ฑ (0) | 2026.01.07 |
| [Spring] JWT ์ด์ฉํด์ ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ๋ง๋ค๊ธฐ - ์ฌ์ฉ์ ๊ฒ์ฆ (1) | 2025.12.31 |
| [Spring] Spring Security ๋ง๋ณด๊ธฐ (0) | 2025.12.11 |
| [Spring] Spring Security - application.yml๋ก URL๊ถํ์ ๋์ ์ผ๋ก ๊ด๋ฆฌํ๊ธฐ (0) | 2025.10.20 |