프레임워크/스프링

[스프링] 스프링시큐리티 + 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));
}

 

 

이렇게 기초뼈대를 만드는 작업을 끝내고 다음 글에서 로그인 기능을 구현해 보겠다.