Skip to content

Commit 7719f9b

Browse files
committed
# Conflicts: # build.gradle.kts # src/main/resources/application-dev.yml
2 parents ef8eaee + 95b305e commit 7719f9b

File tree

13 files changed

+489
-33
lines changed

13 files changed

+489
-33
lines changed

.env.default

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
JWT_SECRET=your-secret-key

build.gradle.kts

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -25,39 +25,25 @@ repositories {
2525
}
2626

2727
dependencies {
28-
// Spring
28+
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
2929
implementation("org.springframework.boot:spring-boot-starter-web")
3030
implementation("org.springframework.boot:spring-boot-starter-websocket")
31-
32-
// Database & JPA
33-
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
34-
runtimeOnly("com.h2database:h2")
35-
runtimeOnly("com.mysql:mysql-connector-j")
36-
37-
// QueryDSL
38-
implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta")
39-
annotationProcessor("com.querydsl:querydsl-apt:5.0.0:jakarta")
40-
annotationProcessor("jakarta.persistence:jakarta.persistence-api")
41-
annotationProcessor("jakarta.annotation:jakarta.annotation-api")
42-
43-
// Security
4431
implementation("org.springframework.boot:spring-boot-starter-security")
45-
46-
// Development Tools
32+
testImplementation("org.springframework.security:spring-security-test")
4733
compileOnly("org.projectlombok:lombok")
48-
annotationProcessor("org.projectlombok:lombok")
4934
developmentOnly("org.springframework.boot:spring-boot-devtools")
50-
51-
// Swagger
35+
runtimeOnly("com.h2database:h2")
36+
runtimeOnly("com.mysql:mysql-connector-j")
37+
annotationProcessor("org.projectlombok:lombok")
38+
testImplementation("org.springframework.boot:spring-boot-starter-test")
39+
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
5240
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13")
53-
54-
// Env
5541
implementation ("io.github.cdimascio:dotenv-java:3.0.0")
5642

57-
// Test
58-
testImplementation("org.springframework.boot:spring-boot-starter-test")
59-
testImplementation("org.springframework.security:spring-security-test")
60-
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
43+
// JWT
44+
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
45+
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
46+
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
6147
}
6248

6349
tasks.withType<Test> {

src/main/java/com/back/global/exception/ErrorCode.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,16 @@ public enum ErrorCode {
2525
// ======================== 공통 에러 ========================
2626
BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON_400", "잘못된 요청입니다."),
2727
FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON_403", "접근 권한이 없습니다."),
28-
NOT_FOUND(HttpStatus.NOT_FOUND, "COMMON_404", "요청하신 리소스를 찾을 수 없습니다.");
28+
NOT_FOUND(HttpStatus.NOT_FOUND, "COMMON_404", "요청하신 리소스를 찾을 수 없습니다."),
29+
30+
// ======================== 인증/인가 에러 ========================
31+
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "AUTH_401", "인증이 필요합니다."),
32+
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_401", "유효하지 않은 토큰입니다."),
33+
EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_401", "만료된 액세스 토큰입니다."),
34+
EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "AUTH_401", "만료된 리프레시 토큰입니다."),
35+
REFRESH_TOKEN_REUSE(HttpStatus.FORBIDDEN, "AUTH_403", "재사용된 리프레시 토큰입니다."),
36+
ACCESS_DENIED(HttpStatus.FORBIDDEN, "AUTH_403", "권한이 없습니다.");
37+
2938

3039
private final HttpStatus status;
3140
private final String code;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.back.global.security;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
6+
import org.springframework.security.core.userdetails.UserDetails;
7+
8+
import java.util.Collection;
9+
import java.util.List;
10+
11+
/**
12+
* Spring Security에서 사용하는 사용자 인증 정보 클래스
13+
* - JWT에서 파싱한 사용자 정보를 담고 있음
14+
*/
15+
@Getter
16+
@AllArgsConstructor
17+
public class CustomUserDetails implements UserDetails {
18+
private Long userId;
19+
private String username;
20+
private String role;
21+
22+
@Override
23+
public Collection<SimpleGrantedAuthority> getAuthorities() {
24+
// Spring Security 권한 체크는 "ROLE_" prefix 필요
25+
return List.of(new SimpleGrantedAuthority("ROLE_" + role));
26+
}
27+
28+
@Override
29+
public String getPassword() {
30+
// JWT 인증에서는 비밀번호를 사용하지 않음
31+
return null;
32+
}
33+
34+
@Override
35+
public String getUsername() {
36+
return username;
37+
}
38+
39+
@Override
40+
public boolean isAccountNonExpired() {
41+
return true;
42+
}
43+
44+
@Override
45+
public boolean isAccountNonLocked() {
46+
return true;
47+
}
48+
49+
@Override
50+
public boolean isCredentialsNonExpired() {
51+
return true;
52+
}
53+
54+
@Override
55+
public boolean isEnabled() {
56+
return true;
57+
}
58+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.back.global.security;
2+
3+
import com.back.global.common.dto.RsData;
4+
import com.back.global.exception.ErrorCode;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import jakarta.servlet.http.HttpServletRequest;
7+
import jakarta.servlet.http.HttpServletResponse;
8+
import org.springframework.security.access.AccessDeniedException;
9+
import org.springframework.security.web.access.AccessDeniedHandler;
10+
import org.springframework.stereotype.Component;
11+
12+
import java.io.IOException;
13+
14+
/**
15+
* 인가 실패(403 Forbidden) 처리 클래스
16+
* - 인증은 되었으나, 권한(Role)이 부족한 경우
17+
* - Json 형태로 에러 응답을 반환
18+
*/
19+
@Component
20+
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
21+
private final ObjectMapper objectMapper = new ObjectMapper();
22+
23+
@Override
24+
public void handle(
25+
HttpServletRequest request,
26+
HttpServletResponse response,
27+
AccessDeniedException accessDeniedException
28+
) throws IOException {
29+
30+
response.setContentType("application/json;charset=UTF-8");
31+
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
32+
33+
RsData<Void> body = RsData.fail(ErrorCode.ACCESS_DENIED);
34+
35+
response.getWriter().write(objectMapper.writeValueAsString(body));
36+
}
37+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.back.global.security;
2+
3+
import com.back.global.common.dto.RsData;
4+
import com.back.global.exception.ErrorCode;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import jakarta.servlet.http.HttpServletRequest;
7+
import jakarta.servlet.http.HttpServletResponse;
8+
import org.springframework.security.core.AuthenticationException;
9+
import org.springframework.security.web.AuthenticationEntryPoint;
10+
import org.springframework.stereotype.Component;
11+
12+
import java.io.IOException;
13+
14+
/**
15+
* 인증 실패(401 Unauthorized) 처리 클래스
16+
* - JwtAuthenticationFilter에서 토큰이 없거나 잘못된 경우
17+
* - 인증되지 않은 사용자가 보호된 API에 접근하려는 경우
18+
* - Json 형태로 에러 응답을 반환
19+
*/
20+
@Component
21+
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
22+
private final ObjectMapper objectMapper = new ObjectMapper();
23+
24+
@Override
25+
public void commence(
26+
HttpServletRequest request,
27+
HttpServletResponse response,
28+
AuthenticationException authException
29+
) throws IOException {
30+
31+
response.setContentType("application/json;charset=UTF-8");
32+
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
33+
34+
RsData<Void> body = RsData.fail(ErrorCode.UNAUTHORIZED);
35+
36+
response.getWriter().write(objectMapper.writeValueAsString(body));
37+
}
38+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.back.global.security;
2+
3+
import jakarta.servlet.FilterChain;
4+
import jakarta.servlet.ServletException;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpServletResponse;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.security.core.Authentication;
9+
import org.springframework.security.core.context.SecurityContextHolder;
10+
import org.springframework.stereotype.Component;
11+
import org.springframework.web.filter.OncePerRequestFilter;
12+
13+
import java.io.IOException;
14+
15+
/**
16+
* JWT 인증을 처리하는 필터
17+
* - 모든 요청에 대해 JWT 토큰을 검사
18+
* - 토큰이 유효하면 Authentication 객체를 생성하여 SecurityContext에 저장
19+
*/
20+
@Component
21+
@RequiredArgsConstructor
22+
public class JwtAuthenticationFilter extends OncePerRequestFilter {
23+
private final JwtTokenProvider jwtTokenProvider;
24+
25+
@Override
26+
protected void doFilterInternal(
27+
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain
28+
) throws ServletException, IOException {
29+
30+
// Request Header에서 토큰 추출
31+
String token = resolveToken(request);
32+
33+
// 토큰이 유효한 경우에만 Authentication 객체 생성 및 SecurityContext에 저장
34+
if (token != null && jwtTokenProvider.validateToken(token)) {
35+
Authentication authentication = jwtTokenProvider.getAuthentication(token);
36+
SecurityContextHolder.getContext().setAuthentication(authentication);
37+
}
38+
39+
// 다음 필터로 요청 전달
40+
filterChain.doFilter(request, response);
41+
}
42+
43+
/**
44+
* Request의 Authorization 헤더에서 JWT 토큰을 추출
45+
*
46+
* @param request HTTP 요청 객체
47+
* @return JWT 토큰 문자열 또는 null
48+
*/
49+
private String resolveToken(HttpServletRequest request) {
50+
String bearerToken = request.getHeader("Authorization");
51+
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
52+
return bearerToken.substring(7);
53+
}
54+
return null;
55+
}
56+
}

0 commit comments

Comments
 (0)