Skip to content

Commit 3f3b72b

Browse files
authored
[FEAT] 스프링 인증/인가 개발 (#32)
* ✨feat: Spring Security, jwt, h2 의존성 추가 * ✨feat: Spring Security + JWT 작업 - JWT 커스텀 필터 추가 - JWT 유틸리티 작성 * ✨feat: USER 도메인 개발 - 테스트 컨트롤러 추가 - Entity 개발 * ✨feat: 인증 작업을 위한 비즈니스 로직 개발 * ✨feat: baseEntity 추가 * ✨feat: 스프링부트 실행 시점 설정 정보 로깅 추가 * ✨feat: http 테스트코드 작성 * ✨feat: yml 변경점 적용 * 🩹fix: jwt 시크릿 노출 제외 및 만료시간 노출 * 🩹fix: UserInfoResponse DTO 필드명 불일치 수정 * ✨feat: refresh관련 로직 수정 - API 인가 refresh 분기처리 추가 - accesstoken 재발급 엔드포인트 개발 - Refresh Token JSON -> Cookie 변경 (secure, httponly 설정) - .http 테스트 코드 작성 * ✨feat: 로그아웃 엔드포인트 개발
1 parent ea5bea9 commit 3f3b72b

File tree

24 files changed

+1063
-2
lines changed

24 files changed

+1063
-2
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,4 +747,7 @@ FodyWeavers.xsd
747747
### VisualStudio Patch ###
748748
# Additional files built by Visual Studio
749749

750+
# Local Profile
751+
/src/main/resources/application-local.yml
752+
750753
# End of https://www.toptal.com/developers/gitignore/api/java,intellij+all,intellij+iml,netbeans,macos,windows,visualstudiocode,visualstudio,gradle,groovy,linux,sonarqube,sonar,dotenv,redis,git

build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ dependencies {
3636
implementation 'com.twelvemonkeys.imageio:imageio-core:3.12.0'
3737
implementation 'org.sejda.imageio:webp-imageio:0.1.6'
3838

39+
// Auth
40+
implementation 'org.springframework.boot:spring-boot-starter-security'
41+
testImplementation 'org.springframework.security:spring-security-test'
42+
implementation 'io.jsonwebtoken:jjwt:0.12.6'
43+
44+
// H2 Database
45+
runtimeOnly 'com.h2database:h2'
3946

4047
}
4148

src/main/java/team/wego/wegobackend/WegobackendApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
56

67
@SpringBootApplication
8+
@EnableJpaAuditing
79
public class WegobackendApplication {
810

911
public static void main(String[] args) {
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package team.wego.wegobackend.auth.application;
2+
3+
import jakarta.servlet.http.Cookie;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
7+
import org.springframework.stereotype.Service;
8+
import org.springframework.transaction.annotation.Transactional;
9+
import team.wego.wegobackend.auth.application.dto.request.LoginRequest;
10+
import team.wego.wegobackend.auth.application.dto.request.SignupRequest;
11+
import team.wego.wegobackend.auth.application.dto.response.LoginResponse;
12+
import team.wego.wegobackend.auth.application.dto.response.RefreshResponse;
13+
import team.wego.wegobackend.auth.application.dto.response.SignupResponse;
14+
import team.wego.wegobackend.common.security.Role;
15+
import team.wego.wegobackend.common.security.jwt.JwtTokenProvider;
16+
import team.wego.wegobackend.user.domain.User;
17+
import team.wego.wegobackend.user.repository.UserRepository;
18+
19+
@Slf4j
20+
@Service
21+
@RequiredArgsConstructor
22+
@Transactional(readOnly = true)
23+
public class AuthService {
24+
25+
private final UserRepository userRepository;
26+
27+
private final BCryptPasswordEncoder passwordEncoder;
28+
29+
private final JwtTokenProvider jwtTokenProvider;
30+
31+
/**
32+
* 회원가입
33+
*
34+
* @return
35+
*/
36+
@Transactional
37+
public SignupResponse signup(SignupRequest request) {
38+
// 이메일 중복 체크
39+
if (userRepository.existsByEmail(request.getEmail())) {
40+
throw new IllegalArgumentException("이미 존재하는 회원");
41+
}
42+
43+
User user = User.builder().email(request.getEmail())
44+
.password(passwordEncoder.encode(request.getPassword())).nickName(request.getNickName())
45+
.phoneNumber(request.getPhoneNumber()).role(Role.ROLE_USER) //default
46+
.build();
47+
48+
userRepository.save(user);
49+
50+
return SignupResponse.from(user);
51+
}
52+
53+
/**
54+
* 로그인
55+
*/
56+
public LoginResponse login(LoginRequest request) {
57+
58+
User user = userRepository.findByEmail(request.getEmail())
59+
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자"));
60+
61+
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
62+
throw new IllegalArgumentException("잘못된 비밀번호");
63+
}
64+
65+
if (user.getDeleted()) {
66+
throw new IllegalArgumentException("탈퇴한 계정");
67+
}
68+
69+
String accessToken = jwtTokenProvider.createAccessToken(user.getEmail(),
70+
user.getRole().name());
71+
72+
String refreshToken = jwtTokenProvider.createRefreshToken(user.getEmail());
73+
74+
Long expiresIn = jwtTokenProvider.getAccessTokenExpiresIn();
75+
76+
return LoginResponse.of(user, accessToken, refreshToken, expiresIn);
77+
}
78+
79+
/**
80+
* Access Token 재발급
81+
*/
82+
public RefreshResponse refresh(String refreshToken) {
83+
84+
if (!jwtTokenProvider.validateRefreshToken(refreshToken)) {
85+
throw new IllegalArgumentException("유효하지 않은 Refresh Token");
86+
}
87+
88+
String email = jwtTokenProvider.getEmailFromToken(refreshToken);
89+
90+
User user = userRepository.findByEmail(email)
91+
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자"));
92+
93+
if (user.getDeleted()) {
94+
throw new IllegalArgumentException("탈퇴한 계정");
95+
}
96+
97+
String newAccessToken = jwtTokenProvider.createAccessToken(
98+
user.getEmail(),
99+
user.getRole().name()
100+
);
101+
102+
Long expiresIn = jwtTokenProvider.getAccessTokenExpiresIn();
103+
104+
return RefreshResponse.of(newAccessToken, expiresIn);
105+
}
106+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package team.wego.wegobackend.auth.application.dto.request;
2+
3+
import jakarta.validation.constraints.Email;
4+
import jakarta.validation.constraints.NotBlank;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
8+
@Getter
9+
@NoArgsConstructor
10+
public class LoginRequest {
11+
12+
@Email(message = "올바른 이메일 형식이 아닙니다")
13+
@NotBlank(message = "이메일은 필수입니다")
14+
private String email;
15+
16+
@NotBlank(message = "비밀번호는 필수입니다")
17+
private String password;
18+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package team.wego.wegobackend.auth.application.dto.request;
2+
3+
import jakarta.validation.constraints.Email;
4+
import jakarta.validation.constraints.NotBlank;
5+
import jakarta.validation.constraints.Pattern;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
@Getter
10+
@NoArgsConstructor
11+
public class SignupRequest {
12+
13+
@Email(message = "올바른 이메일 형식이 아닙니다")
14+
@NotBlank(message = "이메일은 필수입니다")
15+
private String email;
16+
17+
@NotBlank(message = "비밀번호는 필수입니다")
18+
private String password;
19+
20+
@NotBlank(message = "이름은 필수입니다")
21+
private String nickName;
22+
23+
@NotBlank(message = "전화번호는 필수입니다")
24+
@Pattern(regexp = "^01[016789]-?\\d{3,4}-?\\d{4}$", message = "올바른 전화번호 형식이 아닙니다")
25+
private String phoneNumber;
26+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package team.wego.wegobackend.auth.application.dto.response;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnore;
4+
import com.fasterxml.jackson.annotation.JsonInclude;
5+
import java.time.LocalDateTime;
6+
import lombok.AccessLevel;
7+
import lombok.AllArgsConstructor;
8+
import lombok.Builder;
9+
import lombok.Getter;
10+
import lombok.NoArgsConstructor;
11+
import team.wego.wegobackend.user.application.dto.response.UserInfoResponse;
12+
import team.wego.wegobackend.user.domain.User;
13+
14+
@Getter
15+
@Builder
16+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
17+
@AllArgsConstructor(access = AccessLevel.PRIVATE)
18+
@JsonInclude(JsonInclude.Include.NON_NULL)
19+
public class LoginResponse {
20+
21+
private String accessToken;
22+
23+
@JsonIgnore
24+
private String refreshToken;
25+
26+
private String tokenType;
27+
28+
private Long expiresIn;
29+
30+
private LocalDateTime expiresAt;
31+
32+
private UserInfoResponse user;
33+
34+
public static LoginResponse of(User user, String accessToken, String refreshToken,
35+
Long expiresIn) {
36+
return LoginResponse.builder()
37+
.accessToken(accessToken)
38+
.refreshToken(refreshToken)
39+
.tokenType("Bearer")
40+
.expiresIn(expiresIn)
41+
.expiresAt(LocalDateTime.now().plusSeconds(expiresIn))
42+
.user(UserInfoResponse.from(user))
43+
.build();
44+
}
45+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package team.wego.wegobackend.auth.application.dto.response;
2+
3+
import java.time.LocalDateTime;
4+
import lombok.AccessLevel;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
10+
@Getter
11+
@Builder
12+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
13+
@AllArgsConstructor(access = AccessLevel.PRIVATE)
14+
public class RefreshResponse {
15+
16+
private String accessToken;
17+
18+
@Builder.Default
19+
private String tokenType = "Bearer";
20+
21+
private Long expiresIn;
22+
23+
private LocalDateTime expiresAt;
24+
25+
public static RefreshResponse of(String accessToken, Long expiresIn) {
26+
return RefreshResponse.builder()
27+
.accessToken(accessToken)
28+
.tokenType("Bearer")
29+
.expiresIn(expiresIn)
30+
.expiresAt(LocalDateTime.now().plusSeconds(expiresIn))
31+
.build();
32+
}
33+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package team.wego.wegobackend.auth.application.dto.response;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import java.time.LocalDateTime;
5+
import lombok.AccessLevel;
6+
import lombok.AllArgsConstructor;
7+
import lombok.Builder;
8+
import lombok.Getter;
9+
import lombok.NoArgsConstructor;
10+
import team.wego.wegobackend.user.domain.User;
11+
12+
@Getter
13+
@Builder
14+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
15+
@AllArgsConstructor(access = AccessLevel.PRIVATE)
16+
@JsonInclude(JsonInclude.Include.NON_NULL)
17+
public class SignupResponse {
18+
19+
private Long userId;
20+
21+
private String email;
22+
23+
private String nickName;
24+
25+
private String phoneNumber;
26+
27+
private LocalDateTime createdAt;
28+
29+
public static SignupResponse from(User user) {
30+
return SignupResponse.builder()
31+
.userId(user.getId())
32+
.email(user.getEmail())
33+
.nickName(user.getNickName())
34+
.phoneNumber(user.getPhoneNumber())
35+
.createdAt(user.getCreatedAt())
36+
.build();
37+
}
38+
}

0 commit comments

Comments
 (0)