프레임워크/스프링
[스프링] 스프링시큐리티 + JWT 연동 (1) 기본셋팅
believekim
2025. 6. 5. 19:14
시스템 환경
IDE | IntelliJ IDEA |
Language | Java 17 |
Framework | Spring Boot 3.4.5 |
Build Tool | Gradle |
ORM | JPA (Hibernate) |
Database | MySQL |
Security | Spring Security + JWT (jjwt 0.11.5) |
API 테스트 도구 | Postman |
환경 변수 관리 | .env 파일 (spring-dotenv:4.0.0) |
의존성 관리 | io.spring.dependency-management 1.1.7 |
로깅/편의성 | Lombok |
https://github.com/pleasebelieveme/security-jwt-template
GitHub - pleasebelieveme/security-jwt-template
Contribute to pleasebelieveme/security-jwt-template development by creating an account on GitHub.
github.com
1. build.gradle
- 스프링 시큐리티와 JWT를 기본으로 환경변수까지 기본적으로 셋팅하였다.
dependencies {
...
// 스프링 시큐리티
testImplementation 'org.springframework.security:spring-security-test'
// 환경변수 .env
implementation("me.paulschwarz:spring-dotenv:4.0.0")
// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
...
}
2. .env
- 실재 프로젝트에서는 .env파일안에 있는 환경변수 값은 노출되지 않아야 한다.
- 그래서 .gitignore에 추가시켜 push되지 않도록 한다.
- 현재는 예시를 위해 노출되어도 되는 값을 넣어두었다.
MYSQL_URL=jdbc:mysql://localhost:3306/security_jwt_template
MYSQL_USERNAME=root
MYSQL_PASSWORD=1234
SPRING_JWT_SECRET=TlSTtzcspRw0Ec1sUrK9WwJ7a5XHVWzisIs2/5rg+bU=
3. SecurityConfig.java
- 세션없이 JWT를 사용하기 때문에 Stateless 구조와 CSRF 비활성화를 적용
- SecurityUrlMatcher.java를 따로 만들어 URL을 외부에서 관리
@EnableWebSecurity
@Configuration
public class SecurityConfig {
private final JwtFilter jwtFilter;
public SecurityConfig(JwtFilter jwtFilter) {
this.jwtFilter = jwtFilter;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(SecurityUrlMatcher.PUBLIC_URLS).permitAll()
.requestMatchers(SecurityUrlMatcher.REFRESH_URL).authenticated()
.requestMatchers(SecurityUrlMatcher.ADMIN_URLS).hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
4. SecurityUrlMatcher.java
- URL 패턴을 한 곳에 모아두면 보안 정책 변경이 훨씬 쉬움.
- isRefreshUrl(), isPublicUrl()는 테스트용으로 사용.
public class SecurityUrlMatcher {
public static final String[] PUBLIC_URLS = {
"/api/users",
"/api/auth/login",
};
public static final String[] ADMIN_URLS = {
"/api/admin/**"
};
public static final String REFRESH_URL = "/api/auth/reissue";
public static boolean isRefreshUrl(String path) {
return REFRESH_URL.equals(path);
}
public static boolean isPublicUrl(String path) {
return Arrays.stream(PUBLIC_URLS).anyMatch(path::startsWith);
}
}
5. JwtFilter.java
- 클라이언트가 인증된 JWT를 보내면, 이 필터가 이를 검증해 인증 상태로 만들어주는 클래스
- 인증 서버를 따로 두지 않고 무상태(Stateless) 구조에서 보안 처리를 가능하게 해주는 핵심 요소
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = jwtUtil.extractToken(request);
try {
if(jwtUtil.validateToken(token)){
// id 혹은 UserRole 검증
UserAuth userAuth = jwtUtil.extractUserAuth(token);
List<SimpleGrantedAuthority> authorities = List.of(
new SimpleGrantedAuthority("ROLE_" + userAuth.getUserRole().name())
);
UsernamePasswordAuthenticationToken authToken = //userAuth,null,authorities
new UsernamePasswordAuthenticationToken(userAuth,null, authorities);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
} catch (Exception e) {
log.error("JWT 인증 처리 중 예외 발생", e);
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"유효하지 않은 접근입니다.");
return;
}
filterChain.doFilter(request,response);
}
}
6. JwtUtil.java
- JWT 토큰 생성, 파싱, 검증, 추출 등의 JWT 관련 로직을 통합 관리하는 클래스
@Component
@RequiredArgsConstructor
public class JwtUtil {
@Value("${jwt.secret}")
private String secretKey;
private static final long EXPIRATION = 1000L * 60 * 30; // 30분
public String createToken(Long id, UserRole userRole){
return Jwts.builder()
.setSubject(String.valueOf(id))
.claim("userRole", userRole.name())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis()+EXPIRATION))
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)))
.compact();
}
public UserAuth extractUserAuth(String token){
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey.getBytes())
.build()
.parseClaimsJws(token)
.getBody();
return new UserAuth(Long.parseLong(claims.getSubject()), UserRole.valueOf(claims.get("userRole",String.class)));
}
public boolean validateToken(String token){
try {
extractUserAuth(token);
return true;
} catch (Exception e) {
return false;
}
}
public String extractToken(HttpServletRequest request){
String bearer = request.getHeader("Authorization");
if(bearer != null && bearer.startsWith("Bearer ")){
return bearer.substring(7);
}
return null;
}
public long getExpiration(String token){
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey.getBytes())
.build()
.parseClaimsJws(token)
.getBody();
return claims.getExpiration().getTime() - System.currentTimeMillis();
}
}
7. UserAuth.java
- JWT에서 추출한 사용자 정보를 담는 DTO 역할 (principal)
- 스프링 시큐리티의 @AuthenticationPrincipal로 컨트롤러에 주입
@Getter
@RequiredArgsConstructor
public class UserAuth {
private final Long id;
private final UserRole userRole;
}
Controller 내정보조회 메서드 예시
@GetMapping("/me")
public ResponseEntity<UserResponse> findById(@AuthenticationPrincipal UserAuth userAuth) {
return ResponseEntity.status(HttpStatus.OK).body(userService.findById(userAuth));
}
이렇게 기초뼈대를 만드는 작업을 끝내고 다음 글에서 로그인 기능을 구현해 보겠다.