Skip to content

Commit 390ddec

Browse files
committed
API요청시 JWT 검증
1 parent 9f27dcb commit 390ddec

File tree

7 files changed

+123
-14
lines changed

7 files changed

+123
-14
lines changed

backend/src/main/java/com/backend/domain/user/entity/User.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
package com.backend.domain.user.entity;
22

33
import jakarta.persistence.*;
4-
import jakarta.validation.constraints.NotBlank;
5-
import jakarta.validation.constraints.NotNull;
64
import lombok.Getter;
75
import lombok.NoArgsConstructor;
86
import org.hibernate.annotations.Where;
9-
import org.springframework.cglib.core.Local;
107
import org.springframework.data.annotation.CreatedDate;
118
import org.springframework.data.annotation.LastModifiedDate;
129
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

backend/src/main/java/com/backend/domain/user/service/EmailService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public void sendEmail(String email) throws MessagingException {
4646
helper.setSubject("[임시 서비스 이름] 회원가입 인증 코드입니다.");
4747

4848
// 이메일 본문 (HTML 형식으로 보냄)
49-
String content = "<h2>안녕하세요. [임시 서비스 이름]입니다.</h2>"
49+
String content = "<h2>안녕하세요. [PortfolioIQ]입니다.</h2>"
5050
+ "<p>아래 6자리 인증 코드를 인증 창에 입력해 주세요.</p>"
5151
+ "<div style='font-size: 24px; font-weight: bold; color: #1e88e5;'>"
5252
+ authCode

backend/src/main/java/com/backend/domain/user/service/UserService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public User join(@NotBlank String email, @NotBlank String password, @NotBlank St
4848

4949
//검증이 완료되었다면 해당 email을 redis에서 삭제
5050
if(redisUtil.deleteData("VERIFIED_EMAIL:" + email)) {
51-
System.out.println("VERIFIED_EMAIL:" + email + "은 삭제가 됐습니다.");
51+
System.out.println("VERIFIED_EMAIL:" + email + "은 email검증이 완료되어서 redis에서 삭제가 됐습니다.");
5252
}else{
5353
System.out.println("redis 삭제 실패입니다.");
5454
}

backend/src/main/java/com/backend/domain/user/util/JwtUtil.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
import org.springframework.beans.factory.annotation.Value;
99
import org.springframework.stereotype.Component;
1010

11+
import javax.crypto.SecretKey;
1112
import java.nio.charset.StandardCharsets;
12-
import java.security.Key;
1313
import java.util.Date;
1414
import java.util.HashMap;
1515
import java.util.Map;
@@ -22,8 +22,8 @@ public class JwtUtil {
2222
@Value("${jwt.access-token-expiration-in-milliseconds}")
2323
private int tokenValidityMilliSeconds;
2424

25-
//SecretKey를 Base64로 인코딩하여 Key객체로 변환
26-
private Key key;
25+
//SecretKey를 Base64로 인코딩하여 SecretKey객체로 변환
26+
private SecretKey key;
2727

2828
//key값 초기화
2929
@PostConstruct //의존성 주입이 될때 딱 1번만 실행되기 때문에 key값은 이후로 변하지 않음
@@ -39,9 +39,7 @@ public String createToken(String email, String name) {
3939
claims.put("name", name);
4040

4141
Date now = new Date();
42-
System.out.println("now : "+now.getTime());
4342
Date expiration = new Date(now.getTime() + tokenValidityMilliSeconds);
44-
System.out.println("expiration : "+expiration.getTime());
4543
return Jwts.builder()
4644
.claims(claims)
4745
.issuedAt(now)
@@ -50,4 +48,13 @@ public String createToken(String email, String name) {
5048
.compact();
5149
}
5250

51+
//JWT 검증 및 Claims 추출
52+
public Claims parseClaims(String token) {
53+
return Jwts.parser()
54+
.verifyWith(key)
55+
.build()
56+
.parseSignedClaims(token)
57+
.getPayload();
58+
59+
}
5360
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package com.backend.global.security;
2+
3+
import com.backend.domain.user.util.JwtUtil;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import io.jsonwebtoken.Claims;
6+
import io.jsonwebtoken.ExpiredJwtException;
7+
import io.jsonwebtoken.JwtException;
8+
import jakarta.servlet.FilterChain;
9+
import jakarta.servlet.ServletException;
10+
import jakarta.servlet.http.HttpServletRequest;
11+
import jakarta.servlet.http.HttpServletResponse;
12+
import lombok.RequiredArgsConstructor;
13+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
14+
import org.springframework.security.core.Authentication;
15+
import org.springframework.security.core.context.SecurityContextHolder;
16+
import org.springframework.web.filter.OncePerRequestFilter;
17+
18+
import java.io.IOException;
19+
import java.time.LocalDateTime;
20+
import java.util.Collections;
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
24+
@RequiredArgsConstructor
25+
public class JwtAuthenticationFilter extends OncePerRequestFilter {
26+
private final JwtUtil jwtUtil;
27+
28+
29+
@Override
30+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
31+
String authorizationHeader = request.getHeader("Authorization");
32+
String token = null;
33+
34+
//"Bearer " 제거
35+
if(authorizationHeader != null && authorizationHeader.startsWith("Bearer ")){
36+
token = authorizationHeader.substring(7);
37+
}
38+
39+
//토큰이 있다면 검증 및 인증
40+
if(token != null) {
41+
try {
42+
Claims claims = jwtUtil.parseClaims(token);
43+
44+
String email = claims.getSubject(); //"sub"값 가져오기
45+
46+
//추출된 정보로 Spring Security 인증 객체 생성 (파싱)
47+
Authentication authentication = null;
48+
if (email != null) {
49+
//나중에 권한을 추가하면 Collections.emptyList()대신 넣을것
50+
authentication = new UsernamePasswordAuthenticationToken(
51+
email,
52+
null,
53+
Collections.emptyList());
54+
}
55+
56+
// SecurityContextHolder에 인증 정보 저장
57+
SecurityContextHolder.getContext().setAuthentication(authentication);
58+
}catch (ExpiredJwtException e) {
59+
// 토큰 만료 시
60+
sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED, "Token Expired : 토큰의 유효기간이 지났습니다.");
61+
return;
62+
} catch (JwtException e) {
63+
// 서명 불일치, 토큰 형식 오류 등
64+
sendErrorResponse(response, HttpServletResponse.SC_FORBIDDEN, "Invalid Token : 올바른 토큰 값이 아닙니다.");
65+
return;
66+
}
67+
68+
}
69+
// 다음 필터로 요청 전달
70+
filterChain.doFilter(request, response);
71+
}
72+
73+
private void sendErrorResponse(HttpServletResponse response, int status, String message) throws IOException {
74+
response.setContentType("application/json;charset=UTF-8");
75+
response.setStatus(status);
76+
77+
Map<String, Object> errorDetails = new HashMap<>();
78+
errorDetails.put("status", status);
79+
errorDetails.put("error", "Authentication Failed");
80+
errorDetails.put("message", message);
81+
errorDetails.put("timestamp", LocalDateTime.now().toString());
82+
83+
//Map을 JSON 문자열로 변환하여 응답 본문에 작성
84+
ObjectMapper objectMapper = new ObjectMapper();
85+
response.getWriter().write(objectMapper.writeValueAsString(errorDetails));
86+
}
87+
}

backend/src/main/java/com/backend/global/security/SecurityConfig.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,45 @@
11
package com.backend.global.security;
22

3+
import com.backend.domain.user.util.JwtUtil;
4+
import lombok.RequiredArgsConstructor;
35
import org.springframework.context.annotation.Bean;
46
import org.springframework.context.annotation.Configuration;
57
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
68
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
9+
import org.springframework.security.config.http.SessionCreationPolicy;
710
import org.springframework.security.web.SecurityFilterChain;
11+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
812

913
@Configuration
1014
@EnableWebSecurity
15+
@RequiredArgsConstructor
1116
public class SecurityConfig {
1217

18+
private final JwtUtil jwtUtil;
19+
1320
@Bean
1421
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
1522
http
16-
.csrf(csrf -> csrf.disable())
23+
// JWT 인증을 사용하므로 세션을 사용하지 않음 (Stateless)
24+
.sessionManagement(session -> session
25+
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
26+
)
1727

1828
// H2 콘솔은 frameOptions 해제 필요 (iframe으로 동작)
1929
.headers(headers -> headers
2030
.frameOptions(frame -> frame.disable())
2131
)
32+
33+
// CSRF 공격 방지 비활성화 (Stateless API에서 주로 사용)
2234
// H2 콘솔은 CSRF 예외로 설정
2335
.csrf(csrf -> csrf.disable())
2436

2537
.authorizeHttpRequests(auth -> auth
2638
.requestMatchers(
27-
"/api/**",
39+
"/api/login", //로그인
40+
"/api/auth", //이메인 인증코드 전송
41+
"/api/verify",//이메일 인증코드 검증
42+
"/api/user", //회원가입
2843
"/v3/api-docs/**",
2944
"/swagger-ui/**",
3045
"/swagger-resources/**",
@@ -40,7 +55,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
4055
.anyRequest().authenticated()
4156
)
4257
.formLogin(login -> login.disable())
43-
.httpBasic(basic -> basic.disable());
58+
.httpBasic(basic -> basic.disable())
59+
//커스텀 JWT 필터를 등록
60+
.addFilterBefore(new JwtAuthenticationFilter(jwtUtil),
61+
UsernamePasswordAuthenticationFilter.class);
4462

4563
return http.build();
4664
}

backend/src/main/resources/application.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,4 @@ github:
4343

4444
jwt:
4545
secret: ${SECRET_KEY}
46-
access-token-expiration-in-milliseconds: 600000
46+
access-token-expiration-in-milliseconds: 600000 # 600 * 1000

0 commit comments

Comments
 (0)