Skip to content

Commit 8201e15

Browse files
authored
Merge pull request #104 from RealPawParazzi/feature/103/FixLogin
[FIX] refreshToken 발급 방식으로 변경
2 parents 8b55200 + fb9b1ab commit 8201e15

File tree

6 files changed

+187
-24
lines changed

6 files changed

+187
-24
lines changed

src/main/java/pawparazzi/back/member/controller/MemberController.java

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@
22

33
import com.fasterxml.jackson.core.JsonProcessingException;
44
import com.fasterxml.jackson.databind.ObjectMapper;
5-
import jakarta.servlet.http.HttpServletRequest;
65
import jakarta.validation.Valid;
76
import lombok.RequiredArgsConstructor;
87
import org.springframework.http.MediaType;
98
import org.springframework.http.ResponseEntity;
109
import org.springframework.web.bind.annotation.*;
1110
import org.springframework.web.multipart.MultipartFile;
12-
import pawparazzi.back.S3.service.S3AsyncService;
1311
import pawparazzi.back.member.dto.request.LoginRequestDto;
1412
import pawparazzi.back.member.dto.request.SignUpRequestDto;
1513
import pawparazzi.back.member.dto.request.UpdateMemberRequestDto;
@@ -19,7 +17,6 @@
1917
import pawparazzi.back.member.service.MemberService;
2018
import pawparazzi.back.security.util.JwtUtil;
2119

22-
import java.io.IOException;
2320
import java.util.List;
2421
import java.util.Map;
2522
import java.util.concurrent.CompletableFuture;
@@ -33,7 +30,6 @@ public class MemberController {
3330
private final MemberService memberService;
3431
private final ObjectMapper objectMapper;
3532

36-
3733
/**
3834
* 회원 가입
3935
*/
@@ -60,8 +56,8 @@ public CompletableFuture<ResponseEntity<String>> registerUser(
6056
*/
6157
@PostMapping("/login")
6258
public ResponseEntity<Map<String, String>> login(@Valid @RequestBody LoginRequestDto request) {
63-
String token = memberService.login(request);
64-
return ResponseEntity.ok(Map.of("token", token));
59+
Map<String, String> tokenMap = memberService.login(request);
60+
return ResponseEntity.ok(tokenMap);
6561
}
6662

6763
/**
@@ -100,7 +96,7 @@ public CompletableFuture<ResponseEntity<UpdateMemberResponseDto>> updateMember(
10096
}
10197

10298
/**
103-
* 회원 탙퇴
99+
* 회원 탈퇴
104100
*/
105101
@DeleteMapping("/delete")
106102
public ResponseEntity<String> deleteMember(@RequestHeader("Authorization") String token) {
@@ -110,7 +106,6 @@ public ResponseEntity<String> deleteMember(@RequestHeader("Authorization") Strin
110106
return ResponseEntity.ok("회원 탈퇴 완료");
111107
}
112108

113-
114109
/**
115110
* 전체 회원 목록 조회 API
116111
*/
@@ -119,4 +114,24 @@ public ResponseEntity<List<MemberResponseDto>> getAllMembers() {
119114
List<MemberResponseDto> members = memberService.getAllMembers();
120115
return ResponseEntity.ok(members);
121116
}
117+
118+
/**
119+
* 로그아웃
120+
*/
121+
@PostMapping("/logout")
122+
public ResponseEntity<String> logout(@RequestHeader("Authorization") String accessToken,
123+
@RequestBody Map<String, String> body) {
124+
String refreshToken = body.get("refreshToken");
125+
accessToken = accessToken.replace("Bearer ", "");
126+
Long memberId = jwtUtil.extractMemberId(accessToken);
127+
memberService.logout(memberId, refreshToken);
128+
return ResponseEntity.ok("로그아웃 완료");
129+
}
130+
131+
@PostMapping("/reissue")
132+
public ResponseEntity<Map<String, String>> reissue(@RequestBody Map<String, String> request) {
133+
String refreshToken = request.get("refreshToken");
134+
Map<String, String> tokenMap = memberService.reissueAccessToken(refreshToken);
135+
return ResponseEntity.ok(tokenMap);
136+
}
122137
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package pawparazzi.back.member.entity;
2+
3+
import jakarta.persistence.*;
4+
import lombok.*;
5+
6+
import java.time.LocalDateTime;
7+
8+
@Entity
9+
@Getter
10+
@Setter
11+
@NoArgsConstructor
12+
@AllArgsConstructor
13+
@Builder
14+
public class RefreshToken {
15+
16+
@Id
17+
@GeneratedValue(strategy = GenerationType.IDENTITY)
18+
private Long id;
19+
20+
@OneToOne(fetch = FetchType.LAZY)
21+
@JoinColumn(name = "member_id", nullable = false)
22+
private Member member;
23+
24+
@Column(nullable = false, length = 512)
25+
private String token;
26+
27+
@Column(nullable = false)
28+
private LocalDateTime expiryDate;
29+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package pawparazzi.back.member.repository;
2+
3+
import org.springframework.data.jpa.repository.JpaRepository;
4+
import pawparazzi.back.member.entity.RefreshToken;
5+
import pawparazzi.back.member.entity.Member;
6+
7+
import java.util.Optional;
8+
9+
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
10+
Optional<RefreshToken> findByMember(Member member);
11+
Optional<RefreshToken> findByToken(String token);
12+
void deleteByMember(Member member);
13+
}

src/main/java/pawparazzi/back/member/service/MemberService.java

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,20 @@
1919
import pawparazzi.back.member.dto.response.MemberResponseDto;
2020
import pawparazzi.back.member.dto.response.UpdateMemberResponseDto;
2121
import pawparazzi.back.member.entity.Member;
22+
import pawparazzi.back.member.entity.RefreshToken;
2223
import pawparazzi.back.member.repository.MemberRepository;
24+
import pawparazzi.back.member.repository.RefreshTokenRepository;
2325
import pawparazzi.back.security.util.JwtUtil;
2426

2527
import java.io.IOException;
28+
import java.time.LocalDateTime;
2629
import java.util.List;
30+
import java.util.Map;
2731
import java.util.Optional;
2832
import java.util.UUID;
2933
import java.util.concurrent.CompletableFuture;
3034
import java.util.stream.Collectors;
31-
35+
import java.time.temporal.ChronoUnit;
3236

3337
@Service
3438
@RequiredArgsConstructor
@@ -41,6 +45,7 @@ public class MemberService {
4145
private final BoardMongoRepository boardMongoRepository;
4246
private final S3AsyncService s3AsyncService;
4347
private final S3UploadUtil s3UploadUtil;
48+
private final RefreshTokenRepository refreshTokenRepository;
4449

4550
/**
4651
* 회원가입
@@ -72,19 +77,48 @@ public CompletableFuture<Void> registerUser(SignUpRequestDto request, MultipartF
7277
/**
7378
* 로그인
7479
*/
75-
public String login(LoginRequestDto request) {
76-
Optional<Member> memberOptional = memberRepository.findByEmail(request.getEmail());
77-
if (memberOptional.isEmpty()) {
80+
public Map<String, String> login(LoginRequestDto request) {
81+
Member member = memberRepository.findByEmail(request.getEmail())
82+
.orElseThrow(() -> new BadCredentialsException("이메일 또는 비밀번호가 잘못되었습니다."));
83+
84+
if (!passwordEncoder.matches(request.getPassword(), member.getPassword())) {
7885
throw new BadCredentialsException("이메일 또는 비밀번호가 잘못되었습니다.");
7986
}
8087

81-
Member member = memberOptional.get();
88+
String accessToken = jwtUtil.generateIdToken(member.getId());
8289

83-
if (!passwordEncoder.matches(request.getPassword(), member.getPassword())) {
84-
throw new BadCredentialsException("이메일 또는 비밀번호가 잘못되었습니다.");
90+
Optional<RefreshToken> existingTokenOpt = refreshTokenRepository.findByMember(member);
91+
String refreshToken;
92+
93+
if (existingTokenOpt.isPresent()) {
94+
RefreshToken existingToken = existingTokenOpt.get();
95+
long remainingDays = ChronoUnit.DAYS.between(LocalDateTime.now(), existingToken.getExpiryDate());
96+
97+
if (remainingDays <= 1) {
98+
// 만료 임박 → 새로 발급
99+
refreshToken = jwtUtil.generateRefreshToken();
100+
existingToken.setToken(refreshToken);
101+
existingToken.setExpiryDate(jwtUtil.getRefreshTokenExpiryDate());
102+
refreshTokenRepository.save(existingToken);
103+
} else {
104+
// 기존 토큰 사용
105+
refreshToken = existingToken.getToken();
106+
}
107+
} else {
108+
// 존재하지 않으면 새로 발급
109+
refreshToken = jwtUtil.generateRefreshToken();
110+
RefreshToken newToken = RefreshToken.builder()
111+
.member(member)
112+
.token(refreshToken)
113+
.expiryDate(jwtUtil.getRefreshTokenExpiryDate())
114+
.build();
115+
refreshTokenRepository.save(newToken);
85116
}
86117

87-
return jwtUtil.generateIdToken(member.getId());
118+
return Map.of(
119+
"accessToken", accessToken,
120+
"refreshToken", refreshToken
121+
);
88122
}
89123

90124
/**
@@ -222,4 +256,53 @@ public Long handleKakaoLogin(KakaoUserDto kakaoUser) {
222256
return newMember.getId();
223257
}
224258
}
259+
260+
/**
261+
* 로그아웃
262+
*/
263+
@Transactional
264+
public void logout(Long memberId, String refreshToken) {
265+
RefreshToken savedToken = refreshTokenRepository.findByToken(refreshToken)
266+
.orElseThrow(() -> new IllegalArgumentException("이미 만료되었거나 존재하지 않는 토큰입니다."));
267+
268+
if (!savedToken.getMember().getId().equals(memberId)) {
269+
throw new SecurityException("토큰의 사용자 정보가 일치하지 않습니다.");
270+
}
271+
272+
refreshTokenRepository.delete(savedToken);
273+
}
274+
275+
276+
@Transactional
277+
public Map<String, String> reissueAccessToken(String refreshToken) {
278+
RefreshToken savedToken = refreshTokenRepository.findByToken(refreshToken)
279+
.orElseThrow(() -> new IllegalArgumentException("유효하지 않은 리프레시 토큰입니다."));
280+
281+
if (savedToken.getExpiryDate().isBefore(LocalDateTime.now())) {
282+
refreshTokenRepository.delete(savedToken);
283+
throw new IllegalArgumentException("리프레시 토큰이 만료되었습니다. 다시 로그인 해주세요.");
284+
}
285+
286+
Member member = savedToken.getMember();
287+
String newAccessToken = jwtUtil.generateIdToken(member.getId());
288+
289+
long remainingDays = ChronoUnit.DAYS.between(LocalDateTime.now(), savedToken.getExpiryDate());
290+
291+
if (remainingDays <= 1) {
292+
// RefreshToken 재발급
293+
String newRefreshToken = jwtUtil.generateRefreshToken();
294+
savedToken.setToken(newRefreshToken);
295+
savedToken.setExpiryDate(jwtUtil.getRefreshTokenExpiryDate());
296+
refreshTokenRepository.save(savedToken);
297+
298+
return Map.of(
299+
"accessToken", newAccessToken,
300+
"refreshToken", newRefreshToken
301+
);
302+
} else {
303+
return Map.of(
304+
"accessToken", newAccessToken
305+
);
306+
}
307+
}
225308
}

src/main/java/pawparazzi/back/security/filter/JwtAuthenticationFilter.java

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package pawparazzi.back.security.filter;
22

3+
import io.jsonwebtoken.ExpiredJwtException;
4+
import io.jsonwebtoken.JwtException;
35
import jakarta.servlet.FilterChain;
46
import jakarta.servlet.ServletException;
57
import jakarta.servlet.http.HttpServletRequest;
@@ -28,16 +30,26 @@ protected void doFilterInternal(HttpServletRequest request,
2830

2931
if (token != null && token.startsWith("Bearer ")) {
3032
token = token.substring(7);
31-
32-
if (jwtUtil.validateToken(token)) {
33-
Long memberId = jwtUtil.extractMemberId(token);
34-
UserDetails userDetails = userDetailsService.loadUserById(memberId);
35-
36-
JwtAuthenticationToken authentication =
37-
new JwtAuthenticationToken(userDetails, token, userDetails.getAuthorities());
38-
SecurityContextHolder.getContext().setAuthentication(authentication);
33+
try {
34+
if (jwtUtil.validateToken(token)) {
35+
Long memberId = jwtUtil.extractMemberId(token);
36+
UserDetails userDetails = userDetailsService.loadUserById(memberId);
37+
38+
JwtAuthenticationToken authentication =
39+
new JwtAuthenticationToken(userDetails, token, userDetails.getAuthorities());
40+
SecurityContextHolder.getContext().setAuthentication(authentication);
41+
}
42+
} catch (ExpiredJwtException e) {
43+
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401로 응답
44+
response.getWriter().write("Access Token Expired");
45+
return;
46+
} catch (JwtException e) {
47+
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
48+
response.getWriter().write("Invalid JWT");
49+
return;
3950
}
4051
}
52+
4153
chain.doFilter(request, response);
4254
}
4355
}

src/main/java/pawparazzi/back/security/util/JwtUtil.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
import org.springframework.stereotype.Component;
77

88
import java.security.Key;
9+
import java.time.LocalDateTime;
910
import java.util.Date;
11+
import java.util.UUID;
1012

1113
@Component
1214
public class JwtUtil {
@@ -30,6 +32,15 @@ public String generateIdToken(Long memberId) {
3032
.compact();
3133
}
3234

35+
public String generateRefreshToken() {
36+
return UUID.randomUUID().toString();
37+
}
38+
39+
public LocalDateTime getRefreshTokenExpiryDate() {
40+
return LocalDateTime.now().plusDays(7); // 7일
41+
}
42+
43+
3344
// 토큰에서 사용자 ID 추출
3445
public Long extractMemberId(String token) {
3546
return Long.parseLong(Jwts.parserBuilder()

0 commit comments

Comments
 (0)