-
Notifications
You must be signed in to change notification settings - Fork 0
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
feat: 관리자 인증 기능을 구현한다 #37
Changes from all commits
443a56f
e554ca1
0a3a813
ab4b26f
6d66b4a
2c9eef7
b0ca1fc
93e4724
7ead965
be5a429
6c929ba
55b13a1
d02be3b
b62f73d
b8ddb8d
99a5b5e
41788fc
c072bd6
3115b3c
48b6564
1ab3e4d
c63397c
fe6d42f
cba1d28
2912e1e
2b0b85f
db2dc41
0af78a7
a74eabb
ea9ee8c
a52a28e
49f6a0a
dae8193
9a28b47
33ea1cd
b96c120
56c22ee
d33bfc5
4aae7e6
c491d58
4c9ebfb
551d5d4
ce994a9
10cf2e7
6eb2887
99553e8
ab57bc3
ffc72da
dc412d0
0b72297
7e5d3bd
46fb99a
213b5a7
782cb53
b9aa487
fccd653
ebbfec8
58160b3
2df5b04
c08ec60
c78c9ea
b85000f
bd171df
edc1577
9f21e41
0741feb
15e5a93
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
:toc: left | ||
:source-highlighter: highlightjs | ||
:sectlinks: | ||
:toclevels: 2 | ||
:sectlinks: | ||
:sectnums: | ||
|
||
== Admin | ||
|
||
=== 회원 가입(POST api/admins/auth/sign-up) | ||
|
||
==== 요청 | ||
include::{snippets}/admin-auth-controller-test/관리자_회원가입/http-request.adoc[] | ||
include::{snippets}/admin-auth-controller-test/관리자_회원가입/request-fields.adoc[] | ||
|
||
==== 응답 | ||
include::{snippets}/admin-auth-controller-test/관리자_회원가입/http-response.adoc[] | ||
include::{snippets}/admin-auth-controller-test/관리자_회원가입/response-headers.adoc[] | ||
include::{snippets}/admin-auth-controller-test/관리자_회원가입/response-fields.adoc[] | ||
|
||
|
||
=== 로그인(POST api/admins/auth/login) | ||
|
||
==== 요청 | ||
include::{snippets}/admin-auth-controller-test/관리자_로그인/http-request.adoc[] | ||
include::{snippets}/admin-auth-controller-test/관리자_로그인/request-fields.adoc[] | ||
|
||
==== 응답 | ||
include::{snippets}/admin-auth-controller-test/관리자_로그인/http-response.adoc[] | ||
include::{snippets}/admin-auth-controller-test/관리자_로그인/response-headers.adoc[] | ||
include::{snippets}/admin-auth-controller-test/관리자_로그인/response-fields.adoc[] | ||
|
||
|
||
=== 엑세스 토큰 재발행(POST api/admins/auth/access-token-regeneration) | ||
|
||
==== 요청 | ||
include::{snippets}/admin-auth-controller-test/관리자_액세스_토큰_재발행/http-request.adoc[] | ||
include::{snippets}/admin-auth-controller-test/관리자_액세스_토큰_재발행/request-headers.adoc[] | ||
|
||
==== 응답 | ||
include::{snippets}/admin-auth-controller-test/관리자_액세스_토큰_재발행/http-response.adoc[] | ||
include::{snippets}/admin-auth-controller-test/관리자_액세스_토큰_재발행/response-fields.adoc[] |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package com.atwoz.admin.application.auth; | ||
|
||
import com.atwoz.admin.application.auth.dto.AdminAccessTokenResponse; | ||
import com.atwoz.admin.application.auth.dto.AdminLoginRequest; | ||
import com.atwoz.admin.application.auth.dto.AdminProfileSignUpRequest; | ||
import com.atwoz.admin.application.auth.dto.AdminSignUpRequest; | ||
import com.atwoz.admin.application.auth.dto.AdminTokenResponse; | ||
import com.atwoz.admin.domain.admin.Admin; | ||
import com.atwoz.admin.domain.admin.AdminRepository; | ||
import com.atwoz.admin.domain.admin.AdminTokenProvider; | ||
import com.atwoz.admin.exception.exceptions.AdminNotFoundException; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
@RequiredArgsConstructor | ||
@Service | ||
@Transactional | ||
public class AdminAuthService { | ||
|
||
private static final String ID = "id"; | ||
|
||
private final AdminRepository adminRepository; | ||
private final AdminTokenProvider adminTokenProvider; | ||
|
||
public AdminTokenResponse signUp(final AdminSignUpRequest adminSignUpRequest) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. dto로의 변환은 레이어드 아키텍처에서 컨트롤러에서 해주는 것이 책임이라고 생각하는데 어떻게 생각하시나요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 응용레이어에서 AT와 RT를 만들어서 컨트롤러로 넘겨주기 때문에 이를 컨트롤러에서 받기 위한 DTO를 만들어서 사용했습니다. 응답 바디에 담기는 DTO 타입은 저도 컨트롤러에서 하는게 맞다고 생각합니다! 하지만 지금은 응용 레이어에서 컨트롤러로 넘겨주기 위한 DTO이기 때문에 큰 문제가 없어보이는데 어떻게 생각하시나요? 컨트롤러에서 응답 바디에 넘겨주는 DTO의 타입은 AdminAccessTokenResponse입니다! |
||
AdminProfileSignUpRequest adminProfileSignUpRequest = adminSignUpRequest.adminProfileSignUpRequest(); | ||
Admin admin = Admin.createWith( | ||
adminSignUpRequest.email(), | ||
adminSignUpRequest.password(), | ||
adminSignUpRequest.confirmPassword(), | ||
adminProfileSignUpRequest.name(), | ||
adminProfileSignUpRequest.phoneNumber() | ||
); | ||
Admin savedAdmin = adminRepository.save(admin); | ||
|
||
return createAdminTokenResponse(savedAdmin.getId()); | ||
} | ||
|
||
private AdminTokenResponse createAdminTokenResponse(final Long id) { | ||
return new AdminTokenResponse( | ||
adminTokenProvider.createAccessToken(id), | ||
adminTokenProvider.createRefreshToken(id) | ||
); | ||
} | ||
|
||
public AdminTokenResponse login(final AdminLoginRequest adminLoginRequest) { | ||
Admin foundAdmin = findAdminByEmail(adminLoginRequest.email()); | ||
foundAdmin.validatePassword(adminLoginRequest.password()); | ||
|
||
return createAdminTokenResponse(foundAdmin.getId()); | ||
} | ||
|
||
public AdminAccessTokenResponse reGenerateAccessToken(final String refreshToken) { | ||
Admin foundAdmin = findAdminById(adminTokenProvider.extract(refreshToken, ID, Long.class)); | ||
|
||
return new AdminAccessTokenResponse(adminTokenProvider.createAccessToken(foundAdmin.getId())); | ||
} | ||
|
||
private Admin findAdminByEmail(final String email) { | ||
return adminRepository.findAdminByEmail(email) | ||
.orElseThrow(AdminNotFoundException::new); | ||
} | ||
|
||
private Admin findAdminById(final Long id) { | ||
return adminRepository.findAdminById(id) | ||
.orElseThrow(AdminNotFoundException::new); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.atwoz.admin.application.auth.dto; | ||
|
||
public record AdminAccessTokenResponse( | ||
String accessToken | ||
) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package com.atwoz.admin.application.auth.dto; | ||
|
||
import jakarta.validation.constraints.NotBlank; | ||
|
||
public record AdminLoginRequest( | ||
@NotBlank(message = "이메일을 입력해주세요") | ||
String email, | ||
|
||
@NotBlank(message = "비밀번호를 입력해주세요") | ||
String password | ||
) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package com.atwoz.admin.application.auth.dto; | ||
|
||
import jakarta.validation.constraints.NotBlank; | ||
|
||
public record AdminProfileSignUpRequest( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기도 validation 추가 해주세요! |
||
@NotBlank(message = "이름을 입력해주세요.") | ||
String name, | ||
|
||
@NotBlank(message = "전화번호를 입력해주세요.") | ||
String phoneNumber | ||
) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package com.atwoz.admin.application.auth.dto; | ||
|
||
import jakarta.validation.Valid; | ||
import jakarta.validation.constraints.NotBlank; | ||
import jakarta.validation.constraints.NotNull; | ||
|
||
public record AdminSignUpRequest( | ||
@NotBlank(message = "이메일을 입력해주세요") | ||
String email, | ||
|
||
@NotBlank(message = "비밀번호를 입력해주세요") | ||
String password, | ||
|
||
@NotBlank(message = "비밀번호 확인을 위해 다시한번 입력해주세요") | ||
String confirmPassword, | ||
|
||
@Valid | ||
@NotNull(message = "관리자 프로필 정보를 입력해주세요") | ||
AdminProfileSignUpRequest adminProfileSignUpRequest | ||
) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package com.atwoz.admin.application.auth.dto; | ||
|
||
public record AdminTokenResponse( | ||
String accessToken, | ||
String refreshToken | ||
) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package com.atwoz.admin.config; | ||
|
||
import com.atwoz.admin.ui.auth.interceptor.AdminLoginValidCheckerInterceptor; | ||
import com.atwoz.admin.ui.auth.support.resolver.AdminAuthArgumentResolver; | ||
import com.atwoz.admin.ui.auth.support.resolver.AdminRefreshTokenExtractionArgumentResolver; | ||
import com.atwoz.global.config.interceptor.PathMatcherInterceptor; | ||
import java.util.List; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.web.method.support.HandlerMethodArgumentResolver; | ||
import org.springframework.web.servlet.HandlerInterceptor; | ||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; | ||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | ||
|
||
import static com.atwoz.global.config.interceptor.support.HttpMethod.OPTIONS; | ||
|
||
@RequiredArgsConstructor | ||
@Configuration | ||
public class AdminAuthConfig implements WebMvcConfigurer { | ||
|
||
private final AdminAuthArgumentResolver adminAuthArgumentResolver; | ||
private final AdminRefreshTokenExtractionArgumentResolver adminRefreshTokenExtractionArgumentResolver; | ||
private final AdminLoginValidCheckerInterceptor adminLoginValidCheckerInterceptor; | ||
|
||
@Override | ||
public void addInterceptors(final InterceptorRegistry registry) { | ||
registry.addInterceptor(adminLoginValidCheckerInterceptor()); | ||
} | ||
|
||
private HandlerInterceptor adminLoginValidCheckerInterceptor() { | ||
return new PathMatcherInterceptor(adminLoginValidCheckerInterceptor) | ||
.excludePathPattern("/**", OPTIONS); | ||
} | ||
|
||
@Override | ||
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) { | ||
resolvers.add(adminAuthArgumentResolver); | ||
resolvers.add(adminRefreshTokenExtractionArgumentResolver); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package com.atwoz.admin.domain.admin; | ||
|
||
import com.atwoz.admin.domain.admin.vo.AdminStatus; | ||
import com.atwoz.admin.domain.admin.vo.Authority; | ||
import com.atwoz.admin.domain.admin.vo.Department; | ||
import com.atwoz.admin.exception.exceptions.InvalidPasswordException; | ||
import com.atwoz.admin.exception.exceptions.PasswordMismatchException; | ||
import jakarta.persistence.Column; | ||
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.AccessLevel; | ||
import lombok.AllArgsConstructor; | ||
import lombok.Builder; | ||
import lombok.EqualsAndHashCode; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Getter | ||
@Builder | ||
@EqualsAndHashCode(of = "id", callSuper = false) | ||
@AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
@Entity | ||
public class Admin { | ||
|
||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
|
||
@Column(unique = true, nullable = false) | ||
private String email; | ||
|
||
@Column(nullable = false) | ||
private String password; | ||
|
||
@Column(nullable = false) | ||
private String name; | ||
|
||
@Column(nullable = false) | ||
private String phoneNumber; | ||
|
||
@Enumerated(value = EnumType.STRING) | ||
@Column(nullable = false) | ||
private Authority authority; | ||
|
||
@Enumerated(value = EnumType.STRING) | ||
@Column(nullable = false) | ||
private Department department; | ||
|
||
@Enumerated(value = EnumType.STRING) | ||
@Column(nullable = false) | ||
private AdminStatus adminStatus; | ||
|
||
public static Admin createWith(final String email, | ||
final String password, | ||
final String confirmPassword, | ||
final String name, | ||
final String phoneNumber) { | ||
validatePasswordsMatch(password, confirmPassword); | ||
return Admin.builder() | ||
.email(email) | ||
.password(password) | ||
.name(name) | ||
.phoneNumber(phoneNumber) | ||
.authority(Authority.ADMIN) | ||
.department(Department.OPERATION) | ||
.adminStatus(AdminStatus.PENDING) | ||
.build(); | ||
} | ||
|
||
private static void validatePasswordsMatch(final String password, | ||
final String confirmPassword) { | ||
if (!password.equals(confirmPassword)) { | ||
throw new PasswordMismatchException(); | ||
} | ||
} | ||
|
||
public void validatePassword(final String password) { | ||
if (!password.equals(this.password)) { | ||
throw new InvalidPasswordException(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package com.atwoz.admin.domain.admin; | ||
|
||
import java.util.Optional; | ||
|
||
public interface AdminRepository { | ||
|
||
Admin save(Admin admin); | ||
|
||
Optional<Admin> findAdminById(Long id); | ||
|
||
Optional<Admin> findAdminByEmail(String email); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package com.atwoz.admin.domain.admin; | ||
|
||
public interface AdminTokenProvider { | ||
|
||
String createAccessToken(Long id); | ||
|
||
String createRefreshToken(Long id); | ||
|
||
<T> T extract(String token, String claimName, Class<T> classType); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 제네릭을 사용하신 이유가 따로 있을까요?! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 클레임에는 문자열(이메일), 숫자(id)등이 들어값니다. 그래서 추출할 때 만약 문자열 값과 숫자 값을 모두 추출할 일이 있으면 해당 메서드를 여러개 만들어야 해서 중복을 최소한해보고자 제네릭을 이용해서 구현해보았습니다. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package com.atwoz.admin.domain.admin.vo; | ||
|
||
public enum AdminStatus { | ||
|
||
AVAILABLE, | ||
UNAVAILABLE, | ||
PENDING | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
토큰을 주는 것은 도메인 보다는 서비스가 낫지 않을까 조심스럽게 의견 드려봅니다..! 다르게 생각한다면 편하게 말씀해주세요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저는 토큰을 암호화하는 부분이 외부라이브러리를 이용해서 인프라 레이어로 두고 이를 인터페이스화시킨 것을 도메인 레이어에서 둬서 응용 계층에서 호출해서 사용하도록 구현하려고 했는데 혹시 서비스 레이어에 두는게 더 좋은 것 같다고 생각한 이유를 알 수 있을까요?