Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

회원 가입 및 로그인 기능 구현 #21

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ out/
### VS Code ###
.vscode/

application-*.yml
**/src/main/**/application-*.yml
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt:0.12.5'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
testRuntimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/com/wooteco/wiki/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.wooteco.wiki.controller;

import com.wooteco.wiki.dto.AuthTokens;
import com.wooteco.wiki.dto.JoinRequest;
import com.wooteco.wiki.dto.LoginRequest;
import com.wooteco.wiki.service.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;

@PostMapping("/login")
public AuthTokens login(@RequestBody LoginRequest loginRequest) {
return authService.login(loginRequest);
}

@PostMapping("/join")
public AuthTokens join(@RequestBody JoinRequest joinRequest) {
return authService.join(joinRequest);
}
}
33 changes: 33 additions & 0 deletions src/main/java/com/wooteco/wiki/domain/Email.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.wooteco.wiki.domain;

import com.wooteco.wiki.exception.WrongEmailException;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.Getter;

@Embeddable
@Getter
public class Email {
private static final String EMAIL_FORMAT = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";
private static final Pattern PATTERN = Pattern.compile(EMAIL_FORMAT);
@Column(name = "email")
private String rawEmail;

public Email(String rawEmail) {
validateEmail(rawEmail);
this.rawEmail = rawEmail;
}

private void validateEmail(String rawEmail) {
Matcher matcher = PATTERN.matcher(rawEmail);
if (!matcher.matches()) {
throw new WrongEmailException("잘못된 이메일입니다.");
}
}

protected Email() {

}
}
37 changes: 32 additions & 5 deletions src/main/java/com/wooteco/wiki/domain/Member.java
Original file line number Diff line number Diff line change
@@ -1,28 +1,55 @@
package com.wooteco.wiki.domain;

import static com.wooteco.wiki.domain.MemberState.INITIAL;
import static com.wooteco.wiki.domain.MemberState.WAITING;

import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@AllArgsConstructor
@Getter
@NoArgsConstructor
@Builder
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long memberId;
private String nickname;
private String email;
private String password;
private Email email;
@Embedded
private Password password;
@Enumerated(EnumType.STRING)
private MemberState state;

@Builder
public Member(Long memberId, String nickname, String email, String password, MemberState state) {
this.memberId = memberId;
this.nickname = nickname;
this.email = new Email(email);
this.password = new Password(password);
this.state = state;
}

public void updateEmailAndPasswordWhenINITIAL(String email, String password) {
if (INITIAL.equals(state)) {
this.email = new Email(email);
this.password = new Password(password);
state = WAITING;
}
}

public String getRawEmail() {
return email.getRawEmail();
}

private String getRawPassword() {
return password.getRawPassword();
}
}
34 changes: 34 additions & 0 deletions src/main/java/com/wooteco/wiki/domain/Password.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.wooteco.wiki.domain;

import com.wooteco.wiki.exception.WrongPasswordException;
import com.wooteco.wiki.util.Sha256Encryptor;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.Getter;

@Embeddable
@Getter
public class Password {
private static final String PASSWORD_REGEX = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#$%^&*()_\\-+=|{}\\[\\]:;\"',.?/~`]).{8,20}$";
private static final Pattern PATTERN = Pattern.compile(PASSWORD_REGEX);
@Column(name = "password")
private String rawPassword;

public Password(String rawPassword) {
validatePassword(rawPassword);
this.rawPassword = Sha256Encryptor.encrypt(rawPassword);
}

private static void validatePassword(String rawPassword) {
Matcher matcher = PATTERN.matcher(rawPassword);
if (!matcher.matches()) {
throw new WrongPasswordException("잘못된 비밀번호입니다.");
}
}

protected Password() {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.wooteco.wiki.exception;

import org.springframework.http.HttpStatus;

public class DuplicateEmailException extends WikiException {
public DuplicateEmailException(String message) {
super(message, HttpStatus.CONFLICT);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.wooteco.wiki.exception;

import org.springframework.http.HttpStatus;

public class MemberNotFoundException extends WikiException {
public MemberNotFoundException(String message) {
super(message, HttpStatus.NOT_FOUND);
}
}
6 changes: 5 additions & 1 deletion src/main/java/com/wooteco/wiki/exception/WikiException.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
public class WikiException extends RuntimeException {
private final HttpStatus httpStatus;

public WikiException(String message, HttpStatus httpStatus) {
public WikiException(String message) {
this(message, HttpStatus.INTERNAL_SERVER_ERROR);
}

WikiException(String message, HttpStatus httpStatus) {
super(message);
this.httpStatus = httpStatus;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.wooteco.wiki.exception;

import static org.springframework.http.HttpStatus.BAD_REQUEST;

public class WrongEmailException extends WikiException {
public WrongEmailException(String message) {
super(message, BAD_REQUEST);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.wooteco.wiki.exception;

import static org.springframework.http.HttpStatus.BAD_REQUEST;

public class WrongPasswordException extends WikiException {
public WrongPasswordException(String message) {
super(message, BAD_REQUEST);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package com.wooteco.wiki.repository;

import com.wooteco.wiki.domain.Email;
import com.wooteco.wiki.domain.Member;
import com.wooteco.wiki.domain.MemberState;
import com.wooteco.wiki.domain.Password;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);
Optional<Member> findByEmail(Email email);

Optional<Member> findByEmailAndPassword(String email, String password);
Optional<Member> findByEmailAndPassword(Email email, Password password);

Optional<Member> findByNicknameAndState(String nickName, MemberState state);
}
50 changes: 48 additions & 2 deletions src/main/java/com/wooteco/wiki/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,61 @@
package com.wooteco.wiki.service;

import static com.wooteco.wiki.domain.MemberState.INITIAL;
import static com.wooteco.wiki.domain.MemberState.WAITING;

import com.wooteco.wiki.domain.Email;
import com.wooteco.wiki.domain.Member;
import com.wooteco.wiki.domain.Password;
import com.wooteco.wiki.domain.TokenManager;
import com.wooteco.wiki.dto.AuthTokens;
import com.wooteco.wiki.dto.JoinRequest;
import com.wooteco.wiki.dto.LoginRequest;
import com.wooteco.wiki.exception.DuplicateEmailException;
import com.wooteco.wiki.exception.MemberNotFoundException;
import com.wooteco.wiki.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class AuthService {
private final TokenManager tokenManager;
private final MemberRepository memberRepository;

public AuthTokens login(LoginRequest loginRequest) {
return null;
Email email = new Email(loginRequest.email());
Password password = new Password(loginRequest.password());
Member member = memberRepository.findByEmailAndPassword(email, password)
.orElseThrow(() -> new MemberNotFoundException("아이디 혹은 비밀번호가 잘못되었습니다."));
return generateAuthTokens(member);
}

private AuthTokens generateAuthTokens(Member member) {
String accessToken = tokenManager.generateAccessToken(member);
String refreshToken = tokenManager.generateRefreshToken(member);
return new AuthTokens(accessToken, refreshToken);
}

public AuthTokens join(JoinRequest joinRequest) {
return null;
String nickname = joinRequest.nickname();
Member member = memberRepository.findByNicknameAndState(nickname, INITIAL)
.orElseGet(() -> makeNewMember(joinRequest));
member.updateEmailAndPasswordWhenINITIAL(joinRequest.email(), joinRequest.password());
memberRepository.saveAndFlush(member);
return generateAuthTokens(member);
}

private Member makeNewMember(JoinRequest joinRequest) {
if (memberRepository.findByEmail(new Email(joinRequest.email())).isPresent()) {
throw new DuplicateEmailException("이미 가입된 이메일입니다.");
}
return Member.builder()
.email(joinRequest.email())
.password(joinRequest.password())
.state(WAITING)
.nickname(joinRequest.nickname())
.build();
}
}
25 changes: 25 additions & 0 deletions src/main/java/com/wooteco/wiki/util/Sha256Encryptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.wooteco.wiki.util;

import com.wooteco.wiki.exception.WikiException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class Sha256Encryptor {
public static String encrypt(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
throw new WikiException("암호화 과정에 문제가 발생했습니다.");
}
}
}
7 changes: 2 additions & 5 deletions src/test/java/com/wooteco/wiki/WikiApplicationTests.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package com.wooteco.wiki;

import com.wooteco.wiki.testinfra.ActiveProfileSpringBootTest;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;

@SpringBootTest
@ActiveProfiles(profiles = {"local", "info-logging"})
class WikiApplicationTests {
class WikiApplicationTests extends ActiveProfileSpringBootTest {

@Test
void contextLoads() {
Expand Down
17 changes: 17 additions & 0 deletions src/test/java/com/wooteco/wiki/domain/EmailTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.wooteco.wiki.domain;

import com.wooteco.wiki.exception.WrongEmailException;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class EmailTest {
@ParameterizedTest
@ValueSource(strings = {"a", "b", "a@b", "A#d.com"})
@DisplayName("이메일 형식이 아닌 경우 예외가 발생하는지 확인")
void failCauseWrongEmail(String rawEmail) {
Assertions.assertThatThrownBy(() -> new Email(rawEmail))
.isInstanceOf(WrongEmailException.class);
}
}
Loading