Spring Boot 3์ Spring Security 6.x๊ฐ ์ ์ ๋ฆด๋ฆฌ์ค๋๋ฉด์ JWT ๊ธฐ๋ฐ ์ธ์ฆ ๊ตฌํ ๋ฐฉ์๋ ์๋นํ ๋ฌ๋ผ์ก๋ค. ๊ธฐ์กด WebSecurityConfigurerAdapter๋ฅผ ์์ํ๋ ๋ฐฉ์์ ์์ ํ ์ ๊ฑฐ๋์๊ณ , ๋๋ค DSL ๋ฐฉ์์ SecurityFilterChain ๋น ๋ฑ๋ก์ผ๋ก ์ ํ๋์๋ค. ์ด๋ฒ ํฌ์คํ
์์๋ Spring Boot 3์ Spring Security 6.x ํ๊ฒฝ์์ Spring Security JWT ์ธ์ฆ์ ์์กด์ฑ ์ถ๊ฐ๋ถํฐ JwtService, JwtAuthenticationFilter, ํ์๊ฐ์
ยท๋ก๊ทธ์ธ REST API๊น์ง ์ค์ ๋์ํ๋ ์ฝ๋ ๊ธฐ์ค์ผ๋ก ๊ตฌํํ๋ค.
ํ ํฐ์ด ์์ฒญ์ ํ๊ณ ํ๋ฅด๋ ๋ฐฉ์
์ฝ๋๋ฅผ ๋ณด๊ธฐ ์ ์ ์์ฒญ์ด ์ด๋ค ์์๋ก ์ฒ๋ฆฌ๋๋์ง ์ง๊ณ ๋์ด๊ฐ์.

- ํด๋ผ์ด์ธํธ๊ฐ
/api/v1/auth/login์๋ํฌ์ธํธ๋ก ์ด๋ฉ์ผ๊ณผ ๋น๋ฐ๋ฒํธ๋ฅผ ์ ์กํ๋ค. - ์๋ฒ๋
AuthenticationManager๋ฅผ ํตํด ์๊ฒฉ ์ฆ๋ช ์ ๊ฒ์ฆํ๊ณ , ์ฑ๊ณต ์ JWT ์ก์ธ์ค ํ ํฐ์ ์๋ต์ผ๋ก ๋ฐํํ๋ค. - ์ดํ ํด๋ผ์ด์ธํธ๋ ๋ณดํธ๋ API๋ฅผ ํธ์ถํ ๋๋ง๋ค HTTP ํค๋์
Authorization: Bearer {token}์ ํฌํจํ์ฌ ์์ฒญํ๋ค. JwtAuthenticationFilter๊ฐ ๋ชจ๋ ์์ฒญ์ ์์ ์คํ๋์ด ํ ํฐ์ ์๋ช ๊ณผ ๋ง๋ฃ ์ฌ๋ถ๋ฅผ ๊ฒ์ฆํ๋ค.- ๊ฒ์ฆ์ ์ฑ๊ณตํ๋ฉด
SecurityContextHolder์ ์ธ์ฆ ์ ๋ณด๋ฅผ ์ ์ฅํ๊ณ ์ดํ ํํฐ ์ฒด์ธ๊ณผ ์ปจํธ๋กค๋ฌ๊ฐ ์ ์ ๋์ํ๋ค.
์ด ํ๋ฆ์ ๊ธฐ์ตํด๋๋ฉด ์๋์์ ์ค๋ช ํ ๊ฐ ์ปดํฌ๋ํธ์ ์ญํ ์ด ํจ์ฌ ์ ๋ช ํ๊ฒ ๋ค์ด์จ๋ค.
1. pom.xml์ ๋ฌด์์ ๋ฃ์ด์ผ ํ๋
Spring Boot 3.x ๊ธฐ๋ฐ ํ๋ก์ ํธ์ ์๋ ์์กด์ฑ์ ์ถ๊ฐํ๋ค. JWT ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ JJWT 0.11.5 ๊ธฐ์ค์ผ๋ก ์์ฑํ๋ค. 0.12.x๋ API ๋ฉ์๋๋ช ์ด ์ผ๋ถ ๋ณ๊ฒฝ๋์์ผ๋ฏ๋ก ๋ฒ์ ์ ์ ํํ ํ์ธํ๊ณ ์ฌ์ฉํด์ผ ํ๋ค.
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- JJWT 0.11.5 โ JWT ์์ฑ ๋ฐ ํ์ฑ ๋ผ์ด๋ธ๋ฌ๋ฆฌ -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>XMLjjwt-api๋ ์ปดํ์ผ ํ์ ์์กด์ฑ์ผ๋ก, ์ค์ ๊ตฌํ์ฒด์ธ jjwt-impl์ ๋ฐํ์ scope์ผ๋ก ๋ถ๋ฆฌํ๋ ๊ฒ์ด JJWT ๊ณต์ ๊ฐ์ด๋์ ๊ถ์ฅ ๋ฐฉ์์ด๋ค. jjwt-jackson์ JWT์ JSON ์ง๋ ฌํ๋ฅผ ๋ด๋นํ๋ฏ๋ก ํจ๊ป ์ถ๊ฐํด์ผ ํ๋ค.
2. ์ํฌ๋ฆฟ ํค๋ ์ฝ๋ ๋ฐ์ผ๋ก
JWT ์ํฌ๋ฆฟ ํค์ ํ ํฐ ๋ง๋ฃ ์๊ฐ์ ์ธ๋ถ ์ค์ ์ผ๋ก ๋ถ๋ฆฌํ๋ค. ์ํฌ๋ฆฟ ํค๋ ๋ฐ๋์ 256๋นํธ(32๋ฐ์ดํธ) ์ด์์ Base64 ์ธ์ฝ๋ฉ ๋ฌธ์์ด์ ์ฌ์ฉํด์ผ HS256 ์๊ณ ๋ฆฌ์ฆ ์๊ตฌ์ฌํญ์ ๋ง์กฑํ๋ค.
spring:
datasource:
url: jdbc:mysql://localhost:3306/jwt_demo
username: root
password: password
jpa:
hibernate:
ddl-auto: update
show-sql: true
application:
security:
jwt:
# 256๋นํธ ์ด์์ Base64 ์ธ์ฝ๋ฉ ์ํฌ๋ฆฟ ํค (์ด์ํ๊ฒฝ์์๋ ํ๊ฒฝ๋ณ์๋ก ์ฃผ์
)
secret-key: <secret-key>
expiration: 86400000 # ์ก์ธ์ค ํ ํฐ ๋ง๋ฃ: 24์๊ฐ (๋ฐ๋ฆฌ์ด)
refresh-token:
expiration: 604800000 # ๋ฆฌํ๋ ์ ํ ํฐ ๋ง๋ฃ: 7์ผ (๋ฐ๋ฆฌ์ด)YAMLsecret-key ๊ฐ์ ์ฝ๋์ ํ๋์ฝ๋ฉํ๋ฉด ์ ๋ ์ ๋๋ค. ์ด์ ํ๊ฒฝ์์๋ ํ๊ฒฝ๋ณ์ APPLICATION_SECURITY_JWT_SECRET_KEY๋ก ์ฃผ์
ํ๊ณ .gitignore์ ์ค์ ์ค์ ํ์ผ์ ์ ์ธํด์ผ ํ๋ค.
3. WebSecurityConfigurerAdapter๋ ์๋ค โ SecurityFilterChain์ผ๋ก ์ ํ
Spring Security 6.x์์ ๋ณด์ ์ค์ ์ ์ง์
์ ์ SecurityFilterChain ๋น์ด๋ค. WebSecurityConfigurerAdapter๋ ์์ ํ ์ฌ๋ผ์ก์ผ๋ฏ๋ก ์๋ ํจํด์ผ๋ก ์์ฑํ๋ค.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// JWT ๊ธฐ๋ฐ Stateless ๋ฐฉ์์์๋ CSRF ๋ณดํธ๊ฐ ๋ถํ์
.csrf(csrf -> csrf.disable())
.cors(Customizer.withDefaults())
.authorizeHttpRequests(auth -> auth
// ์ธ์ฆ ์์ด ์ ๊ทผ ๊ฐ๋ฅํ ๊ฒฝ๋ก ํ์ฉ
.requestMatchers("/api/v1/auth/**").permitAll()
// ๊ด๋ฆฌ์ ์ ์ฉ ๊ฒฝ๋ก โ getAuthorities()์์ "ADMIN"์ ์ง์ ๋ฐํํ๋ฏ๋ก hasAuthority() ์ฌ์ฉ
.requestMatchers("/api/v1/admin/**").hasAuthority("ADMIN")
// ๊ทธ ์ธ ๋ชจ๋ ์์ฒญ์ ์ธ์ฆ ํ์
.anyRequest().authenticated()
)
// ์ธ์
์ ์์ฑํ์ง ์๋ Stateless ์ ์ฑ
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authenticationProvider(authenticationProvider)
// UsernamePasswordAuthenticationFilter ์์ JWT ํํฐ ์ฝ์
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}Java.csrf(csrf -> csrf.disable())๋ JWT Stateless ๋ฐฉ์์์ ์ธ์
์ฟ ํค๋ฅผ ์ฌ์ฉํ์ง ์๊ธฐ ๋๋ฌธ์ CSRF ๊ณต๊ฒฉ ๋ฒกํฐ๊ฐ ์กด์ฌํ์ง ์์ ๋นํ์ฑํํ๋ค. .sessionManagement()์์ STATELESS๋ฅผ ์ง์ ํ๋ฉด Spring Security๊ฐ ์ธ์
์ ์์ฑํ๊ฑฐ๋ ์ฌ์ฉํ์ง ์์ผ๋ฏ๋ก ์๋ฒ๊ฐ ์ด๋ ํ ์ธ์ฆ ์ํ๋ ๋ฉ๋ชจ๋ฆฌ์ ์ ์งํ์ง ์๋๋ค. addFilterBefore()๋ก JwtAuthenticationFilter๋ฅผ ๊ธฐ๋ณธ ์ธ์ฆ ํํฐ๋ณด๋ค ์์ ๋ฑ๋กํ์ฌ ๋ชจ๋ ์์ฒญ์ ํ ํฐ์ ๋จผ์ ๊ฒ์ฆํ๋ค.

4. ํ ํฐ์ ๋ฐ๊ธํ๊ณ ์ฝ๋ ํต์ฌ ๋ก์ง
JwtService๋ JWT ํ ํฐ ์์ฑ, ํ์ฑ, ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ๋ด๋นํ๋ค. JJWT 0.11.5 ๊ธฐ์ค ์ ์ฒด ๊ตฌํ์ด๋ค.
@Service
public class JwtService {
@Value("${application.security.jwt.secret-key}")
private String secretKey;
@Value("${application.security.jwt.expiration}")
private long jwtExpiration;
/** UserDetails๋ง์ผ๋ก ๊ธฐ๋ณธ ํ ํฐ ์์ฑ */
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
/** ์ถ๊ฐ ํด๋ ์์ ํฌํจํ ํ ํฐ ์์ฑ (์: userId, roles) */
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return buildToken(extraClaims, userDetails, jwtExpiration);
}
private String buildToken(
Map<String, Object> extraClaims,
UserDetails userDetails,
long expiration
) {
return Jwts.builder()
.setClaims(extraClaims)
// subject์ ์ฌ์ฉ์ ์๋ณ์(์ด๋ฉ์ผ) ์ ์ฅ
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
// ํ์ฌ ์๊ฐ + ๋ง๋ฃ ์๊ฐ์ผ๋ก exp ํด๋ ์ ์ค์
.setExpiration(new Date(System.currentTimeMillis() + expiration))
// HMAC-SHA256 ์๊ณ ๋ฆฌ์ฆ์ผ๋ก ์๋ช
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}
/** ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฆ โ ์ฌ์ฉ์๋ช
์ผ์น + ๋ง๋ฃ ์ฌ๋ถ ํ์ธ */
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private boolean isTokenExpired(String token) {
return extractClaim(token, Claims::getExpiration).before(new Date());
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSignInKey())
.build()
// ์๋ช
๊ฒ์ฆ ํ ํ์ด๋ก๋(Claims) ๋ฐํ
.parseClaimsJws(token)
.getBody();
}
private Key getSignInKey() {
// Base64 ๋์ฝ๋ฉํ์ฌ HMAC ํค ์์ฑ
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
}JavabuildToken() ๋ฉ์๋๋ Jwts.builder()๋ฅผ ํตํด ํค๋ยทํ์ด๋ก๋ยท์๋ช
์ธ ๋ถ๋ถ์ผ๋ก ๊ตฌ์ฑ๋ JWT๋ฅผ ์์ฑํ๊ณ , compact()๋ก ์ต์ข
๋ฌธ์์ด์ ๋ฐํํ๋ค. extractAllClaims()์์ parseClaimsJws()๋ฅผ ํธ์ถํ๋ฉด JJWT๊ฐ ๋ด๋ถ์ ์ผ๋ก ์๋ช
์ ๊ฒ์ฆํ๋ฉฐ, ์๋ช
์ด ์ฌ๋ฐ๋ฅด์ง ์๊ฑฐ๋ ํ ํฐ ํ์์ด ์๋ชป๋ ๊ฒฝ์ฐ JwtException์ ๋์ง๋ค.
5. ๋ชจ๋ ์์ฒญ ์์ ์๋ ํํฐ
๋ชจ๋ HTTP ์์ฒญ์์ Authorization ํค๋๋ฅผ ํ์ธํ๊ณ JWT๋ฅผ ๊ฒ์ฆํ๋ ํํฐ์ด๋ค. OncePerRequestFilter๋ฅผ ์์ํ๋ฉด ํ๋์ ์์ฒญ์์ ํํฐ๊ฐ ๋จ ํ ๋ฒ๋ง ์คํ๋จ์ ๋ณด์ฅํ๋ค.

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
// Authorization ํค๋๊ฐ ์๊ฑฐ๋ Bearer ์คํด์ด ์๋ ๊ฒฝ์ฐ ๊ฑด๋๋
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
// "Bearer " ์ดํ 7์๋ฆฌ๋ถํฐ ์ค์ ํ ํฐ ๋ฌธ์์ด ์ถ์ถ
final String jwt = authHeader.substring(7);
final String userEmail;
try {
userEmail = jwtService.extractUsername(jwt);
} catch (JwtException e) {
// ๋ง๋ฃยท๋ณ์กฐยทํ์ ์ค๋ฅ ํ ํฐ: ์ธ์ฆ ์์ด ๋ค์ ํํฐ๋ก ์ ๋ฌ โ Spring Security๊ฐ 401 ์ฒ๋ฆฌ
filterChain.doFilter(request, response);
return;
}
// ์ฌ์ฉ์๋ช
์ด ์กด์ฌํ๊ณ ์์ง SecurityContext์ ์ธ์ฆ์ด ์๋ ๊ฒฝ์ฐ์๋ง ์ฒ๋ฆฌ
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
if (jwtService.isTokenValid(jwt, userDetails)) {
// ์ธ์ฆ ํ ํฐ ์์ฑ โ credentials๋ null (JWT ๋ฐฉ์์์๋ ๋ถํ์)
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
// SecurityContext์ ์ธ์ฆ ์ ๋ณด ๋ฑ๋ก โ ์ดํ ์ปจํธ๋กค๋ฌ์์ @AuthenticationPrincipal๋ก ์ ๊ทผ ๊ฐ๋ฅ
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}JavaSecurityContextHolder.getContext().getAuthentication() == null ์กฐ๊ฑด์ ๋ฐ๋์ ํ์ธํด์ผ ํ๋ค. ์ด ์กฐ๊ฑด์ด ์์ผ๋ฉด ์ด๋ฏธ ์ธ์ฆ๋ ์์ฒญ์์๋ ๋ถํ์ํ๊ฒ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์กฐํ๊ฐ ๋ฐ์ํ๋ค. UsernamePasswordAuthenticationToken์ ์ธ ๋ฒ์งธ ์ธ์์ธ authorities๋ฅผ ์ ๋ฌํ๋ ๊ฒ์ด ์ค์ํ๋ฉฐ, ์ด๋ฅผ ์๋ตํ๋ฉด isAuthenticated()๊ฐ false๋ก ์ฒ๋ฆฌ๋๋ค.

6. ์คํ๋ง์ด ์ธ์ฆ ํ์ดํ๋ผ์ธ์ ์กฐ๋ฆฝํ๋ ๋ฐฉ๋ฒ
UserDetailsService, AuthenticationProvider, PasswordEncoder๋ฅผ ๋น์ผ๋ก ๋ฑ๋กํ๋ ์ค์ ํด๋์ค์ด๋ค.
@Configuration
@RequiredArgsConstructor
public class ApplicationConfig {
private final UserRepository repository;
@Bean
public UserDetailsService userDetailsService() {
// ์ด๋ฉ์ผ(username)๋ก ์ฌ์ฉ์๋ฅผ ์กฐํํ๋ ๋๋ค ๊ตฌํ
return username -> repository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค: " + username));
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
// BCrypt ํด์ ๋น๊ต๋ฅผ ํตํ ๋น๋ฐ๋ฒํธ ๊ฒ์ฆ
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
// BCrypt strength ๊ธฐ๋ณธ๊ฐ 10 โ ์ด์ ํ๊ฒฝ์์ ์ถฉ๋ถํ ํด์ ๊ฐ๋
return new BCryptPasswordEncoder();
}
}JavaDaoAuthenticationProvider๋ UserDetailsService๋ก ์ฌ์ฉ์๋ฅผ ์กฐํํ ๋ค PasswordEncoder๋ก ์
๋ ฅ๋ ๋น๋ฐ๋ฒํธ๋ฅผ ๊ฒ์ฆํ๋ Spring Security ๊ธฐ๋ณธ ์ธ์ฆ ์ ๊ณต์์ด๋ค. AuthenticationManager๋ AuthService์์ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ ์ ์ง์ ํธ์ถํ๋ฏ๋ก ๋น์ผ๋ก ๋
ธ์ถํด์ผ ํ๋ค.
7. ์ํฐํฐ๊ฐ UserDetails๋ฅผ ์ง์ ๊ตฌํํ๋ฉด ์๊ธฐ๋ ์ผ
JPA ์ํฐํฐ์์ UserDetails๋ฅผ ์ง์ ๊ตฌํํ๋ค.
@Entity
@Table(name = "users")
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = "id") // JPA ํ๋ก์ ์์ โ id ๊ธฐ์ค์ผ๋ก๋ง ๋๋ฑ์ฑ ๋น๊ต
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstname;
private String lastname;
@Column(unique = true, nullable = false)
private String email;
private String password;
@Enumerated(EnumType.STRING)
private Role role;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// Role enum ๊ฐ์ GrantedAuthority๋ก ๋ณํ
return List.of(new SimpleGrantedAuthority(role.name()));
}
@Override
public String getUsername() {
// Spring Security์ username ๊ธฐ์ค์ผ๋ก ์ด๋ฉ์ผ ๋ฐํ
return email;
}
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
@Override
public boolean isEnabled() { return true; }
}Java์ํฐํฐ๊ฐ UserDetails๋ฅผ ์ง์ ๊ตฌํํ๋ฉด ๋ณ๋์ UserDetailsImpl ๋ํผ ํด๋์ค๊ฐ ํ์ ์์ด ์ฝ๋๊ฐ ๋จ์ํด์ง๋ค. getUsername()์ด ์ด๋ฉ์ผ์ ๋ฐํํ๋๋ก ์ค๋ฒ๋ผ์ด๋ํ๋ ๊ฒ์ด ํต์ฌ์ด๋ฉฐ, UserDetailsService.loadUserByUsername()๊ณผ JwtService.extractUsername()์ ๊ธฐ์ค ์๋ณ์๊ฐ ์ผ์นํด์ผ ํ๋ค.
8. ํ์๊ฐ์ ๋ถํฐ ํ ํฐ ๋ฐ๊ธ๊น์ง
AuthController์ AuthService๋ก ์ธ์ฆ ์๋ํฌ์ธํธ๋ฅผ ๊ตฌ์ฑํ๋ค. ๊ตฌ์กฐ๋ ๋จ์ํ๋ค.
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService service;
@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@RequestBody RegisterRequest request) {
return ResponseEntity.ok(service.register(request));
}
@PostMapping("/login")
public ResponseEntity<AuthResponse> authenticate(@RequestBody AuthRequest request) {
return ResponseEntity.ok(service.authenticate(request));
}
}Java์ ์ปจํธ๋กค๋ฌ๋ SecurityConfig์์ /api/v1/auth/**๋ฅผ permitAll()๋ก ํ์ฉํ์ผ๋ฏ๋ก ์ธ์ฆ ์์ด ์ ๊ทผ๋๋ค. ๋ ์๋ํฌ์ธํธ ๋ชจ๋ ์ฑ๊ณต ์ JWT ํ ํฐ์ด ๋ด๊ธด AuthResponse๋ฅผ ๋ฐํํ๋ค.
@Service
@RequiredArgsConstructor
public class AuthService {
private final UserRepository repository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
private final AuthenticationManager authenticationManager;
public AuthResponse register(RegisterRequest request) {
// ์ด๋ฉ์ผ ์ค๋ณต ๊ฒ์ฌ โ 409 Conflict ์๋ต์ ์ํด ์ ์ฅ ์ ํ์ธ
if (repository.findByEmail(request.getEmail()).isPresent()) {
throw new IllegalArgumentException("์ด๋ฏธ ์ฌ์ฉ ์ค์ธ ์ด๋ฉ์ผ์
๋๋ค: " + request.getEmail());
}
var user = User.builder()
.firstname(request.getFirstname())
.lastname(request.getLastname())
.email(request.getEmail())
// ๋น๋ฐ๋ฒํธ๋ ๋ฐ๋์ BCrypt ํด์ฑ ํ ์ ์ฅ
.password(passwordEncoder.encode(request.getPassword()))
.role(Role.USER)
.build();
repository.save(user);
// ๋ฑ๋ก ์ฆ์ JWT ๋ฐ๊ธ โ ๋ณ๋ ๋ก๊ทธ์ธ ๋ถํ์
var jwtToken = jwtService.generateToken(user);
return AuthResponse.builder().token(jwtToken).build();
}
public AuthResponse authenticate(AuthRequest request) {
// ์๊ฒฉ ์ฆ๋ช
๊ฒ์ฆ ์คํจ ์ BadCredentialsException ๋ฐ์
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(),
request.getPassword()
)
);
var user = repository.findByEmail(request.getEmail())
.orElseThrow(() -> new UsernameNotFoundException("์ธ์ฆ ํ ์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค: " + request.getEmail()));
var jwtToken = jwtService.generateToken(user);
return AuthResponse.builder().token(jwtToken).build();
}
}Javaauthenticate() ๋ฉ์๋์์ authenticationManager.authenticate()๊ฐ ์ฑ๊ณตํ๋ฉด Spring Security๊ฐ ๋ด๋ถ์ ์ผ๋ก UserDetailsService๋ฅผ ํธ์ถํ๊ณ PasswordEncoder๋ก ๋น๋ฐ๋ฒํธ๋ฅผ ๊ฒ์ฆํ๋ค. ๊ฒ์ฆ ์คํจ ์ BadCredentialsException์ด ๋ฐ์ํ๋ฏ๋ก ์ปจํธ๋กค๋ฌ ๋ ๋ฒจ์ @ExceptionHandler๋ก 401 ์๋ต์ ๋ฐํํ๋๋ก ์ฒ๋ฆฌํ๋ ๊ฒ์ด ์ข๋ค.

9. JJWT 0.12.x๋ก ์ ๊ทธ๋ ์ด๋ํ๋ฉด ์ฝ๋๊ฐ ์ด๋ ๊ฒ ๋ฐ๋๋ค
์ค๋ฌด์์ ๋ ๋ฒ์ ์ด ํผ์ฉ๋๋ฏ๋ก ์ฃผ์ API ์ฐจ์ด๋ฅผ ์ ๋ฆฌํ๋ค.
| ๊ธฐ๋ฅ | JJWT 0.11.x | JJWT 0.12.x |
|---|---|---|
| ๋น๋ ์์ | Jwts.builder() | Jwts.builder() (๋์ผ) |
| ํด๋ ์ ์ค์ | .setClaims(map) | .claims(map) |
| Subject ์ค์ | .setSubject(str) | .subject(str) |
| ๋ฐํ ์๊ฐ | .setIssuedAt(date) | .issuedAt(date) |
| ๋ง๋ฃ ์๊ฐ | .setExpiration(date) | .expiration(date) |
| ์๋ช | .signWith(key, algo) | .signWith(key) |
| ํ์ ์์ฑ | Jwts.parserBuilder() | Jwts.parser() |
| ํด๋ ์ ํ์ฑ | .parseClaimsJws(token).getBody() | .parseSignedClaims(token).getPayload() |
0.12.x์์ ๋ฉ์๋ ์ด๋ฆ์ด setter ํ์์์ ๋น๋ ํ์์ผ๋ก ํต์ผ๋๊ณ , ์๋ช ์ ์๊ณ ๋ฆฌ์ฆ ํ๋ผ๋ฏธํฐ๊ฐ ์ ๊ฑฐ๋์๋ค. ๋ ๋ฒ์ ์ API๋ฅผ ํผ์ฉํ๋ฉด ์ปดํ์ผ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ฏ๋ก ๋ฒ์ ์ ๋ฐ๋์ ๊ณ ์ ํด์ผ ํ๋ค.
10. Spring Security ๋ญ๊ฐ ๋ฌ๋ผ์ก๋
Spring Security JWT ์ธ์ฆ ๊ตฌํ ์ ๋ง์ด๊ทธ๋ ์ด์ ์์ ๊ฐ์ฅ ํผ์ ์ด ์๊ธฐ๋ ๋ณ๊ฒฝ์ฌํญ์ ์ ๋ฆฌํ๋ค.

| ํญ๋ชฉ | Spring Security 5.x | Spring Security 6.x |
|---|---|---|
| ์ค์ ๋ฐฉ์ | extends WebSecurityConfigurerAdapter | @Bean SecurityFilterChain |
| ์ธ๊ฐ ๋ฉ์๋ | .authorizeRequests() | .authorizeHttpRequests() |
| ๊ฒฝ๋ก ๋งค์ฒ | .antMatchers() | .requestMatchers() |
| DSL ์ฐ๊ฒฐ | .and() ์ฒด์ด๋ | ๋๋ค DSL (.and() ์ ๊ฑฐ๋จ) |
| CSRF ๋นํ์ฑํ | .csrf().disable() | .csrf(c -> c.disable()) |
| ๋ฉ์๋ ๋ณด์ ํ์ฑํ | @EnableGlobalMethodSecurity | @EnableMethodSecurity |
| ์ต์ Java ๋ฒ์ | Java 8 | Java 17 |
Spring ๊ณต์ ๋ง์ด๊ทธ๋ ์ด์
๊ฐ์ด๋์ ๋ฐ๋ฅด๋ฉด authorizeRequests()์ antMatchers() ์ฌ์ฉ ์ spring-security-6.x์์๋ ์ปดํ์ผ ๊ฒฝ๊ณ ๊ฐ ๋ฐ์ํ๋ฉฐ, ์ดํ ๋ฆด๋ฆฌ์ค์์ ์ ๊ฑฐ๋ ์์ ์ด๋ค.
11. cURL๋ก ์ง์ ๊ฒ์ฆํด๋ณด๊ธฐ
cURL๋ก ์ค์ ๋์์ ํ์ธํด๋ณด์.
ํ์๊ฐ์ :
curl -X POST http://localhost:8080/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"firstname":"ํ","lastname":"๊ธธ๋","email":"hong@example.com","password":"secret123"}'ShellScript์๋ต์ผ๋ก JWT ํ ํฐ ๋ฌธ์์ด์ด ๋ฐํ๋๋ค.
๋ก๊ทธ์ธ:
curl -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"hong@example.com","password":"secret123"}'
# ์๋ต: {"token":"eyJhbGciOiJIUzI1NiJ9..."}ShellScript๋ณดํธ๋ ์๋ํฌ์ธํธ ํธ์ถ:
TOKEN="eyJhbGciOiJIUzI1NiJ9..."
curl http://localhost:8080/api/v1/users/me \
-H "Authorization: Bearer $TOKEN"ShellScriptAuthorization: Bearer {token} ํค๋ ์์ด ๋ณดํธ๋ ๊ฒฝ๋ก๋ฅผ ํธ์ถํ๋ฉด Spring Security๊ฐ ์๋์ผ๋ก HTTP 401 Unauthorized๋ฅผ ๋ฐํํ๋ค. ํ ํฐ์ด ๋ง๋ฃ๋์๊ฑฐ๋ ์๋ช
์ด ์๋ชป๋ ๊ฒฝ์ฐ๋ ๋์ผํ๊ฒ ์ฒ๋ฆฌ๋๋ค.
๋ง์ง๋ง์ผ๋ก
WebSecurityConfigurerAdapter์์ SecurityFilterChain์ผ๋ก์ ์ ํ์ ๋จ์ํ API ๋ณ๊ฒฝ์ด ์๋๋ค. ๋๋ค DSL๋ก ๋์ด์ค๋ฉด ๊ฐ ๋ณด์ ์ค์ ์ด ๋ช
ํํ ๋ถ๋ฆฌ๋๊ณ , ์์กด์ฑ ์ฃผ์
๊ธฐ๋ฐ์ผ๋ก ๊ตฌ์ฑ๋์ด ํ
์คํธํ๊ธฐ๋ ์์ํด์ง๋ค. ๋ ์ธ๋ถ์ ์ธ ์ต์
์ Spring Security ๊ณต์ ๋ ํผ๋ฐ์ค์ JJWT GitHub์์ ํ์ธํ ์ ์๋ค.
