diff --git a/build.gradle b/build.gradle index fee9ba6..a61b108 100644 --- a/build.gradle +++ b/build.gradle @@ -46,6 +46,11 @@ dependencies { // Jasypt Spring Boot Starter implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.5' + // JWT 관련 의존성 (최신 0.12.x 버전 기준) + implementation 'io.jsonwebtoken:jjwt-api:0.12.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5' // JSON 파싱용 + } tasks.named('test') { diff --git a/src/main/java/company/specialsource/config/JwtTokenProvider.java b/src/main/java/company/specialsource/config/JwtTokenProvider.java new file mode 100644 index 0000000..63e10f3 --- /dev/null +++ b/src/main/java/company/specialsource/config/JwtTokenProvider.java @@ -0,0 +1,68 @@ +package company.specialsource.config; // 본인의 패키지 경로에 맞게 수정하세요 + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.Date; + +@Component +public class JwtTokenProvider { + + private final SecretKey key; + private final long expirationTime; + + // application.yml에 적은 설정값들을 생성자를 통해 주입받습니다. + public JwtTokenProvider( + @Value("${jwt.secret}") String secretKey, + @Value("${jwt.expiration-time}") long expirationTime) { + + // Base64로 인코딩된 비밀키를 디코딩하여 실제 암호화 키 객체로 변환합니다. + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + this.expirationTime = expirationTime; + } + + // 1. 유저 이름을 받아서 JWT 토큰을 생성하는 메서드 + public String createToken(String username) { + Date now = new Date(); + Date validity = new Date(now.getTime() + expirationTime); + + return Jwts.builder() + .subject(username) // 토큰의 주인(주체) 저장 + .issuedAt(now) // 토큰 발행 시간 + .expiration(validity) // 토큰 만료 시간 + .signWith(key) // 내 서버의 비밀키로 서명(Signature) 생성 + .compact(); + } + + // 2. 토큰에서 유저 이름(Subject)을 꺼내는 메서드 + public String getUsername(String token) { + Claims claims = Jwts.parser() + .verifyWith(key) // 내 비밀키로 서명을 검증한 뒤 + .build() + .parseSignedClaims(token) + .getPayload(); // 데이터 덩어리를 꺼냄 + + return claims.getSubject(); + } + + // 3. 토큰이 유효한지(만료되진 않았는지, 위조되진 않았는지) 검증하는 메서드 + public boolean validateToken(String token) { + try { + Jwts.parser() + .verifyWith(key) + .build() + .parseSignedClaims(token); + return true; // 아무 에러도 안 나면 정상적인 토큰! + } catch (JwtException | IllegalArgumentException e) { + // 토큰이 만료되었거나, 위조되었거나, 형식이 잘못되었을 때 여기로 빠집니다. + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/company/specialsource/controller/AuthController.java b/src/main/java/company/specialsource/controller/AuthController.java index 0179006..3f3cdc5 100644 --- a/src/main/java/company/specialsource/controller/AuthController.java +++ b/src/main/java/company/specialsource/controller/AuthController.java @@ -1,26 +1,39 @@ package company.specialsource.controller; +import company.specialsource.config.JwtTokenProvider; import company.specialsource.dto.LoginRequest; import company.specialsource.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.HashMap; +import java.util.Map; + @RestController @RequestMapping("/api") -@RequiredArgsConstructor +@RequiredArgsConstructor // 롬복이 jwtTokenProvider와 userService의 생성자를 자동으로 만들어줍니다. public class AuthController { - private final UserService userService; + private final JwtTokenProvider jwtTokenProvider; + private final UserService userService; // 실제 서비스 주입 @PostMapping("/login") - public ResponseEntity login(@RequestBody LoginRequest loginRequest) { - boolean isSuccess = userService.login(loginRequest.getUsername(), loginRequest.getPassword()); + public ResponseEntity login(@RequestBody LoginRequest request) { - if (isSuccess) { - return ResponseEntity.ok("로그인 성공!!!"); - } else { - return ResponseEntity.status(401).body("아이디 또는 비밀번호가 틀렸습니다."); + // 1. 기존 하드코딩("admin", "1234") 대신 실제 DB를 조회하는 서비스 메서드 호출 + if (userService.login(request.getUsername(), request.getPassword())) { + + // 2. 로그인 성공 시, 입력받은 username으로 JWT 토큰 생성 + String token = jwtTokenProvider.createToken(request.getUsername()); + + // 3. 응답 상자에 토큰을 담아서 리턴 + Map response = new HashMap<>(); + response.put("token", token); + return ResponseEntity.ok(response); } + + // 4. DB에 유저가 없거나 비밀번호가 틀리면 401 반환 + return ResponseEntity.status(401).body("아이디 또는 비밀번호가 틀렸습니다."); } } \ No newline at end of file diff --git a/src/main/java/company/specialsource/dto/LoginRequest.java b/src/main/java/company/specialsource/dto/LoginRequest.java index 8a44b88..34d7e7a 100644 --- a/src/main/java/company/specialsource/dto/LoginRequest.java +++ b/src/main/java/company/specialsource/dto/LoginRequest.java @@ -1,10 +1,12 @@ -package company.specialsource.dto; +package company.specialsource.dto; // 패키지 위치에 맞게 조정하세요 -import lombok.Getter; -import lombok.Setter; - -@Getter @Setter public class LoginRequest { private String username; private String password; + + // Getter, Setter (인텔리제이에서 롬복 @Getter, @Setter 쓰셔도 됩니다!) + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } } \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 45ec844..9640b51 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -15,4 +15,10 @@ spring: jasypt: encryptor: - password: 81sungjin1015 \ No newline at end of file + password: 81sungjin1015 + +jwt: + # 256비트 이상이어야 하므로 영문+숫자 (최소 32글자 이상) + secret: c3ByaW5nYm9vdC1qd3Qtc2VjcmV0LWtleS1zcGVjaWFsc291cmNlLWhvbWVwYWdlLWJhY2tlbmQtMjAyNg== + # 토큰 유효 시간 (1시간으로 설정 = 60분 * 60초 * 1000밀리초) + expiration-time: 3600000 \ No newline at end of file