Skip to content

Commit f76e04b

Browse files
committed
feat: JWT 인증 필터 추가 및 인증 처리/토큰 재발급 로직 설정 (#4)
1 parent 2c1fdb3 commit f76e04b

File tree

3 files changed

+163
-7
lines changed

3 files changed

+163
-7
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package com.project.mapdagu.jwt.filter;
2+
3+
import com.project.mapdagu.domain.member.entity.Member;
4+
import com.project.mapdagu.domain.member.repository.MemberRepository;
5+
import com.project.mapdagu.error.ErrorCode;
6+
import com.project.mapdagu.error.exception.custom.TokenException;
7+
import com.project.mapdagu.jwt.service.JwtService;
8+
import com.project.mapdagu.jwt.util.PasswordUtil;
9+
import com.project.mapdagu.util.RedisUtil;
10+
import jakarta.servlet.FilterChain;
11+
import jakarta.servlet.ServletException;
12+
import jakarta.servlet.http.HttpServletRequest;
13+
import jakarta.servlet.http.HttpServletResponse;
14+
import lombok.RequiredArgsConstructor;
15+
import lombok.extern.slf4j.Slf4j;
16+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
17+
import org.springframework.security.core.Authentication;
18+
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
19+
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
20+
import org.springframework.security.core.context.SecurityContextHolder;
21+
import org.springframework.security.core.userdetails.UserDetails;
22+
import org.springframework.web.filter.OncePerRequestFilter;
23+
24+
import java.io.IOException;
25+
26+
/**
27+
* Jwt 인증 필터
28+
* "/login" 이외의 URI 요청이 왔을 때 처리하는 필터
29+
*/
30+
@RequiredArgsConstructor
31+
@Slf4j
32+
public class JwtAuthenticationProcessingFilter extends OncePerRequestFilter {
33+
34+
private static final String NO_CHECK_URL = "/login"; // "/login"으로 들어오는 요청은 Filter 작동 X
35+
36+
private final JwtService jwtService;
37+
private final MemberRepository memberRepository;
38+
private final RedisUtil redisUtil;
39+
40+
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
41+
42+
@Override
43+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, ServletException, IOException {
44+
if (request.getRequestURI().equals(NO_CHECK_URL)) {
45+
filterChain.doFilter(request, response); // "/login" 요청이 들어오면, 다음 필터 호출
46+
return;
47+
}
48+
49+
// 사용자 요청 헤더에서 RefreshToken 추출-> RefreshToken이 없거나 유효하지 않다면 null
50+
String refreshToken = jwtService.extractRefreshToken(request)
51+
.filter(jwtService::isTokenValid)
52+
.orElse(null);
53+
String email = jwtService.extractEmail(refreshToken).orElseThrow(() -> new TokenException(ErrorCode.INVALID_TOKEN));
54+
55+
// 리프레시 토큰이 요청 헤더에 존재하고 유효하다면, AccessToken이 만료된 것 -> AccessToken 재발급
56+
if (refreshToken != null && isRefreshTokenMatch(email, refreshToken)) {
57+
String newAccessToken = jwtService.createAccessToken(email);
58+
String newRefreshToken = jwtService.createRefreshToken(email);
59+
jwtService.updateRefreshToken(email, newRefreshToken);
60+
jwtService.sendAccessAndRefreshToken(response, newAccessToken, refreshToken);
61+
return;
62+
}
63+
64+
// AccessToken을 검사하고 인증 처리
65+
// AccessToken이 없거나 유효하지 않다면, 인증 객체가 담기지 않은 상태로 다음 필터로 넘어가기 때문에 403 에러 발생
66+
// AccessToken이 유효하다면, 인증 객체가 담긴 상태로 다음 필터로 넘어가기 때문에 인증 성공
67+
else {
68+
checkAccessTokenAndAuthentication(request, response, filterChain);
69+
}
70+
}
71+
72+
public boolean isRefreshTokenMatch(String email, String refreshToken) {
73+
if (redisUtil.get(email).equals(refreshToken)) {
74+
return true;
75+
}
76+
throw new TokenException(ErrorCode.INVALID_TOKEN);
77+
}
78+
79+
/**
80+
* [액세스 토큰 체크 & 인증 처리 메소드]
81+
* 인증 허가 처리된 객체를 SecurityContextHolder에 담기
82+
*/
83+
public void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response,
84+
FilterChain filterChain) throws ServletException, IOException {
85+
log.info("checkAccessTokenAndAuthentication() 호출");
86+
jwtService.extractAccessToken(request)
87+
.filter(jwtService::isTokenValid)
88+
.ifPresent(accessToken -> jwtService.extractEmail(accessToken)
89+
.ifPresent(email -> memberRepository.findByEmail(email)
90+
.ifPresent(this::saveAuthentication)));
91+
92+
filterChain.doFilter(request, response);
93+
}
94+
95+
/**
96+
* [인증 허가 메소드]
97+
* 파라미터의 유저 : 우리가 만든 회원 객체 / 빌더의 유저 : UserDetails의 User 객체
98+
*/
99+
public void saveAuthentication(Member member) {
100+
String password = member.getPassword();
101+
if (password == null) { // 소셜 로그인 유저의 비밀번호 임의로 설정 하여 소셜 로그인 유저도 인증 되도록 설정
102+
password = PasswordUtil.generateRandomPassword();
103+
}
104+
105+
UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder()
106+
.username(member.getEmail())
107+
.password(password)
108+
.roles(member.getRole().name())
109+
.build();
110+
111+
Authentication authentication =
112+
new UsernamePasswordAuthenticationToken(userDetailsUser, null,
113+
authoritiesMapper.mapAuthorities(userDetailsUser.getAuthorities()));
114+
115+
SecurityContextHolder.getContext().setAuthentication(authentication);
116+
}
117+
}

src/main/java/com/project/mapdagu/jwt/service/JwtService.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.auth0.jwt.JWT;
44
import com.auth0.jwt.algorithms.Algorithm;
55
import com.project.mapdagu.domain.member.repository.MemberRepository;
6+
import jakarta.servlet.http.Cookie;
67
import jakarta.servlet.http.HttpServletRequest;
78
import jakarta.servlet.http.HttpServletResponse;
89
import lombok.Getter;
@@ -25,10 +26,10 @@ public class JwtService {
2526
private String secretKey;
2627

2728
@Value("${jwt.access.expiration}")
28-
private Long accessTokenExpirationPeriod;
29+
private Integer accessTokenExpirationPeriod;
2930

3031
@Value("${jwt.refresh.expiration}")
31-
private Long refreshTokenExpirationPeriod;
32+
private Integer refreshTokenExpirationPeriod;
3233

3334
@Value("${jwt.access.header}")
3435
private String accessHeader;
@@ -52,19 +53,19 @@ public String createAccessToken(String email) {
5253
return JWT.create()
5354
.withSubject(ACCESS_TOKEN_SUBJECT)
5455
.withExpiresAt(new Date(now.getTime() + accessTokenExpirationPeriod)) // 토큰 만료 시간 설정
55-
// claim -> email 사용
5656
.withClaim(EMAIL_CLAIM, email)
5757
.sign(Algorithm.HMAC512(secretKey));
5858
}
5959

6060
/**
61-
* RefreshToken 생성 (claim에 email 필요 x)
61+
* RefreshToken 생성
6262
*/
63-
public String createRefreshToken() {
63+
public String createRefreshToken(String email) {
6464
Date now = new Date();
6565
return JWT.create()
6666
.withSubject(REFRESH_TOKEN_SUBJECT)
6767
.withExpiresAt(new Date(now.getTime() + refreshTokenExpirationPeriod))
68+
.withClaim(EMAIL_CLAIM, email)
6869
.sign(Algorithm.HMAC512(secretKey));
6970
}
7071

@@ -77,6 +78,15 @@ public void sendAccessToken(HttpServletResponse response, String accessToken) {
7778
log.info("재발급된 Access Token : {}", accessToken);
7879
}
7980

81+
public void sendRefreshToken(HttpServletResponse response, String refreshToken) {
82+
Cookie cookie = new Cookie(REFRESH_TOKEN_SUBJECT, refreshToken);
83+
cookie.setMaxAge(refreshTokenExpirationPeriod);
84+
cookie.setSecure(true);
85+
cookie.setHttpOnly(true);
86+
cookie.setPath("/");
87+
response.addCookie(cookie);
88+
}
89+
8090
/**
8191
* 로그인 시 AccessToken + RefreshToken 헤더에 실어서 보내기
8292
*/
@@ -109,11 +119,11 @@ public Optional<String> extractAccessToken(HttpServletRequest request) {
109119
* AccessToken에서 Email 추출
110120
* AceessToken 검증 후 이메일 추출 (유효하지 않다면 빈 Optional 객체 반환)
111121
*/
112-
public Optional<String> extractEmail(String accessToken) {
122+
public Optional<String> extractEmail(String token) {
113123
try {
114124
return Optional.ofNullable(JWT.require(Algorithm.HMAC512(secretKey))
115125
.build()
116-
.verify(accessToken)
126+
.verify(token)
117127
.getClaim(EMAIL_CLAIM)
118128
.asString());
119129
} catch (Exception e) {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.project.mapdagu.jwt.util;
2+
3+
import java.util.Random;
4+
5+
public class PasswordUtil {
6+
public static String generateRandomPassword() {
7+
int index = 0;
8+
char[] charSet = new char[] {
9+
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
10+
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
11+
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
12+
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
13+
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
14+
}; //배열안의 문자 숫자는 원하는대로
15+
16+
StringBuffer password = new StringBuffer();
17+
Random random = new Random();
18+
19+
for (int i = 0; i < 8 ; i++) {
20+
double rd = random.nextDouble();
21+
index = (int) (charSet.length * rd);
22+
23+
password.append(charSet[index]);
24+
}
25+
System.out.println(password);
26+
return password.toString();
27+
//StringBuffer를 String으로 변환해서 return 하려면 toString()을 사용하면 된다.
28+
}
29+
}

0 commit comments

Comments
 (0)