Skip to content
This repository was archived by the owner on Jan 11, 2026. It is now read-only.
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,5 @@ out/
src/main/resources/application.yml

### jacoco ###
jacoco
jacoco
src/main/java/com/example/ApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public enum ErrorStatus implements BaseErrorCode {
_VALUE_RANGE_EXCEEDED(HttpStatus.BAD_REQUEST, "COMMON4012", "값이 지정된 범위를 초과합니다."),
_TERMS_NOT_AGREED(HttpStatus.FORBIDDEN, "COMMON4013", "이용 약관이 동의되지 않았습니다."),
_MEMBER_EMAIL_EXIST(HttpStatus.BAD_REQUEST, "COMMON4014", "이미 가입 된 이메일입니다. 다른 로그인 방식을 이용해주세요."),
_RSA_ERROR(HttpStatus.BAD_REQUEST, "COMMON4015", "RSA 에러가 발생했습니다."),

// 네이버 소셜 로그인 관련 에러
_NAVER_SIGN_IN_INTEGRATION_FAILED(HttpStatus.UNAUTHORIZED, "NAVER4001", "네이버 로그인 연동에 실패하였습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public enum SuccessStatus implements BaseCode {
_MEMBER_LOGIN_ID_FOUND(HttpStatus.OK, "MEMBER2012", "회원 아이디 조회 완료"),
_MEMBER_LOGIN_ID_CHECK_COMPLETED(HttpStatus.OK, "MEMBER2012", "회원 아이디 사용 가능 여부 조회 완료"),
_MEMBER_EMAIL_CHECK_COMPLETED(HttpStatus.OK, "MEMBER2013", "회원 이메일 사용 가능 여부 조회 완료"),
_RSA_PUBLIC_KEY_FOUND(HttpStatus.OK, "MEMBER2014", "RSA Public Key 조회 완료"),

//스터디 게시글 관련 응답
_STUDY_POST_CREATED(HttpStatus.CREATED, "STUDYPOST3001", "스터디 게시글 작성 완료"),
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/example/spot/config/WebSecurity.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ protected SecurityFilterChain configure(HttpSecurity http) throws Exception {
.requestMatchers(new AntPathRequestMatcher("/spot/reissue")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/spot/sign-up", "POST")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/spot/login", "POST")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/spot/login/rsa", "POST")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/spot/members/sign-in/naver", "POST")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/spot/members/sign-in/naver/redirect", "GET")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/spot/members/sign-in/naver/authorize/test", "GET")).permitAll()
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/com/example/spot/domain/auth/RsaKey.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.example.spot.domain.auth;

import com.example.spot.domain.common.BaseEntity;
import jakarta.persistence.*;
import lombok.*;

@Getter
@Entity
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class RsaKey extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false, unique = true)
private Long id;

@Column(columnDefinition = "text")
private String privateKey;

@Column(columnDefinition = "text")
private String publicKey;

@Column(columnDefinition = "text")
private String modulus;

@Column(columnDefinition = "text")
private String exponent;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.spot.repository.rsa;

import com.example.spot.domain.auth.RsaKey;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;

@Repository
public interface RSAKeyRepository extends JpaRepository<RsaKey, Long> {

void deleteByCreatedAtBefore(LocalDateTime localDateTime);
}
134 changes: 134 additions & 0 deletions src/main/java/com/example/spot/security/utils/RSAUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package com.example.spot.security.utils;

import com.example.spot.api.code.status.ErrorStatus;
import com.example.spot.api.exception.handler.MemberHandler;
import com.example.spot.domain.auth.RsaKey;
import org.springframework.stereotype.Component;

import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.util.Base64;

@Component
public class RSAUtils {

private final KeyPairGenerator generator;
private final KeyFactory keyFactory;
private final Cipher cipher;

public RSAUtils() throws Exception {
SecureRandom secureRandom = new SecureRandom();
generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048, secureRandom);
keyFactory = KeyFactory.getInstance("RSA");
cipher = Cipher.getInstance("RSA");
}

/**
* RSA Public Key와 Private Key를 생성하는 함수
* @return 생성된 RSA 객체
*/
public RsaKey createRSA() {

try {
// Key Pair 생성
KeyPair keyPair = generator.generateKeyPair();
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();

RSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey, RSAPublicKeySpec.class);
String modulus = publicKeySpec.getModulus().toString(16); // 16진수 문자열
String exponent = publicKeySpec.getPublicExponent().toString(16); // 16진수 문자열

return RsaKey.builder()
.publicKey(getBase64StringFromPublicKey(publicKey))
.privateKey(getBase64StringFromPrivateKey(privateKey))
.modulus(modulus)
.exponent(exponent)
.build();

} catch (Exception e) {
e.printStackTrace();
throw new MemberHandler(ErrorStatus._RSA_ERROR);
}

}

/**
* Public Key를 Base64 문자열로 변환하는 함수
* @param publicKey : 암호화에 사용될 publicKey
* @return Public Key를 Base64 문자열로 변환한 값
*/
public String getBase64StringFromPublicKey(PublicKey publicKey) {
return Base64.getEncoder().encodeToString(publicKey.getEncoded());
}

/**
* Private Key를 Base64 문자열로 변환하는 함수
* @param privateKey : 암호화에 사용될 privateKey
* @return Public Key를 Base64 문자열로 변환한 값
*/
public String getBase64StringFromPrivateKey(PrivateKey privateKey) {
return Base64.getEncoder().encodeToString(privateKey.getEncoded());
}

/**
* Base64 문자열을 PrivateKey 객체로 변환하는 함수
* @param privateKeyString :Private Key를 Base64 문자열로 변환한 값
* @return PrivateKey 객체
*/
public PrivateKey getPrivateKeyFromBase64String(String privateKeyString) {

try {
// Base64 디코딩
byte[] keyBytes = Base64.getDecoder().decode(privateKeyString);

// PrivateKey 객체 생성
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
} catch (Exception e) {
e.printStackTrace();
throw new MemberHandler(ErrorStatus._RSA_ERROR);
}

}

/**
* Public Key로 문자열을 암호화하는 함수
* @param publicKey : 암호화에 사용될 publicKey
* @param plainText : 암호화되지 않은 문자열
* @return 암호화된 문자열
*/
public String getEncryptedText(PublicKey publicKey, String plainText) {
try {
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] plainBytes = cipher.doFinal(plainText.getBytes());
return Base64.getEncoder().encodeToString(plainBytes);
} catch (Exception e) {
e.printStackTrace();
throw new MemberHandler(ErrorStatus._RSA_ERROR);
}
}

/**
* Private Key로 문자열을 복호화하는 함수
* @param privateKey : 복호화에 사용될 privateKey
* @param encryptedText : 암호화된 문자열
* @return 복호화된 문자열
*/
public String getDecryptedText(PrivateKey privateKey, String encryptedText) {
try {
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedText);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] plainBytes = cipher.doFinal(encryptedBytes);
return new String(plainBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
throw new MemberHandler(ErrorStatus._RSA_ERROR);
}
}
}
7 changes: 5 additions & 2 deletions src/main/java/com/example/spot/service/auth/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.spot.service.auth;

import com.example.spot.web.dto.rsa.Rsa;
import com.example.spot.web.dto.member.MemberRequestDTO;
import com.example.spot.web.dto.member.MemberResponseDTO;
import com.example.spot.web.dto.member.MemberResponseDTO.SocialLoginSignInDTO;
Expand All @@ -23,13 +24,15 @@ public interface AuthService {

SocialLoginSignInDTO signInWithNaver(HttpServletRequest request, HttpServletResponse response, NaverOAuthToken.NaverTokenIssuanceDTO naverTokenDTO) throws Exception;

MemberResponseDTO.MemberSignInDTO signIn(MemberRequestDTO.SignInDTO signInDTO);
MemberResponseDTO.MemberSignInDTO signIn(Long httpSession, MemberRequestDTO.SignInDTO signInDTO) throws Exception;

Rsa.RSAPublicKey getRSAPublicKey() throws Exception;

void sendVerificationCode(HttpServletRequest request, HttpServletResponse response, String email);

TokenResponseDTO.TempTokenDTO verifyEmail(String verificationCode, String email);

MemberResponseDTO.MemberSignInDTO signUp(MemberRequestDTO.SignUpDTO signUpDTO);
MemberResponseDTO.MemberSignInDTO signUp(Long rsaId, MemberRequestDTO.SignUpDTO signUpDTO) throws Exception;

MemberResponseDTO.FindIdDTO findId();

Expand Down
56 changes: 49 additions & 7 deletions src/main/java/com/example/spot/service/auth/AuthServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.example.spot.api.exception.GeneralException;
import com.example.spot.api.exception.handler.MemberHandler;
import com.example.spot.domain.Member;
import com.example.spot.domain.auth.RsaKey;
import com.example.spot.web.dto.rsa.Rsa;
import com.example.spot.domain.auth.RefreshToken;
import com.example.spot.domain.auth.VerificationCode;
import com.example.spot.domain.enums.Carrier;
Expand All @@ -13,9 +15,11 @@
import com.example.spot.domain.enums.Status;
import com.example.spot.repository.MemberRepository;
import com.example.spot.repository.RefreshTokenRepository;
import com.example.spot.repository.rsa.RSAKeyRepository;
import com.example.spot.repository.verification.VerificationCodeRepository;
import com.example.spot.security.utils.JwtTokenProvider;
import com.example.spot.security.utils.MemberUtils;
import com.example.spot.security.utils.RSAUtils;
import com.example.spot.web.dto.member.MemberRequestDTO;
import com.example.spot.web.dto.member.MemberResponseDTO;
import com.example.spot.security.utils.SecurityUtils;
Expand All @@ -27,8 +31,10 @@
import com.example.spot.web.dto.token.TokenResponseDTO;
import com.example.spot.web.dto.token.TokenResponseDTO.TokenDTO;

import java.security.PrivateKey;
import java.security.SecureRandom;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.Random;
Expand Down Expand Up @@ -56,6 +62,9 @@ public class AuthServiceImpl implements AuthService{
private final MailService mailService;
private final NaverOAuthService naverOAuthService;

private final RSAUtils rsaUtils;
private final RSAKeyRepository rsaKeyRepository;

@Value("${image.post.anonymous.profile}")
private String DEFAULT_PROFILE_IMAGE_URL;

Expand Down Expand Up @@ -258,18 +267,19 @@ private void signUpWithNaver(NaverMember.ResponseDTO memberDTO) {

/**
* 일반 로그인을 위한 메서드입니다. 아이디와 비밀번호를 확인한 후 토큰을 발급하는 로직을 수행합니다.
* @param signInDTO 로그인할 회원의 아이디와 비밀번호를 입력 받습니다.
* @param signInDTO 로그인할 회원의 아이디와 비밀번호를 입력 받습니다.
* @return 로그인한 회원의 토큰 정보(액세스 & 리프레시 토큰 & 만료기간), 이메일과 회원 아이디(정수)가 반환됩니다.
*/
@Override
public MemberResponseDTO.MemberSignInDTO signIn(MemberRequestDTO.SignInDTO signInDTO) {
public MemberResponseDTO.MemberSignInDTO signIn(Long rsaId, MemberRequestDTO.SignInDTO signInDTO) throws Exception {

// 아이디가 일치하는 유저가 있는지 확인
Member member = memberRepository.findByLoginId(signInDTO.getLoginId())
.orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND));

// 비밀번호 확인
if (!signInDTO.getPassword().equals(member.getPassword())) {
String password = getDecryptedPassword(rsaId, signInDTO.getPassword());
if (!password.equals(member.getPassword())) {
throw new MemberHandler(ErrorStatus._MEMBER_PASSWORD_NOT_MATCH);
}

Expand All @@ -285,6 +295,33 @@ public MemberResponseDTO.MemberSignInDTO signIn(MemberRequestDTO.SignInDTO signI
.build();
}

private String getDecryptedPassword(Long rsaId, String encryptedPassword) throws Exception {

// Private Key 추출 후 Session에 저장된 Private Key 초기화
RsaKey rsaKey = rsaKeyRepository.findById(rsaId)
.orElseThrow(() -> new MemberHandler(ErrorStatus._RSA_ERROR));
rsaKeyRepository.deleteByCreatedAtBefore(LocalDateTime.now().minusMinutes(1));

// 복호화된 패스워드 반환
PrivateKey privateKey = rsaUtils.getPrivateKeyFromBase64String(rsaKey.getPrivateKey());
return rsaUtils.getDecryptedText(privateKey, encryptedPassword);
}

@Override
public Rsa.RSAPublicKey getRSAPublicKey() throws Exception {

// RSA 생성
RsaKey rsa = rsaUtils.createRSA();
rsa = rsaKeyRepository.save(rsa);

// Public Key 반환
return Rsa.RSAPublicKey.builder()
.rsaId(rsa.getId())
.modulus(rsa.getModulus())
.exponent(rsa.getExponent())
.build();
}

/**
* 인증 코드를 전송하는 메서드입니다.
* 일반 회원가입, 아이디 찾기, 비밀번호 찾기에 공통으로 적용되는 인증 메일 전송 로직입니다.
Expand Down Expand Up @@ -344,18 +381,20 @@ public TokenResponseDTO.TempTokenDTO verifyEmail(String code, String email) {

/**
* 일반 회원가입에 사용되는 메서드입니다.
*
* @param rsaId
* @param signUpDTO 회원의 기본 정보를 입력 받습니다.
* name : 이름
* frontRID : 주민번호 앞자리
* backRID : 주민번호 뒷자리 첫 글자
* email : 이메일
* loginId : 아이디
* password : 비밀번호
* password : 비밀번호 (RSA Key로 암호화한 값)
* pwCheck : 비밀번호 확인
* @return 가입한 회원은 자동으로 로그인되며, 회원의 토큰 정보(액세스 & 리프레시 토큰 & 만료기간), 이메일과 회원 아이디(정수)가 반환됩니다.
*/
@Override
public MemberResponseDTO.MemberSignInDTO signUp(MemberRequestDTO.SignUpDTO signUpDTO) {
public MemberResponseDTO.MemberSignInDTO signUp(Long rsaId, MemberRequestDTO.SignUpDTO signUpDTO) throws Exception {

// 회원 생성
if (memberRepository.existsByEmail(signUpDTO.getEmail())) {
Expand All @@ -364,7 +403,10 @@ public MemberResponseDTO.MemberSignInDTO signUp(MemberRequestDTO.SignUpDTO signU
if (memberRepository.existsByLoginId(signUpDTO.getLoginId())) {
throw new MemberHandler(ErrorStatus._MEMBER_LOGIN_ID_ALREADY_EXISTS);
}
if (!signUpDTO.getPassword().equals(signUpDTO.getPwCheck())) {

String password = getDecryptedPassword(rsaId, signUpDTO.getPassword());
String pwCheck = getDecryptedPassword(rsaId, signUpDTO.getPwCheck());
if (!password.equals(pwCheck)) {
throw new MemberHandler(ErrorStatus._MEMBER_PW_AND_PW_CHECK_DO_NOT_MATCH);
}

Expand All @@ -389,7 +431,7 @@ public MemberResponseDTO.MemberSignInDTO signUp(MemberRequestDTO.SignUpDTO signU
.carrier(Carrier.NONE)
.phone("")
.loginId(signUpDTO.getLoginId())
.password(signUpDTO.getPassword())
.password(password)
.profileImage(DEFAULT_PROFILE_IMAGE_URL)
.personalInfo(false)
.idInfo(false)
Expand Down
Loading
Loading