Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules/blog_manage.main.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 21 additions & 11 deletions blog_manage/build.gradle
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.6'
id 'io.spring.dependency-management' version '1.1.7'
id 'org.cyclonedx.bom' version '2.3.0'
id 'org.springframework.boot' version '3.3.5'
id 'io.spring.dependency-management' version '1.1.6'
}

group = 'com.leets.backend'
Expand All @@ -20,20 +19,31 @@ repositories {
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
// Spring Boot 기본
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

runtimeOnly 'com.h2database:h2'
// DB
runtimeOnly 'com.h2database:h2' // 테스트용
runtimeOnly 'mysql:mysql-connector-java:8.0.33' // 실제 배포용

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'mysql:mysql-connector-java:8.0.33'
// OpenAPI (Swagger UI)
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'

// 설정 메타데이터
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'

// 테스트
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,17 @@ public static <T> ApiResponse<T> onFailure(HttpStatus status, String message, T
return response;
}

// Getter 추가 (JSON 직렬화용)
public int getStatus() {
return status;
// 데이터가 없을 때 간단하게 사용
public static <T> ApiResponse<T> onSuccess(String message) {
return onSuccess(HttpStatus.OK, message, null);
}

public String getMessage() {
return message;
public static <T> ApiResponse<T> onFailure(HttpStatus status, String message) {
return onFailure(status, message, null);
}

public T getData() {
return data;
}
// Getter
public int getStatus() { return status; }
public String getMessage() { return message; }
public T getData() { return data; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.leets.backend.blog.config;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import com.leets.backend.blog.service.CustomUserDetailsService;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import java.io.IOException;

public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenProvider jwtTokenProvider;
private final CustomUserDetailsService userDetailsService;

public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider, CustomUserDetailsService userDetailsService) {
this.jwtTokenProvider = jwtTokenProvider;
this.userDetailsService = userDetailsService;
}

@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws ServletException, IOException {
String header = req.getHeader("Authorization");
String token = null;
if (header != null && header.startsWith("Bearer ")) {
token = header.substring(7);
}
if (token != null && jwtTokenProvider.validateToken(token)) {
String email = jwtTokenProvider.getSubject(token);
var userDetails = userDetailsService.loadUserByUsername(email);
var auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
SecurityContextHolder.getContext().setAuthentication(auth);
}
chain.doFilter(req, res);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.leets.backend.blog.config;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.*;
import java.io.IOException;

@Component
public class JwtEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401
response.setContentType("application/json; charset=UTF-8");

// 인증 실패 메세지
String json = ("{\"message\":\"Unauthorized - 인증이 필요합니다.\"}");

response.getWriter().write(json);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.leets.backend.blog.config;

import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;

@Component
public class JwtTokenProvider {

private final Key key;
private final long accessTokenValidityMs;
private final long refreshTokenValidityMs;

public JwtTokenProvider(
@Value("${spring.jwt.secret}") String secret,
@Value("${spring.jwt.access-token-validity}") long accessValidity,
@Value("${spring.jwt.refresh-token-validity}") long refreshValidity) {

this.key = Keys.hmacShaKeyFor(secret.getBytes());
this.accessTokenValidityMs = accessValidity;
this.refreshTokenValidityMs = refreshValidity;
}

public String createAccessToken(String subject, String role) {
Date now = new Date();
Date expiry = new Date(now.getTime() + accessTokenValidityMs);

return Jwts.builder()
.setSubject(subject)
.claim("role", role)
.setIssuedAt(now)
.setExpiration(expiry)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}

public String createRefreshToken(String subject) {
Date now = new Date();
Date expiry = new Date(now.getTime() + refreshTokenValidityMs);

return Jwts.builder()
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expiry)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}

public String generateToken(String subject) {
return createAccessToken(subject, "ROLE_USER");
}

public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException ex) {
return false;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 validationToken 메서드에서 예외 발생 시 false만 반환하고 있는데, 어떤 이유로 토큰 검증에 실패했는지 로그에 남기면 디버깅에 도움이 된다고 합니다!

} catch (JwtException | IllegalArgumentException ex) {
    log.warn("Invalid JWT token: {}", ex.getMessage()); 
    return false;
}

이런식으로 하는 게 좋다고 하네요 저도 몰랐는데 알아갑니당 👀👀

}
}

public String getSubject(String token) {
return Jwts.parser().setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}

public Date getExpiration(String token) {
return Jwts.parser().setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody()
.getExpiration();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.leets.backend.blog.config;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OpenApiConfig {

@Bean
public OpenAPI openAPI() {
// Security Scheme 이름 정의
String securitySchemeName = "Bearer Authentication";

// 모든 API에 이 Security Requirement를 적용
SecurityRequirement securityRequirement = new SecurityRequirement()
.addList(securitySchemeName);

// JWT Bearer Token을 위한 Security Scheme 정의
SecurityScheme securityScheme = new SecurityScheme()
.name(securitySchemeName)
.type(SecurityScheme.Type.HTTP) // HTTP 인증 방식
.scheme("bearer") // Bearer 토큰 사용
.bearerFormat("JWT") // JWT 형식
.description("JWT 토큰을 입력하세요. 'Bearer ' 접두사는 자동으로 추가됩니다.");

return new OpenAPI()
.info(new Info()
.title("Blog Management API")
.description("블로그 관리 시스템 API 문서")
.version("1.0.0"))
.addSecurityItem(securityRequirement) // 모든 API에 보안 적용
.components(new Components()
.addSecuritySchemes(securitySchemeName, securityScheme));
}
}
Loading