์ด๋ฒ ํฌ์คํ ์์๋ Spring Boot Security๋ฅผ ํ์ฉํ JWT ๊ธฐ๋ฐ ์ธ์ฆ ์์คํ ๊ตฌํ์ ๋ํด์ ์ ๋ฆฌํ๊ณ ์ ํ๋ค. Spring Boot Security๋ Spring ์ํ๊ณ์์ ์ธ์ฆ๊ณผ ์ธ๊ฐ๋ฅผ ๋ด๋นํ๋ ํต์ฌ ํ๋ ์์ํฌ๋ก, Spring Boot 3.x ๋ฒ์ ๋ถํฐ ์ค์ ๋ฐฉ์์ด ํฌ๊ฒ ๋ฐ๋์๊ธฐ ๋๋ฌธ์ ๊ธฐ์กด ๋ฐฉ์๊ณผ ํผ์ฉํ๋ฉด ์์์น ๋ชปํ ๋ฌธ์ ๊ฐ ์๊ธฐ๊ธฐ๋ ํ๋ค. ์ด ํฌ์คํ ์์๋ Spring Boot 3.2, Java 21, Spring Security 6 ๊ธฐ์ค์ผ๋ก JWT ์ธ์ฆ ํ๋ฆ ์ ์ฒด๋ฅผ ์ฒ์๋ถํฐ ๊ตฌํํด๋ณธ๋ค.
ํ๋ก์ ํธ ์์กด์ฑ ์ค์
Spring Boot Security์ JWT๋ฅผ ํจ๊ป ์ฌ์ฉํ๋ ค๋ฉด ์๋ ์์กด์ฑ์ด ํ์ํ๋ค. JWT ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ JJWT๋ฅผ ๊ธฐ์ค์ผ๋ก ํ๋ค.
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
// Spring Boot Security ๊ธฐ๋ณธ ์์กด์ฑ
implementation 'org.springframework.boot:spring-boot-starter-security'
// JPA ์ฌ์ฉ ์ ์ถ๊ฐ
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// JJWT - JWT ์์ฑ ๋ฐ ํ์ฑ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// H2 ์ธ๋ฉ๋ชจ๋ฆฌ DB (ํ
์คํธ์ฉ)
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}Groovy์ ์ฒด ๊ตฌ์กฐ ์ดํด
Spring Boot Security JWT ์ธ์ฆ ํ๋ฆ์ ํฌ๊ฒ ์ธ ๋จ๊ณ๋ก ๋๋๋ค.
- ๋ก๊ทธ์ธ ์์ฒญ ์ ์ฌ์ฉ์ ์๊ฒฉ์ฆ๋ช ์ ๊ฒ์ฆํ๊ณ JWT ํ ํฐ์ ๋ฐ๊ธํ๋ค.
- ์ดํ ์์ฒญ์์๋ ์์ฒญ ํค๋์ JWT ํ ํฐ์ ํ์ฑํ์ฌ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ SecurityContext์ ๋ฑ๋กํ๋ค.
- SecurityFilterChain์ด ๊ฐ ์์ฒญ์ ์ธ๊ฐ ์ฌ๋ถ๋ฅผ ํ๋จํ๋ค.
Spring Security 6๋ถํฐ๋ WebSecurityConfigurerAdapter๊ฐ ์์ ํ ์ ๊ฑฐ๋์๋ค. ๋ชจ๋ ๋ณด์ ์ค์ ์ SecurityFilterChain์ Bean์ผ๋ก ๋ฑ๋กํ๋ ๋ฐฉ์์ผ๋ก๋ง ์ฒ๋ฆฌ๋๋ค. ์ด ์ ์ ๋จผ์ ์์งํ๋ ๊ฒ์ด Spring Boot Security ์ค์ ์ ์ถ๋ฐ์ ์ด๋ค.

User ์ํฐํฐ์ UserDetailsService ๊ตฌํ
Spring Boot Security๋ ์ธ์ฆ ๊ณผ์ ์์ UserDetails ์ธํฐํ์ด์ค๋ฅผ ํตํด ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์กฐํํ๋ค. ๋จผ์ UserDetails๋ฅผ ๊ตฌํํ ์ํฐํฐ๋ฅผ ์์ฑํ๋ค.
// User.java
package com.example.security.domain;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
@Entity
@Table(name = "users")
@Getter
@NoArgsConstructor
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String password;
// ROLE_USER, ROLE_ADMIN ๋ฑ ๊ถํ ์ ๋ณด ์ ์ฅ
@Enumerated(EnumType.STRING)
private Role role;
@Builder
public User(String email, String password, Role role) {
this.email = email;
this.password = password;
this.role = role;
}
// ๊ถํ ๋ชฉ๋ก ๋ฐํ - SecurityContext์์ ์ธ๊ฐ ํ๋จ์ ์ฌ์ฉ
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(role.name()));
}
// Spring Security๋ username์ผ๋ก ์ฌ์ฉ์๋ฅผ ์๋ณ - ์ฌ๊ธฐ์๋ email์ ์ฌ์ฉ
@Override
public String getUsername() {
return this.email;
}
// ๊ณ์ ๋ง๋ฃ, ์ ๊ธ ๋ฑ์ ์ํ๋ฅผ true๋ก ์ค์ (์ค๋ฌด์์๋ DB ์ปฌ๋ผ์ผ๋ก ๊ด๋ฆฌ ๊ถ์ฅ)
@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// Role.java
package com.example.security.domain;
public enum Role {
ROLE_USER,
ROLE_ADMIN
}Java// UserRepository.java
package com.example.security.repository;
import com.example.security.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
// email์ผ๋ก ์ฌ์ฉ์ ์กฐํ - ๋ก๊ทธ์ธ ์ ์ฌ์ฉ์ ์ ๋ณด ๊ฒ์์ ํ์
Optional<User> findByEmail(String email);
}Java์ด์ UserDetailsService๋ฅผ ๊ตฌํํ๋ค. Spring Boot Security๋ ์ธ์ฆ ๊ณผ์ ์์ ์ด ์๋น์ค๋ฅผ ํตํด ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ก๋ํ๋ค.
// CustomUserDetailsService.java
package com.example.security.service;
import com.example.security.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
// username(email)์ผ๋ก DB์์ ์ฌ์ฉ์๋ฅผ ์กฐํ - ์์ผ๋ฉด ์ธ์ฆ ์คํจ ์ฒ๋ฆฌ
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค: " + username));
}
}JavaJWT ์ ํธ๋ฆฌํฐ ํด๋์ค ๊ตฌํ
JWT ํ ํฐ์ ์์ฑ, ํ์ฑ, ๊ฒ์ฆ ๋ก์ง์ ํ๋์ ์ปดํฌ๋ํธ๋ก ๋ถ๋ฆฌํ๋ค. JJWT ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ๋ฉด ์์ธํ ์ต์ ์ ํ์ธํ ์ ์๋ค.
// JwtTokenProvider.java
package com.example.security.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtTokenProvider {
// ์ด์ํ๊ฒฝ์์๋ ํ๊ฒฝ๋ณ์๋ก ์ฃผ์
๊ถ์ฅ - ์ต์ 256๋นํธ(32๋ฐ์ดํธ) ์ด์์ Base64 ์ธ์ฝ๋ฉ ๊ฐ ์ฌ์ฉ
@Value("${jwt.secret}")
private String secretKey;
// ํ ํฐ ๋ง๋ฃ ์๊ฐ (๋ฐ๋ฆฌ์ด ๋จ์) - 1์๊ฐ = 3600000
@Value("${jwt.expiration}")
private long jwtExpiration;
// SecretKey ๊ฐ์ฒด ์์ฑ - HMAC-SHA ์๊ณ ๋ฆฌ์ฆ์ ์ ํฉํ ํค ์์ฑ
private SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
// JWT ํ ํฐ์์ ์ด๋ฉ์ผ(subject) ์ถ์ถ
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);
}
// UserDetails๋ก๋ถํฐ JWT ํ ํฐ ์์ฑ
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
// ์ถ๊ฐ Claims๋ฅผ ํฌํจํ JWT ํ ํฐ ์์ฑ (role, userId ๋ฑ ์ปค์คํ
์ ๋ณด ์ฝ์
๊ฐ๋ฅ)
public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
return Jwts.builder()
.claims(extraClaims)
.subject(userDetails.getUsername())
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + jwtExpiration))
// HS256 ์๊ณ ๋ฆฌ์ฆ์ผ๋ก ์๋ช
- ์๋ฒ๋ง ์๊ณ ์๋ ํค๋ก ํ ํฐ ์๋ณ์กฐ ๋ฐฉ์ง
.signWith(getSigningKey())
.compact();
}
// ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฌ - ์ฌ์ฉ์ ์ ๋ณด ์ผ์น ์ฌ๋ถ + ๋ง๋ฃ ์ฌ๋ถ ๋์ ํ์ธ
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private Claims extractAllClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
}Javaapplication.yml์ JWT ๊ด๋ จ ์ค์ ์ ์ถ๊ฐํ๋ค.
# application.yml
jwt:
# ์ด์ํ๊ฒฝ์์๋ ํ๊ฒฝ๋ณ์(JWT_SECRET)๋ก ์ฃผ์
- ์: ${JWT_SECRET}
secret: 404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970
# 1์๊ฐ ๋ง๋ฃ
expiration: 3600000
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
show-sql: trueYAMLJwtAuthenticationFilter ๊ตฌํ
์์ฒญ๋ง๋ค JWT ํ ํฐ์ ๊ฒ์ฌํ๋ ํํฐ๋ฅผ ๊ตฌํํ๋ค. ์ด ํํฐ๊ฐ Spring Boot Security์ ์ธ์ฆ ํ๋ฆ์์ ํต์ฌ ์ญํ ์ ํ๋ค.
// JwtAuthenticationFilter.java
package com.example.security.jwt;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
@RequiredArgsConstructor
// OncePerRequestFilter: ๋์ผ ์์ฒญ์์ ํํฐ๊ฐ ์ค๋ณต ์คํ๋์ง ์๋๋ก ๋ณด์ฅ
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
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 " ์ดํ์ ์ค์ ํ ํฐ ๋ฌธ์์ด ์ถ์ถ
final String jwt = authHeader.substring(7);
final String userEmail;
try {
userEmail = jwtTokenProvider.extractUsername(jwt);
} catch (JwtException e) {
// ํ ํฐ ํ์ฑ ์คํจ ์ ์ธ์ฆ ์์ด ๋ค์ ํํฐ๋ก ๋๊น - ์ดํ ์ธ๊ฐ ๋จ๊ณ์์ ๊ฑฐ๋ถ๋จ
filterChain.doFilter(request, response);
return;
}
// ์ด๋ฏธ ์ธ์ฆ๋ ์ฌ์ฉ์๋ผ๋ฉด ๋ค์ ์ธ์ฆ ์ฒ๋ฆฌํ์ง ์์
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
if (jwtTokenProvider.isTokenValid(jwt, userDetails)) {
// ํ ํฐ์ด ์ ํจํ๋ฉด Authentication ๊ฐ์ฒด ์์ฑ ํ SecurityContext์ ๋ฑ๋ก
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null, // ์ด๋ฏธ ์ธ์ฆ๋ ์ํ์ด๋ฏ๋ก credentials์ null๋ก ์ค์
userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}JavaSpring Boot Security – SecurityFilterChain ์ค์
Spring Boot Security ์ค์ ์ ํต์ฌ์ธ SecurityFilterChain์ Bean์ผ๋ก ๋ฑ๋กํ๋ค. Spring Security ๊ณต์ ๋ฌธ์์ SecurityFilterChain ๊ตฌ์ฑ ๊ฐ์ด๋๋ฅผ ์ฐธ๊ณ ํ๋ฉด ๋ ๋ค์ํ ์ต์
์ ํ์ธํ ์ ์๋ค.
// SecurityConfig.java
package com.example.security.config;
import com.example.security.jwt.JwtAuthEntryPoint;
import com.example.security.jwt.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity // Spring Boot Security ํ์ฑํ - ๊ธฐ๋ณธ ๋ณด์ ์๋์ค์ ์ปค์คํฐ๋ง์ด์ง
@EnableMethodSecurity // @PreAuthorize, @PostAuthorize ์ด๋
ธํ
์ด์
์ฌ์ฉ ํ์ฑํ
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final JwtAuthEntryPoint jwtAuthEntryPoint;
private final UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// CSRF ๋นํ์ฑํ - JWT ๊ธฐ๋ฐ Stateless ๋ฐฉ์์์๋ CSRF ํ ํฐ์ด ๋ถํ์
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling(exception -> exception
// ์ธ์ฆ ์คํจ ์ JSON ํํ์ 401 ์๋ต ๋ฐํ - ๊ธฐ๋ณธ ๋ก๊ทธ์ธ ํ์ด์ง ๋ฆฌ๋ค์ด๋ ํธ ๋์ REST API ์๋ต ์ฒ๋ฆฌ
.authenticationEntryPoint(jwtAuthEntryPoint)
)
.authorizeHttpRequests(auth -> auth
// ํ์๊ฐ์
, ๋ก๊ทธ์ธ ์๋ํฌ์ธํธ๋ ์ธ์ฆ ์์ด ์ ๊ทผ ํ์ฉ
.requestMatchers("/api/auth/**").permitAll()
// H2 ์ฝ์ ์ ๊ทผ ํ์ฉ (๊ฐ๋ฐํ๊ฒฝ ํ์ - ์ด์์์๋ ์ ๊ฑฐ)
.requestMatchers("/h2-console/**").permitAll()
// ๋๋จธ์ง ๋ชจ๋ ์์ฒญ์ ์ธ์ฆ ํ์
.anyRequest().authenticated()
)
// ์ธ์
๋ฏธ์ฌ์ฉ - JWT Stateless ๋ฐฉ์์ด๋ฏ๋ก ์๋ฒ์ ์ธ์
์ ์ ์ฅํ์ง ์์
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authenticationProvider(authenticationProvider())
// JwtAuthenticationFilter๋ฅผ UsernamePasswordAuthenticationFilter ์์ ๋ฐฐ์น
// ์์ฒญ์ด ์ฌ ๋ JWT ๊ฒ์ฌ๋ฅผ ๋จผ์ ์ํํ๋๋ก ์์ ์ง์ - ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ์ด ํผ ๊ธฐ๋ฐ ์ธ์ฆ๋ณด๋ค ์ฐ์
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
// DB์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ก๋ํ๋ UserDetailsService ์ค์ - loadUserByUsername() ๋ฉ์๋ ํธ์ถ
authProvider.setUserDetailsService(userDetailsService);
// ๋น๋ฐ๋ฒํธ ์ํธํ ๋ฐฉ์ ์ค์ - BCrypt๋ salt๋ฅผ ์๋์ผ๋ก ํฌํจํ์ฌ ๊ฐ์ ๋น๋ฐ๋ฒํธ๋ ๋งค๋ฒ ๋ค๋ฅด๊ฒ ์ํธํ๋จ
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
// Spring Boot๊ฐ ์๋์ผ๋ก ์์ฑํ ๊ธฐ๋ณธ AuthenticationManager ๋ฐํ
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
// BCryptPasswordEncoder - strength ๊ธฐ๋ณธ๊ฐ 10 (๋์์๋ก ๋๋ฆฌ์ง๋ง ๋ณด์ ๊ฐ๋ ์ฆ๊ฐ)
// strength ๊ฐ: 4~31 ๋ฒ์, ์ค๋ฌด ๊ถ์ฅ๊ฐ์ 10~12
return new BCryptPasswordEncoder();
}
}Java์ธ์ฆ ์๋น์ค์ ์ปจํธ๋กค๋ฌ ๊ตฌํ
Spring Boot Security ์ธ์ฆ ์๋น์ค
// AuthenticationService.java
package com.example.security.service;
import com.example.security.domain.Role;
import com.example.security.domain.User;
import com.example.security.dto.AuthRequest;
import com.example.security.dto.AuthResponse;
import com.example.security.dto.RegisterRequest;
import com.example.security.jwt.JwtTokenProvider;
import com.example.security.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class AuthenticationService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final AuthenticationManager authenticationManager;
public AuthResponse register(RegisterRequest request) {
var user = User.builder()
.email(request.getEmail())
// ๋น๋ฐ๋ฒํธ๋ ๋ฐ๋์ ์ํธํํ์ฌ ์ ์ฅ - ํ๋ฌธ ์ ์ฅ ์ ๋ ๊ธ์ง
.password(passwordEncoder.encode(request.getPassword()))
.role(Role.ROLE_USER)
.build();
userRepository.save(user);
var jwtToken = jwtTokenProvider.generateToken(user);
return new AuthResponse(jwtToken);
}
public AuthResponse authenticate(AuthRequest request) {
// AuthenticationManager๊ฐ UserDetailsService์ PasswordEncoder๋ฅผ ์ฌ์ฉํ์ฌ ์๊ฒฉ์ฆ๋ช
๊ฒ์ฆ
// ์คํจ ์ BadCredentialsException ๋ฐ์ - Spring Boot Security๊ฐ ์๋ ์ฒ๋ฆฌ
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getEmail(),
request.getPassword()
)
);
var user = userRepository.findByEmail(request.getEmail())
.orElseThrow();
var jwtToken = jwtTokenProvider.generateToken(user);
return new AuthResponse(jwtToken);
}
}Java// AuthController.java
package com.example.security.controller;
import com.example.security.dto.AuthRequest;
import com.example.security.dto.AuthResponse;
import com.example.security.dto.RegisterRequest;
import com.example.security.service.AuthenticationService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationService authService;
@PostMapping("/register")
public ResponseEntity<AuthResponse> register(@RequestBody RegisterRequest request) {
return ResponseEntity.ok(authService.register(request));
}
@PostMapping("/authenticate")
public ResponseEntity<AuthResponse> authenticate(@RequestBody AuthRequest request) {
return ResponseEntity.ok(authService.authenticate(request));
}
}JavaDTO ํด๋์ค๋ ๊ฐ๋จํ ์ถ๊ฐํ๋ค.
// RegisterRequest.java
package com.example.security.dto;
import lombok.Data;
@Data
public class RegisterRequest {
private String email;
private String password;
}
// AuthRequest.java
package com.example.security.dto;
import lombok.Data;
@Data
public class AuthRequest {
private String email;
private String password;
}
// AuthResponse.java
package com.example.security.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class AuthResponse {
private String token;
}Java๋ฉ์๋ ์์ค ์ธ๊ฐ ์ฒ๋ฆฌ
@EnableMethodSecurity๋ฅผ ํ์ฑํํ์ผ๋ฏ๋ก ์ปจํธ๋กค๋ฌ ๋ฉ์๋ ๋จ์๋ก ๊ถํ์ ์ ์ดํ ์ ์๋ค.
// DemoController.java
package com.example.security.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/demo")
public class DemoController {
// ์ธ์ฆ๋ ๋ชจ๋ ์ฌ์ฉ์ ์ ๊ทผ ๊ฐ๋ฅ - SecurityFilterChain์ anyRequest().authenticated()์ ๋์ผ
@GetMapping("/user")
public ResponseEntity<String> userEndpoint() {
return ResponseEntity.ok("์ผ๋ฐ ์ฌ์ฉ์ ์ ๊ทผ ์ฑ๊ณต");
}
// ROLE_ADMIN ๊ถํ์ ๊ฐ์ง ์ฌ์ฉ์๋ง ์ ๊ทผ ๊ฐ๋ฅ
// Spring Boot Security์ ๋ฉ์๋ ์์ค ์ธ๊ฐ ์ฒ๋ฆฌ
@GetMapping("/admin")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public ResponseEntity<String> adminEndpoint() {
return ResponseEntity.ok("๊ด๋ฆฌ์ ์ ๊ทผ ์ฑ๊ณต");
}
}Java์์ธ ์ฒ๋ฆฌ – Spring Boot Security AuthEntryPoint
์ธ์ฆ ์คํจ๋ ์ธ๊ฐ ๊ฑฐ๋ถ ์์ ์๋ต์ ์ปค์คํฐ๋ง์ด์งํ๋ ค๋ฉด AuthenticationEntryPoint์ AccessDeniedHandler๋ฅผ ๊ตฌํํ๋ฉด ๋๋ค.
// JwtAuthEntryPoint.java
package com.example.security.jwt;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Component
// AuthenticationEntryPoint - ์ธ์ฆ๋์ง ์์ ์์ฒญ์ด ๋ณดํธ๋ ๋ฆฌ์์ค์ ์ ๊ทผํ ๋ ํธ์ถ
public class JwtAuthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException
) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
// 401 Unauthorized ๋ฐํ
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
final Map<String, Object> body = new HashMap<>();
body.put("status", HttpServletResponse.SC_UNAUTHORIZED);
body.put("error", "Unauthorized");
body.put("message", authException.getMessage());
body.put("path", request.getServletPath());
final ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), body);
}
}JavaSecurityConfig์ exceptionHandling ์ค์ ์ ์ถ๊ฐํ๋ค.
// SecurityConfig.java - securityFilterChain ๋ฉ์๋์ ์ถ๊ฐ
.exceptionHandling(exception -> exception
// ์ธ์ฆ ์คํจ ์ JSON ํํ์ 401 ์๋ต ๋ฐํ
.authenticationEntryPoint(jwtAuthEntryPoint)
)Java๋์ ํ์ธ
์๋ฒ๋ฅผ ์คํํ ๋ค ์๋ ์์๋ก API๋ฅผ ํธ์ถํ๋ฉด ์ ์ฒด Spring Boot Security JWT ์ธ์ฆ ํ๋ฆ์ ํ์ธํ ์ ์๋ค.
# 1. ํ์๊ฐ์
curl -X POST http://localhost:8080/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"password123"}'
# ์๋ต ์์
# {"token":"eyJhbGciOiJIUzI1NiJ9..."}
# 2. ํ ํฐ์ผ๋ก ๋ณดํธ๋ ๋ฆฌ์์ค ์ ๊ทผ
curl -X GET http://localhost:8080/api/demo/user \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
# 3. ํ ํฐ ์์ด ์ ๊ทผ ์ - 401 ๋ฐํ
curl -X GET http://localhost:8080/api/demo/userShellScriptSpring Boot Security์ JWT๋ฅผ ์ฒ์ ์ฐ๋ํ ๋ ์์ฃผ ๋ง์ฃผ์น๋ ๋ฌธ์ ์ค ํ๋๋ Filter ๋ฑ๋ก ์์๋ค. JwtAuthenticationFilter๊ฐ UsernamePasswordAuthenticationFilter ๋ค์ ๋ฐฐ์น๋๋ฉด ํ ํฐ์ด ์์ด๋ ์ธ์ฆ์ด ์ฒ๋ฆฌ๋์ง ์๋๋ค. addFilterBefore๋ก ์์๋ฅผ ๋ช
ํํ ์ง์ ํ๋ ์ด์ ๊ฐ ์ฌ๊ธฐ ์๋ค. ๋ํ Spring Security 6์ ๋ณ๊ฒฝ ์ฌํญ์ ๋ํด์๋ Spring Security 6 ๋ง์ด๊ทธ๋ ์ด์
๊ฐ์ด๋๋ฅผ ์ฐธ๊ณ ํ๋ฉด ๊ตฌ๋ฒ์ ๊ณผ์ ์ฐจ์ด์ ์ ํ๋์ ํ์ธํ ์ ์๋ค.
Refresh Token ๊ตฌํ์ด๋ Redis๋ฅผ ํ์ฉํ ํ ํฐ ๋ธ๋๋ฆฌ์คํธ ์ฒ๋ฆฌ ๊ฐ์ ๊ณ ๊ธ ์ฃผ์ ๋ ์ด๋ฒ ํฌ์คํ ๋ฒ์๋ฅผ ๋ฒ์ด๋์ง๋ง, ์ค๋ฌด์์๋ Access Token ๋ง๋ฃ ์๊ฐ์ ์งง๊ฒ ์ ์งํ๊ณ Refresh Token์ผ๋ก ์ฌ๋ฐ๊ธํ๋ ํจํด์ด ๋ณด์ ์ธก๋ฉด์์ ๋์์ง ์์ ๊ฒ ๊ฐ๋ค. Spring Security OAuth2 Resource Server๋ฅผ ํ์ฉํ๋ฉด JWT ๊ฒ์ฆ ๋ก์ง์ ์๋น ๋ถ๋ถ์ ํ๋ ์์ํฌ์ ์์ํ ์๋ ์๋ค.
๋ง์น๋ฉฐ
์ง๊ธ๊น์ง Spring Boot Security์ JWT๋ฅผ ์ฐ๋ํ ์ธ์ฆ ์์คํ
๊ตฌํ ์ ์ฒด ํ๋ฆ์ ์ ๋ฆฌํด ๋ณด์๋ค. SecurityFilterChain ์ค์ ๋ถํฐ JwtAuthenticationFilter, UserDetailsService, ์์ธ ์ฒ๋ฆฌ๊น์ง Spring Boot 3.2 ๊ธฐ์ค์ผ๋ก ์ค์ ๋์ํ๋ ์ฝ๋๋ฅผ ์ค์ฌ์ผ๋ก ๋ค๋ค๋๋ฐ, Spring Security 6๋ก ๋์ด์ค๋ฉด์ ์ค์ ๋ฐฉ์์ด ๊ฝค ๋ง์ด ๋ฌ๋ผ์ง ๋งํผ ๊ตฌ๋ฒ์ ๋ ํผ๋ฐ์ค๋ฅผ ๊ทธ๋๋ก ๋ฐ๋ผ๊ฐ๋ค ๋ณด๋ฉด ์ปดํ์ผ ์ค๋ฅ๋ ๋์ ์ด์์ ๊ฒช๋ ๊ฒฝ์ฐ๊ฐ ์ ์ง ์์ ๊ฒ ๊ฐ๋ค. ์ด ํฌ์คํ
์ด Spring Boot Security๋ฅผ ์ฒ์ ๋์
ํ๊ฑฐ๋ ๋ฒ์ ์ ์ฌ๋ฆฌ๋ ๊ณผ์ ์์ ์์ ์ฐธ๊ณ ๊ฐ ๋์์ผ๋ฉด ํ๋ ๋ฐ๋์ด๋ค.
