Spring Boot3.1 과 JWT를 이용하여 로그인 / 로그아웃 / 토큰 생성 / 토큰 검증 기능을 간다하게 구현해 봅니다.
프로젝트 구성
src/main/java/com/example/jwtapp/ ├── controller/AuthController.java ├── filter/JwtFilter.java ├── model/AuthRequest.java ├── model/AuthResponse.java ├── model/LogoutRequest.java ├── security/JwtUtil.java ├── security/SecurityConfig.java ├── service/TokenBlacklistService.java └── JwtAppApplication.java
Gradle 의존성 추가
build.gradle
plugins { id 'java' id 'war' id 'org.springframework.boot' version '3.1.0' id 'io.spring.dependency-management' version '1.1.6' id 'application' // 추가 id "com.github.node-gradle.node" version "7.1.0" } group = 'com.coforward' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(17) } } node { download = true version = "22.12.0" npmVersion = "10.9.0" distBaseUrl = "https://nodejs.org/dist" npmInstallCommand = "install" workDir = file("${project.projectDir}/.gradle/nodejs") npmWorkDir = file("${project.projectDir}/.gradle/npm") nodeProjectDir = file('./frontend') } /** Node.js 프로젝트 디렉토리(./frontend)에서 npm install 명령을 실행합니다. package.json에 정의된 종속성을 설치합니다. */ task setUp(type: NpmTask) { description = "Install Node.js Packages" args = ['install'] } /** npm run build 명령을 실행하여 Vue.js 프로젝트를 빌드합니다. dependsOn: setUp: Vue 빌드 전에 npm install이 실행되도록 설정합니다. */ task buildFrontEnd(type: NpmTask, dependsOn: setUp) { description = "build Vue" args = ['run', 'build'] } /** Spring Boot의 리소스 처리 단계(processResources)가 Vue 빌드 결과를 포함하도록 설정. Gradle이 Spring Boot 프로젝트를 빌드할 때 Vue.js도 자동으로 빌드되도록 합니다. */ processResources.dependsOn 'buildFrontEnd' configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-devtools' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.0' implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'org.springframework.boot:spring-boot-starter-websocket' implementation 'jakarta.websocket:jakarta.websocket-api' implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16' implementation 'com.google.code.gson:gson:2.10.1' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat' implementation 'org.springframework.boot:spring-boot-starter-tomcat' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' runtimeOnly 'mysql:mysql-connector-java:8.0.25' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' implementation 'jakarta.servlet:jakarta.servlet-api:6.0.0' } tasks.named('test') { useJUnitPlatform() }
JWT 유틸리티 클래스
JwtUtil.java
package com.coforward.egfwts.jwt.security; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.util.Date; @Component public class JwtUtil { // HS256에 적합한 비밀 키 생성 private final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256); private final long EXPIRATION_TIME = 1000 * 60 * 60; // 1시간 // JWT 토큰 생성 public String generateToken(String username) { return Jwts.builder() .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(SECRET_KEY) // 비밀 키를 직접 사용 .compact(); } // JWT 토큰에서 사용자 이름 추출 public String extractUsername(String token) { return Jwts.parserBuilder() // 새로운 파서 빌더 사용 .setSigningKey(SECRET_KEY) .build() .parseClaimsJws(token) .getBody() .getSubject(); } // JWT 토큰 유효성 검증 /* public boolean validateToken(String token) { try { Jwts.parserBuilder() // 새로운 파서 빌더 사용 .setSigningKey(SECRET_KEY) .build() .parseClaimsJws(token); return true; } catch (JwtException e) { return false; } } */ // JWT 토큰에서 클레임 추출 private Claims extractClaims(String token) { return Jwts.parserBuilder() .setSigningKey(SECRET_KEY) .build() .parseClaimsJws(token) .getBody(); } // JWT 토큰 유효성 검사 public boolean isTokenExpired(String token) { return extractClaims(token).getExpiration().before(new Date()); } // JWT 토큰 유효성 검사 및 사용자 추출 public boolean validateToken(String token, String username) { return (username.equals(extractUsername(token)) && !isTokenExpired(token)); } }
로그인 요청 및 응답 모델
AuthRequest.java
package com.coforward.egfwts.jwt.model; import lombok.Getter; import lombok.Setter; @Getter @Setter public class AuthRequest { private String username; private String password; // Getter와 Setter }
AuthResponse.java
package com.coforward.egfwts.jwt.model; import lombok.Getter; import lombok.Setter; @Getter @Setter public class AuthResponse { private String token; public AuthResponse(String token) { this.token = token; } // Getter }
로그아웃 요청 모델
LogoutRequest.java
package com.coforward.egfwts.jwt.model; import lombok.Getter; import lombok.Setter; @Getter @Setter public class LogoutRequest { private String token; // Getter와 Setter }
블랙리스트 서비스
TokenBlacklistService.java
package com.coforward.egfwts.jwt.service; import org.springframework.stereotype.Service; import java.util.HashSet; import java.util.Set; @Service public class TokenBlacklistService { private final Set<String> blacklist = new HashSet<>(); // 토큰 블랙리스트에 추가 public void addTokenToBlacklist(String token) { blacklist.add(token); } // 토큰이 블랙리스트에 있는지 확인 public boolean isTokenBlacklisted(String token) { return blacklist.contains(token); } }
컨트롤러: 로그인 및 로그아웃 토큰 유효서 체크
AuthController.java
package com.coforward.egfwts.jwt.controller; import com.coforward.egfwts.jwt.model.AuthRequest; import com.coforward.egfwts.jwt.model.AuthResponse; import com.coforward.egfwts.jwt.model.LogoutRequest; import com.coforward.egfwts.jwt.security.JwtUtil; import com.coforward.egfwts.jwt.service.TokenBlacklistService; import com.coforward.egfwts.relay.RelaySocket; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/auth") public class AuthController { private static final Logger logger = LoggerFactory.getLogger(AuthController.class); private final JwtUtil jwtUtil; private final TokenBlacklistService blacklistService; public AuthController(JwtUtil jwtUtil, TokenBlacklistService blacklistService) { this.jwtUtil = jwtUtil; this.blacklistService = blacklistService; } @PostMapping("/login") public ResponseEntity<AuthResponse> login(@RequestBody AuthRequest authRequest) { // 간단한 인증 로직 (실제에서는 DB와 연동) logger.debug(">>>>1: " + authRequest.getUsername()); if ("user".equals(authRequest.getUsername()) && "password".equals(authRequest.getPassword())) { String token = jwtUtil.generateToken(authRequest.getUsername()); logger.debug(">>>>2: " + token); return ResponseEntity.ok(new AuthResponse(token)); } return ResponseEntity.status(401).body(null); } @PostMapping("/logout") public ResponseEntity<Void> logout(@RequestBody LogoutRequest logoutRequest) { blacklistService.addTokenToBlacklist(logoutRequest.getToken()); return ResponseEntity.ok().build(); } @GetMapping("/check") public String checkAuthentication(@RequestHeader(value = "Authorization", required = false) String authorization) { if (authorization != null && authorization.startsWith("Bearer ")) { String token = authorization.substring(7); String username = jwtUtil.extractUsername(token); if (jwtUtil.validateToken(token, username)) { return "okok"; // 인증 성공 } } return "notokok"; // 인증 실패 } }
JWT 필터
JwtFilter.java
package com.coforward.egfwts.jwt.filter; import com.coforward.egfwts.jwt.security.JwtUtil; import com.coforward.egfwts.jwt.service.TokenBlacklistService; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.web.filter.OncePerRequestFilter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import org.springframework.lang.NonNull; public class JwtFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final TokenBlacklistService blacklistService; public JwtFilter(JwtUtil jwtUtil, TokenBlacklistService blacklistService) { this.jwtUtil = jwtUtil; this.blacklistService = blacklistService; } @Override protected void doFilterInternal( @NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain chain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); // Authorization 헤더가 "Bearer <token>" 형식인지 확인 if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); String username = jwtUtil.extractUsername(token); if (jwtUtil.validateToken(token, username) && !blacklistService.isTokenBlacklisted(token)) { //String username = jwtUtil.extractUsername(token); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, null); SecurityContextHolder.getContext().setAuthentication(authentication); } else { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("Invalid or expired token"); return; // 유효하지 않거나 만료된 토큰일 경우 인증을 중단하고 응답 } } chain.doFilter(request, response); } }
Spring Security 설정
SecurityConfig.java
package com.coforward.egfwts.jwt.security; import com.coforward.egfwts.jwt.filter.JwtFilter; import com.coforward.egfwts.jwt.service.TokenBlacklistService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration public class SecurityConfig { private final JwtUtil jwtUtil; private final TokenBlacklistService blacklistService; public SecurityConfig(JwtUtil jwtUtil, TokenBlacklistService blacklistService) { this.jwtUtil = jwtUtil; this.blacklistService = blacklistService; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeHttpRequests(authz -> authz .requestMatchers("/auth/login", "/auth/logout").permitAll() .anyRequest().authenticated() ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ); // 필터 순서 지정: JwtFilter를 UsernamePasswordAuthenticationFilter 앞에 추가 http.addFilterBefore(new JwtFilter(jwtUtil, blacklistService), UsernamePasswordAuthenticationFilter.class); return http.build(); } }
실행 및 테스트
- 로그인 요청 (POST)
- URL: http://localhost:8080/auth/login
- Body:
{ "username": "user", "password": "password" }
- 응답:
{ "token": "generated_jwt_token" }
- 로그아웃 요청 (POST)
- URL: http://localhost:8080/auth/logout
- Body:
{ "token": "generated_jwt_token" }
- 토큰 체크 (GET)
- URL: http://localhost:8080/auth/check
- header:
- key : Authorization
- value : Bearer token
로그인 요청

토큰 체크

로그아웃

토큰 체크

'프로그래밍 > Java' 카테고리의 다른 글
전자서명 / 검증 / 부인방지 개념정리 (0) | 2025.02.19 |
---|---|
spring boot gradle REST API 서버 테스트 구축 (1) | 2025.01.17 |
전문 mymq 릴레이소켓 연결시 이슈 (0) | 2024.12.27 |
스프링에서 빈(Bean)을 등록하는 두가지 방법 (0) | 2024.12.19 |
vscode 에서 devtools 정상적으로 작동하지 않을때(자동리로드 안될때) (0) | 2024.12.13 |