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' 카테고리의 다른 글
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 |
spring boot 와 java EE(WebSocket API) 통합 방법 (0) | 2024.12.11 |