Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.back.web7_9_codecrete_be.global.rsData.RsData;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

Expand All @@ -27,14 +28,14 @@ public class AuthController {

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

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

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

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

@Operation(summary = "임시 비밀번호 재발급", description = "특정 이메일로 임시 비밀번호를 발송합니다.")
@PostMapping("/password/reset")
public RsData<?> resetPassword(@RequestBody EmailSendRequest req) {
public RsData<?> resetPassword(@Valid @RequestBody EmailSendRequest req) {
authService.resetPassword(req.getEmail());
return RsData.success("임시 비밀번호가 이메일로 발송되었습니다.");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package com.back.web7_9_codecrete_be.domain.auth.dto.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;

@Getter
public class EmailSendRequest {

@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Email 어노테이션은 생각보다 검증을 탄탄해주게 하지 않아서 해당 부분은 정규식으로 수정하는 게 좋을 것 같습니다.
관련 링크

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다! 추후 반영하겠습니다!

private String email;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
package com.back.web7_9_codecrete_be.domain.auth.dto.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Getter;

@Getter
public class EmailVerifyRequest {

@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
private String email;

@NotBlank(message = "인증 코드는 필수입니다.")
@Pattern(
regexp = "^[A-Z0-9]{6}$",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정규표현식 처리하신 것 잘하신 것 같습니다.

message = "인증 코드는 영문 대문자와 숫자를 포함한 6자리여야 합니다."
)
private String code;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package com.back.web7_9_codecrete_be.domain.auth.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;

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

@NotBlank(message = "비밀번호는 필수입니다.")
@Schema(description = "비밀번호", example = "1234abcd!")
private String password;
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
package com.back.web7_9_codecrete_be.domain.auth.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Getter;

@Getter
@Schema(description = "회원가입 요청 DTO")
public class SignupRequest {

@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
@Schema(description = "사용자 이메일", example = "test@example.com")
private String email;

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

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

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.back.web7_9_codecrete_be.domain.auth.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class RefreshToken {

private Long userId; // 사용자 ID
private String refreshToken; // 실제 토큰 값
private long expiration; // TTL (초 단위)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.back.web7_9_codecrete_be.domain.auth.repository;

import com.back.web7_9_codecrete_be.domain.auth.entity.RefreshToken;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

import java.util.concurrent.TimeUnit;

@Repository
@RequiredArgsConstructor
public class RefreshTokenRedisRepository {
private final RedisTemplate<String, String> redisTemplate;

private static final String REFRESH_TOKEN_PREFIX = "refreshToken: ";

public void save(RefreshToken refreshToken) {
redisTemplate.opsForValue().set(
REFRESH_TOKEN_PREFIX + refreshToken.getUserId(),
refreshToken.getRefreshToken(),
refreshToken.getExpiration(),
TimeUnit.SECONDS
);
}

public String findByUserId(Long userId) {
return (String) redisTemplate.opsForValue().get(REFRESH_TOKEN_PREFIX + userId);
}

public void deleteByUserId(Long userId){
redisTemplate.delete(REFRESH_TOKEN_PREFIX + userId);
}

public boolean existsByUserId(Long userId) {
return redisTemplate.hasKey(REFRESH_TOKEN_PREFIX + userId);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public void signUp(SignupRequest req) {
.build();

userRepository.save(user);

emailService.clearVerifiedEmail(req.getEmail());
}

// 로그인
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.back.web7_9_codecrete_be.domain.auth.service;

import com.back.web7_9_codecrete_be.domain.auth.entity.RefreshToken;
import com.back.web7_9_codecrete_be.domain.auth.repository.RefreshTokenRedisRepository;
import com.back.web7_9_codecrete_be.domain.users.entity.User;
import com.back.web7_9_codecrete_be.domain.users.repository.UserRepository;
import com.back.web7_9_codecrete_be.global.error.code.AuthErrorCode;
import com.back.web7_9_codecrete_be.global.error.exception.BusinessException;
import com.back.web7_9_codecrete_be.global.rq.Rq;
Expand All @@ -16,6 +19,8 @@ public class TokenService {
private final JwtTokenProvider jwtTokenProvider;
private final JwtProperties jwtProperties;
private final Rq rq;
private final UserRepository userRepository;
private final RefreshTokenRedisRepository refreshTokenRedisRepository;

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

rq.setCookie("ACCESS_TOKEN", access, (int) jwtProperties.getAccessTokenExpiration());
rq.setCookie("REFRESH_TOKEN", refresh, (int) jwtProperties.getRefreshTokenExpiration());

refreshTokenRedisRepository.save(
new RefreshToken(
user.getId(),
refresh,
jwtProperties.getRefreshTokenExpiration()
)
);
}

// 로그아웃 시 실행 시 쿠키 삭제
public void removeTokens(User user) {
rq.removeCookie("ACCESS_TOKEN");
rq.removeCookie("REFRESH_TOKEN");

refreshTokenRedisRepository.deleteByUserId(user.getId());
}

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

User user = userRepository.findByEmail(email)
.orElseThrow(() -> new BusinessException(AuthErrorCode.USER_NOT_FOUND));

String savedRefresh =
refreshTokenRedisRepository.findByUserId(user.getId());

if (savedRefresh == null || !savedRefresh.equals(refresh)) {
throw new BusinessException(AuthErrorCode.INVALID_TOKEN);
}

// AccessToken 재발급
String newAccess = jwtTokenProvider.generateAccessToken(email);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.back.web7_9_codecrete_be.domain.email.repository;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

import java.util.concurrent.TimeUnit;

@Repository
@RequiredArgsConstructor
public class VerificationCodeRedisRepository {

private final RedisTemplate<String, String> redisTemplate;

private static final String PREFIX = "email:verify:";

public void save(String email, String code, long ttlSeconds) {
redisTemplate.opsForValue().set(
PREFIX + email,
code,
ttlSeconds,
TimeUnit.SECONDS
);
}

public String findByEmail(String email) {
return redisTemplate.opsForValue().get(PREFIX + email);
}

public void deleteByEmail(String email) {
redisTemplate.delete(PREFIX + email);
}

public boolean existsByEmail(String email) {
return redisTemplate.hasKey(PREFIX + email);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@

public interface VerifiedEmailRepository extends JpaRepository<VerifiedEmail, String> {
boolean existsByEmail(String email);
void deleteByEmail(String email);
}
Loading