Skip to content

Commit a6c19c0

Browse files
Merge pull request #53 from prgrms-web-devcourse-final-project/feat/#38
[Auth] Redis 도입 및 피드백 리팩토링
2 parents cac7012 + df7b8c0 commit a6c19c0

20 files changed

Lines changed: 315 additions & 63 deletions

src/main/java/com/back/web7_9_codecrete_be/domain/auth/controller/AuthController.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import com.back.web7_9_codecrete_be.global.rsData.RsData;
1414
import io.swagger.v3.oas.annotations.Operation;
1515
import io.swagger.v3.oas.annotations.tags.Tag;
16+
import jakarta.validation.Valid;
1617
import lombok.RequiredArgsConstructor;
1718
import org.springframework.web.bind.annotation.*;
1819

@@ -27,14 +28,14 @@ public class AuthController {
2728

2829
@Operation(summary = "회원가입", description = "사용자 이메일, 비밀번호, 닉네임, 생년월일을 이용하여 회원가입을 진행합니다.")
2930
@PostMapping("/signup")
30-
public RsData<?> signUp(@RequestBody SignupRequest req) {
31+
public RsData<?> signUp(@Valid @RequestBody SignupRequest req) {
3132
authService.signUp(req);
3233
return RsData.success("회원가입이 완료되었습니다.");
3334
}
3435

3536
@Operation(summary = "로그인", description = "이메일/비밀번호로 로그인합니다. 성공 시 사용자 닉네임을 반환합니다.")
3637
@PostMapping("/login")
37-
public RsData<?> login(@RequestBody LoginRequest req) {
38+
public RsData<?> login(@Valid @RequestBody LoginRequest req) {
3839
LoginResponse response = authService.login(req);
3940
return RsData.success("로그인 성공", response);
4041
}
@@ -49,14 +50,14 @@ public RsData<?> logout() {
4950

5051
@Operation(summary = "이메일 인증코드 전송", description = "입력된 이메일로 인증코드를 전송합니다.")
5152
@PostMapping("/email/send")
52-
public RsData<?> sendVerificationCode(@RequestBody EmailSendRequest req) {
53+
public RsData<?> sendVerificationCode(@Valid @RequestBody EmailSendRequest req) {
5354
authService.sendVerificationCode(req.getEmail());
5455
return RsData.success("인증코드가 발송되었습니다.");
5556
}
5657

5758
@Operation(summary = "이메일 인증코드 검증", description = "사용자가 입력한 인증코드가 맞는지 확인합니다.")
5859
@PostMapping("/email/verify")
59-
public RsData<?> verifyEmailCode(@RequestBody EmailVerifyRequest req) {
60+
public RsData<?> verifyEmailCode(@Valid @RequestBody EmailVerifyRequest req) {
6061
authService.verifyEmailCode(req.getEmail(), req.getCode());
6162
return RsData.success("이메일 인증이 완료되었습니다.");
6263
}
@@ -70,7 +71,7 @@ public RsData<?> checkNickname(@RequestParam String nickname) {
7071

7172
@Operation(summary = "임시 비밀번호 재발급", description = "특정 이메일로 임시 비밀번호를 발송합니다.")
7273
@PostMapping("/password/reset")
73-
public RsData<?> resetPassword(@RequestBody EmailSendRequest req) {
74+
public RsData<?> resetPassword(@Valid @RequestBody EmailSendRequest req) {
7475
authService.resetPassword(req.getEmail());
7576
return RsData.success("임시 비밀번호가 이메일로 발송되었습니다.");
7677
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package com.back.web7_9_codecrete_be.domain.auth.dto.request;
22

3+
import jakarta.validation.constraints.Email;
4+
import jakarta.validation.constraints.NotBlank;
35
import lombok.Getter;
46

57
@Getter
68
public class EmailSendRequest {
9+
10+
@NotBlank(message = "이메일은 필수입니다.")
11+
@Email(message = "이메일 형식이 올바르지 않습니다.")
712
private String email;
813
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
package com.back.web7_9_codecrete_be.domain.auth.dto.request;
22

3+
import jakarta.validation.constraints.Email;
4+
import jakarta.validation.constraints.NotBlank;
5+
import jakarta.validation.constraints.Pattern;
36
import lombok.Getter;
47

58
@Getter
69
public class EmailVerifyRequest {
10+
11+
@NotBlank(message = "이메일은 필수입니다.")
12+
@Email(message = "이메일 형식이 올바르지 않습니다.")
713
private String email;
14+
15+
@NotBlank(message = "인증 코드는 필수입니다.")
16+
@Pattern(
17+
regexp = "^[A-Z0-9]{6}$",
18+
message = "인증 코드는 영문 대문자와 숫자를 포함한 6자리여야 합니다."
19+
)
820
private String code;
921
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
package com.back.web7_9_codecrete_be.domain.auth.dto.request;
22

33
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.Email;
5+
import jakarta.validation.constraints.NotBlank;
46
import lombok.Getter;
57

68
@Getter
79
@Schema(description = "로그인 요청 DTO")
810
public class LoginRequest {
11+
@NotBlank(message = "이메일은 필수입니다.")
12+
@Email(message = "이메일 형식이 올바르지 않습니다.")
913
@Schema(description = "사용자 이메일", example = "test@example.com")
1014
private String email;
1115

16+
@NotBlank(message = "비밀번호는 필수입니다.")
1217
@Schema(description = "비밀번호", example = "1234abcd!")
1318
private String password;
1419
}

src/main/java/com/back/web7_9_codecrete_be/domain/auth/dto/request/SignupRequest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,37 @@
11
package com.back.web7_9_codecrete_be.domain.auth.dto.request;
22

33
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.Email;
5+
import jakarta.validation.constraints.NotBlank;
6+
import jakarta.validation.constraints.Pattern;
47
import lombok.Getter;
58

69
@Getter
710
@Schema(description = "회원가입 요청 DTO")
811
public class SignupRequest {
12+
13+
@NotBlank(message = "이메일은 필수입니다.")
14+
@Email(message = "이메일 형식이 올바르지 않습니다.")
915
@Schema(description = "사용자 이메일", example = "test@example.com")
1016
private String email;
1117

18+
@NotBlank(message = "닉네임은 필수입니다.")
1219
@Schema(description = "닉네임", example = "codeMaster")
1320
private String nickname;
1421

22+
@NotBlank(message = "비밀번호는 필수입니다.")
23+
@Pattern(
24+
regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*]).{8,}$",
25+
message = "비밀번호는 영문, 숫자, 특수문자를 포함한 8자 이상이어야 합니다."
26+
)
1527
@Schema(description = "비밀번호", example = "1234abcd!")
1628
private String password;
1729

30+
@NotBlank(message = "생년월일은 필수입니다.")
31+
@Pattern(
32+
regexp = "\\d{4}-\\d{2}-\\d{2}",
33+
message = "생년월일은 yyyy-MM-dd 형식이어야 합니다."
34+
)
1835
@Schema(description = "생년월일", example = "2000-08-25")
1936
private String birth;
2037

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.back.web7_9_codecrete_be.domain.auth.entity;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@AllArgsConstructor
8+
public class RefreshToken {
9+
10+
private Long userId; // 사용자 ID
11+
private String refreshToken; // 실제 토큰 값
12+
private long expiration; // TTL (초 단위)
13+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.back.web7_9_codecrete_be.domain.auth.repository;
2+
3+
import com.back.web7_9_codecrete_be.domain.auth.entity.RefreshToken;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.data.redis.core.RedisTemplate;
6+
import org.springframework.stereotype.Repository;
7+
8+
import java.util.concurrent.TimeUnit;
9+
10+
@Repository
11+
@RequiredArgsConstructor
12+
public class RefreshTokenRedisRepository {
13+
private final RedisTemplate<String, String> redisTemplate;
14+
15+
private static final String REFRESH_TOKEN_PREFIX = "refreshToken: ";
16+
17+
public void save(RefreshToken refreshToken) {
18+
redisTemplate.opsForValue().set(
19+
REFRESH_TOKEN_PREFIX + refreshToken.getUserId(),
20+
refreshToken.getRefreshToken(),
21+
refreshToken.getExpiration(),
22+
TimeUnit.SECONDS
23+
);
24+
}
25+
26+
public String findByUserId(Long userId) {
27+
return (String) redisTemplate.opsForValue().get(REFRESH_TOKEN_PREFIX + userId);
28+
}
29+
30+
public void deleteByUserId(Long userId){
31+
redisTemplate.delete(REFRESH_TOKEN_PREFIX + userId);
32+
}
33+
34+
public boolean existsByUserId(Long userId) {
35+
return redisTemplate.hasKey(REFRESH_TOKEN_PREFIX + userId);
36+
}
37+
}

src/main/java/com/back/web7_9_codecrete_be/domain/auth/repository/RefreshTokenRepository.java

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/main/java/com/back/web7_9_codecrete_be/domain/auth/service/AuthService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ public void signUp(SignupRequest req) {
5050
.build();
5151

5252
userRepository.save(user);
53+
54+
emailService.clearVerifiedEmail(req.getEmail());
5355
}
5456

5557
// 로그인

src/main/java/com/back/web7_9_codecrete_be/domain/auth/service/TokenService.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.back.web7_9_codecrete_be.domain.auth.service;
22

3+
import com.back.web7_9_codecrete_be.domain.auth.entity.RefreshToken;
4+
import com.back.web7_9_codecrete_be.domain.auth.repository.RefreshTokenRedisRepository;
35
import com.back.web7_9_codecrete_be.domain.users.entity.User;
6+
import com.back.web7_9_codecrete_be.domain.users.repository.UserRepository;
47
import com.back.web7_9_codecrete_be.global.error.code.AuthErrorCode;
58
import com.back.web7_9_codecrete_be.global.error.exception.BusinessException;
69
import com.back.web7_9_codecrete_be.global.rq.Rq;
@@ -16,6 +19,8 @@ public class TokenService {
1619
private final JwtTokenProvider jwtTokenProvider;
1720
private final JwtProperties jwtProperties;
1821
private final Rq rq;
22+
private final UserRepository userRepository;
23+
private final RefreshTokenRedisRepository refreshTokenRedisRepository;
1924

2025
// 로그인 시 실행 시 쿠키에 토큰 발급
2126
public void issueTokens(User user) {
@@ -24,13 +29,21 @@ public void issueTokens(User user) {
2429

2530
rq.setCookie("ACCESS_TOKEN", access, (int) jwtProperties.getAccessTokenExpiration());
2631
rq.setCookie("REFRESH_TOKEN", refresh, (int) jwtProperties.getRefreshTokenExpiration());
32+
33+
refreshTokenRedisRepository.save(
34+
new RefreshToken(
35+
user.getId(),
36+
refresh,
37+
jwtProperties.getRefreshTokenExpiration()
38+
)
39+
);
2740
}
2841

2942
// 로그아웃 시 실행 시 쿠키 삭제
3043
public void removeTokens(User user) {
3144
rq.removeCookie("ACCESS_TOKEN");
3245
rq.removeCookie("REFRESH_TOKEN");
33-
46+
refreshTokenRedisRepository.deleteByUserId(user.getId());
3447
}
3548

3649
public String reissueAccessToken() {
@@ -47,6 +60,16 @@ public String reissueAccessToken() {
4760
// RefreshToken에서 email(Subject) 추출
4861
String email = jwtTokenProvider.getEmailFromToken(refresh);
4962

63+
User user = userRepository.findByEmail(email)
64+
.orElseThrow(() -> new BusinessException(AuthErrorCode.USER_NOT_FOUND));
65+
66+
String savedRefresh =
67+
refreshTokenRedisRepository.findByUserId(user.getId());
68+
69+
if (savedRefresh == null || !savedRefresh.equals(refresh)) {
70+
throw new BusinessException(AuthErrorCode.INVALID_TOKEN);
71+
}
72+
5073
// AccessToken 재발급
5174
String newAccess = jwtTokenProvider.generateAccessToken(email);
5275

0 commit comments

Comments
 (0)