Skip to content

Commit e713c93

Browse files
committed
Feat: JWT 기반 인증 기능 추가
1 parent ec8a010 commit e713c93

File tree

3 files changed

+254
-0
lines changed

3 files changed

+254
-0
lines changed
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: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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.web.filter.OncePerRequestFilter;
11+
12+
import java.io.IOException;
13+
14+
/**
15+
* JWT 인증을 처리하는 필터
16+
* - 모든 요청에 대해 JWT 토큰을 검사
17+
* - 토큰이 유효하면 Authentication 객체를 생성하여 SecurityContext에 저장
18+
*/
19+
@RequiredArgsConstructor
20+
public class JwtAuthenticationFilter extends OncePerRequestFilter {
21+
private final JwtTokenProvider jwtTokenProvider;
22+
23+
@Override
24+
protected void doFilterInternal(
25+
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain
26+
) throws ServletException, IOException {
27+
28+
// Request Header에서 토큰 추출
29+
String token = resolveToken(request);
30+
31+
// 토큰이 유효한 경우에만 Authentication 객체 생성 및 SecurityContext에 저장
32+
if (token != null && jwtTokenProvider.validateToken(token)) {
33+
Authentication authentication = jwtTokenProvider.getAuthentication(token);
34+
SecurityContextHolder.getContext().setAuthentication(authentication);
35+
}
36+
37+
// 다음 필터로 요청 전달
38+
filterChain.doFilter(request, response);
39+
}
40+
41+
/**
42+
* Request의 Authorization 헤더에서 JWT 토큰을 추출
43+
*
44+
* @param request HTTP 요청 객체
45+
* @return JWT 토큰 문자열 또는 null
46+
*/
47+
private String resolveToken(HttpServletRequest request) {
48+
String bearerToken = request.getHeader("Authorization");
49+
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
50+
return bearerToken.substring(7);
51+
}
52+
return null;
53+
}
54+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package com.back.global.security;
2+
3+
import com.back.global.exception.CustomException;
4+
import com.back.global.exception.ErrorCode;
5+
import io.jsonwebtoken.Claims;
6+
import io.jsonwebtoken.ExpiredJwtException;
7+
import io.jsonwebtoken.JwtException;
8+
import io.jsonwebtoken.Jwts;
9+
import io.jsonwebtoken.security.Keys;
10+
import jakarta.annotation.PostConstruct;
11+
import org.springframework.beans.factory.annotation.Value;
12+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
13+
import org.springframework.security.core.Authentication;
14+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
15+
import org.springframework.stereotype.Component;
16+
17+
import javax.crypto.SecretKey;
18+
import java.util.Date;
19+
import java.util.List;
20+
21+
/**
22+
* JWT 생성 및 검증을 담당하는 Provider 클래스
23+
* - Access Token, Refresh Token 생성
24+
* - 토큰 검증 및 파싱
25+
* - Authentication 객체 생성 (Spring Security 연동)
26+
*/
27+
@Component
28+
public class JwtTokenProvider {
29+
30+
@Value("${jwt.secret}")
31+
private String secretKey;
32+
33+
@Value("${jwt.access-token-expiration}")
34+
private long accessTokenExpirationInSeconds;
35+
36+
@Value("${jwt.refresh-token-expiration}")
37+
private long refreshTokenExpirationInSeconds;
38+
39+
private SecretKey key;
40+
41+
@PostConstruct
42+
public void init() {
43+
this.key = Keys.hmacShaKeyFor(secretKey.getBytes());
44+
}
45+
46+
/**
47+
* Access Token 생성
48+
*
49+
* @param userId 사용자 PK
50+
* @param username 로그인 ID
51+
* @param role 권한
52+
* @return JWT Access Token 문자열
53+
*/
54+
public String createAccessToken(Long userId, String username, String role) {
55+
Date now = new Date();
56+
Date expiryDate = new Date(now.getTime() + accessTokenExpirationInSeconds * 1000);
57+
58+
return Jwts.builder()
59+
.subject(username)
60+
.claim("userId", userId)
61+
.claim("role", role)
62+
.issuedAt(now)
63+
.expiration(expiryDate)
64+
.signWith(key)
65+
.compact();
66+
}
67+
68+
/**
69+
* Refresh Token 생성
70+
*
71+
* @param userId 사용자 PK
72+
* @return JWT Refresh Token 문자열
73+
*/
74+
public String createRefreshToken(Long userId) {
75+
Date now = new Date();
76+
Date expiryDate = new Date(now.getTime() + refreshTokenExpirationInSeconds * 1000);
77+
78+
return Jwts.builder()
79+
.subject(String.valueOf(userId))
80+
.issuedAt(now)
81+
.expiration(expiryDate)
82+
.signWith(key)
83+
.compact();
84+
}
85+
86+
/**
87+
* JWT 토큰에서 인증 정보 추출
88+
*
89+
* @param token JWT Access Token
90+
* @return 인증 정보가 담긴 Authentication 객체
91+
*/
92+
public Authentication getAuthentication(String token) {
93+
Claims claims = parseClaims(token);
94+
Long userId = claims.get("userId", Long.class);
95+
String username = claims.getSubject();
96+
String role = claims.get("role", String.class);
97+
98+
SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + role);
99+
CustomUserDetails principal = new CustomUserDetails(userId, username, role);
100+
101+
return new UsernamePasswordAuthenticationToken(principal, token, List.of(authority));
102+
}
103+
104+
/**
105+
* JWT 토큰 검증
106+
*
107+
* @param token JWT Access Token
108+
* @return 유효한 토큰이면 true, 그렇지 않으면 false
109+
*/
110+
public boolean validateToken(String token) {
111+
try {
112+
Jwts.parser()
113+
.verifyWith(key)
114+
.build()
115+
.parseSignedClaims(token);
116+
return true;
117+
} catch (JwtException e) {
118+
return false;
119+
}
120+
}
121+
122+
/**
123+
* JWT 파싱
124+
*
125+
* @param token JWT 토큰
126+
* @return 토큰의 Claims
127+
* @throws CustomException 토큰이 유효하지 않은 경우
128+
*/
129+
private Claims parseClaims(String token) {
130+
try {
131+
return Jwts.parser()
132+
.verifyWith(key)
133+
.build()
134+
.parseSignedClaims(token)
135+
.getPayload();
136+
} catch (ExpiredJwtException e) {
137+
return e.getClaims();
138+
} catch (JwtException e) {
139+
throw new CustomException(ErrorCode.INVALID_TOKEN);
140+
}
141+
}
142+
}

0 commit comments

Comments
 (0)