-
Notifications
You must be signed in to change notification settings - Fork 16
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
관리자 기능 백엔드 구현 #780
관리자 기능 백엔드 구현 #780
Changes from 70 commits
96e1287
d149c76
8cea490
d256694
9341f48
e8a0e4b
e834830
13719d2
b42233c
6c1bf63
fa168e0
1f04c65
319e1f1
637f942
7b1fd0f
c6a74af
3da90f9
4808192
540fd63
3989c14
a41517e
700ba71
e002a84
e80aedd
a696b33
adacf8c
6eaf012
b44580f
9397c14
8c78c40
b19a32c
0ac1231
f887d59
9d668f7
281d0cc
98fd96c
47e4d08
afb2e94
02d375d
b98d7f1
3ae8de8
8e3fe70
29c05aa
e43fa33
679a1a2
cd51552
3a0752a
161a904
18583b3
9614be7
cc6a8b7
5457d43
238889d
8fd59f6
b59b43e
beb7351
47809f6
cecb6f6
5db7c03
6abd773
ca62ca7
6e78496
0edb405
c7d9cb8
1bdf8e2
5aa99bd
531eedf
e4aae46
531e2b8
34cf691
ebc37a4
fe4e5ca
d429a5a
bedc68c
e4b5941
e7bece6
af24938
d997a8d
1a12237
114cd24
8a374bb
c899d7a
f5cc040
78d696b
bf38a58
6e61d2c
3438afd
5df3614
81bde6d
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,87 @@ | ||
package hanglog.admin; | ||
|
||
import static hanglog.admin.domain.type.AdminType.MASTER; | ||
import static hanglog.global.exception.ExceptionCode.INVALID_REQUEST; | ||
import static hanglog.global.exception.ExceptionCode.NOT_FOUND_REFRESH_TOKEN; | ||
import static org.springframework.http.HttpHeaders.AUTHORIZATION; | ||
|
||
import hanglog.admin.domain.repository.AdminMemberRepository; | ||
import hanglog.auth.AdminAuth; | ||
import hanglog.auth.domain.Accessor; | ||
import hanglog.global.exception.BadRequestException; | ||
import hanglog.global.exception.RefreshTokenException; | ||
import hanglog.login.domain.MemberTokens; | ||
import hanglog.login.domain.repository.RefreshTokenRepository; | ||
import hanglog.login.infrastructure.BearerAuthorizationExtractor; | ||
import hanglog.login.infrastructure.JwtProvider; | ||
import jakarta.servlet.http.Cookie; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import java.util.Arrays; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.core.MethodParameter; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.bind.support.WebDataBinderFactory; | ||
import org.springframework.web.context.request.NativeWebRequest; | ||
import org.springframework.web.method.support.HandlerMethodArgumentResolver; | ||
import org.springframework.web.method.support.ModelAndViewContainer; | ||
|
||
@RequiredArgsConstructor | ||
@Component | ||
public class AdminLoginArgumentResolver implements HandlerMethodArgumentResolver { | ||
|
||
private static final String REFRESH_TOKEN = "refresh-token"; | ||
|
||
private final JwtProvider jwtProvider; | ||
|
||
private final BearerAuthorizationExtractor extractor; | ||
|
||
private final RefreshTokenRepository refreshTokenRepository; | ||
|
||
private final AdminMemberRepository adminMemberRepository; | ||
|
||
@Override | ||
public boolean supportsParameter(final MethodParameter parameter) { | ||
return parameter.withContainingClass(Long.class) | ||
.hasParameterAnnotation(AdminAuth.class); | ||
} | ||
|
||
@Override | ||
public Accessor resolveArgument( | ||
final MethodParameter parameter, | ||
final ModelAndViewContainer mavContainer, | ||
final NativeWebRequest webRequest, | ||
final WebDataBinderFactory binderFactory | ||
) { | ||
final HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); | ||
if (request == null) { | ||
throw new BadRequestException(INVALID_REQUEST); | ||
} | ||
|
||
final String refreshToken = extractRefreshToken(request.getCookies()); | ||
final String accessToken = extractor.extractAccessToken(webRequest.getHeader(AUTHORIZATION)); | ||
jwtProvider.validateTokens(new MemberTokens(refreshToken, accessToken)); | ||
|
||
final Long memberId = Long.valueOf(jwtProvider.getSubject(accessToken)); | ||
|
||
if (adminMemberRepository.existsByIdAndAdminType(memberId, MASTER)) { | ||
return Accessor.master(memberId); | ||
} | ||
return Accessor.admin(memberId); | ||
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. adminMember를 조회하는 이 부분만 현재 LoginArgumentResolver랑의 차이점 같은데, 따로 구현하신 이유가 있을까요 ?! memberId가 admin이랑 일반 user랑 겹칠 수 있어서 .. ? 구분할 방법이 없어서 사용하셨다면 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. 이 부분은 슬랙을 통해 말씀드린 내용 그대로! 다른 백엔드분들 의견 모이는대로 반영하겠습니다. |
||
} | ||
|
||
private String extractRefreshToken(final Cookie... cookies) { | ||
if (cookies == null) { | ||
throw new RefreshTokenException(NOT_FOUND_REFRESH_TOKEN); | ||
} | ||
return Arrays.stream(cookies) | ||
.filter(this::isValidRefreshToken) | ||
.findFirst() | ||
.orElseThrow(() -> new RefreshTokenException(NOT_FOUND_REFRESH_TOKEN)) | ||
.getValue(); | ||
} | ||
|
||
private boolean isValidRefreshToken(final Cookie cookie) { | ||
return REFRESH_TOKEN.equals(cookie.getName()) && | ||
refreshTokenRepository.existsById(cookie.getValue()); | ||
} | ||
Comment on lines
+92
to
+95
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. refreshTokenRepository 일반 유저 리프레시토큰 저장용으로 만든거라, 잘하면 어드민이 아니라 일반 유저의 토큰으로 로그인 시도했을 때 Authority.ADMIN으로 로그인이 돼버릴지도?.. 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. 배포시에 일반 행록과 admin 행록의 도메인이 다를 것이기 때문에 캐시도 따로 저장되서, 해당 문제는 발생하지 않습니다! 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. 앗 아니 레디스에는 똑같이 적용 되는데, 브라우저에서 캐시가 다르게 저장되기 때문에 hanglog.com에 관리자 페이지 캐시로 접근할 수 는 없다는 의미였습니다! 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. 검증 시 Admin, master id 인지 검증 하는 로직 추가했습니다! |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package hanglog.admin; | ||
|
||
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.config.annotation.WebMvcConfigurer; | ||
|
||
@Configuration | ||
@RequiredArgsConstructor | ||
public class AdminLoginResolverConfig implements WebMvcConfigurer { | ||
|
||
private final AdminLoginArgumentResolver adminLoginArgumentResolver; | ||
|
||
@Override | ||
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resolvers) { | ||
resolvers.add(adminLoginArgumentResolver); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package hanglog.admin; | ||
|
||
import hanglog.admin.infrastructure.BCryptPasswordEncoder; | ||
import hanglog.admin.infrastructure.PasswordEncoder; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
|
||
@Configuration | ||
public class SecurityConfig { | ||
@Bean | ||
public PasswordEncoder passwordEncoder() { | ||
return new BCryptPasswordEncoder(); | ||
} | ||
Comment on lines
+9
to
+13
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. BCrypt 말고 다른 기본 단방향 암호화 알고리즘으로는 어떤 문제가 있었나요? 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. 다른 방법들을 예시로 들자면
요약하자면 BCrypt가 많은 케이스를 통해 검증된 방식이고, 자료가 많아 개발 시간이 단축되며, 구현 방식 또한 단순하다는 것이 결정에 가장 크게 작용한 요인입니다! 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. 결국 BCrypt를 사용하기 위해 스프링 시큐리티를 적용한 거라면 너무 과도한 의존성 추가가 아닐까 싶네요..! 간단하게 BCypt 라이브러리만 추가하는게 어떠신지....! 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. 반영하였습니다! 저도 그부분이 걸렸지만 방법을 못찾았었는데, 디노 짱짱입니다. 덕분에 불필요하게 exclude 하던 서브모듈 또한 변경하였습니다. 감사합니다👍 |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package hanglog.admin.domain; | ||
|
||
import static jakarta.persistence.EnumType.STRING; | ||
import static jakarta.persistence.GenerationType.IDENTITY; | ||
import static lombok.AccessLevel.PROTECTED; | ||
|
||
import hanglog.admin.domain.type.AdminType; | ||
import hanglog.member.domain.MemberState; | ||
import jakarta.persistence.Column; | ||
import jakarta.persistence.Entity; | ||
import jakarta.persistence.Enumerated; | ||
import jakarta.persistence.GeneratedValue; | ||
import jakarta.persistence.Id; | ||
import java.time.LocalDateTime; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
import org.hibernate.annotations.SQLDelete; | ||
import org.hibernate.annotations.Where; | ||
import org.springframework.data.annotation.CreatedDate; | ||
import org.springframework.data.annotation.LastModifiedDate; | ||
|
||
@Entity | ||
@Getter | ||
@NoArgsConstructor(access = PROTECTED) | ||
@SQLDelete(sql = "UPDATE admin_member SET status = 'DELETED' WHERE id = ?") | ||
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. 오 AdminUser에 soft delete가 필요하다고 생각하신 이유가 궁금해요! 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. 저도 고민했던 내용인데요! 사실 Soft Delete와 Hard Delete 모두 크게 차이가 나는 장단점이 없다가 결론이었습니다! 저희 시스템 규모에서 AdminUser는 진짜 많아져 봐야 100개가 안될 데이터라서, 성능적으로 비교하는건 의미가 적겠더라구요. |
||
@Where(clause = "status = 'ACTIVE'") | ||
public class AdminMember { | ||
|
||
@Id | ||
@GeneratedValue(strategy = IDENTITY) | ||
private Long id; | ||
|
||
@Column(nullable = false, unique = true, length = 20) | ||
private String userName; | ||
|
||
@Column(nullable = false, length = 64) | ||
private String password; | ||
|
||
@Column(nullable = false) | ||
private LocalDateTime lastLoginDate; | ||
|
||
@Column(nullable = false) | ||
@Enumerated(value = STRING) | ||
private AdminType adminType; | ||
|
||
@Enumerated(value = STRING) | ||
private MemberState status; | ||
|
||
@CreatedDate | ||
@Column(updatable = false) | ||
private LocalDateTime createdAt; | ||
|
||
@LastModifiedDate | ||
private LocalDateTime modifiedAt; | ||
|
||
public AdminMember(final Long id, final String userName, final String password, final AdminType adminType) { | ||
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. 오 그러네요?!?! 근데 Member는 createdAt 어떻게 유지되고 있는거죠..?! |
||
this.id = id; | ||
this.userName = userName; | ||
this.password = password; | ||
this.lastLoginDate = LocalDateTime.now(); | ||
this.adminType = adminType; | ||
this.status = MemberState.ACTIVE; | ||
this.createdAt = LocalDateTime.now(); | ||
this.modifiedAt = LocalDateTime.now(); | ||
} | ||
|
||
public AdminMember(final String userName, final String password, final AdminType adminType) { | ||
this(null, userName, password, adminType); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package hanglog.admin.domain.repository; | ||
|
||
import hanglog.admin.domain.AdminMember; | ||
import hanglog.admin.domain.type.AdminType; | ||
import java.util.Optional; | ||
import org.springframework.data.jpa.repository.JpaRepository; | ||
|
||
public interface AdminMemberRepository extends JpaRepository<AdminMember, Long> { | ||
|
||
Optional<AdminMember> findByUserName(String userName); | ||
|
||
Boolean existsByIdAndAdminType(Long id, AdminType adminType); | ||
|
||
Boolean existsByUserName(String userName); | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,17 @@ | ||||||||||||||
package hanglog.admin.domain.type; | ||||||||||||||
|
||||||||||||||
import static hanglog.global.exception.ExceptionCode.NULL_ADMIN_AUTHORITY; | ||||||||||||||
|
||||||||||||||
import hanglog.global.exception.AdminException; | ||||||||||||||
import java.util.Arrays; | ||||||||||||||
|
||||||||||||||
public enum AdminType { | ||||||||||||||
ADMIN, MASTER; | ||||||||||||||
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. 저희 이넘 첫 줄 개행 한것도 있고 안한것도 있던데 이것도 클래스니까 개행합니까? (
Suggested change
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. 일단 AdminType은 Auth랑 같은 방식을 따르는게 맞는것 같아서 개행을 안했었는데, Trip까지 전부 보니까 Auth가 잘못되있었던것도 같구요..? 둘 모두 개행 하는 쪽으로 수정하였습니다. |
||||||||||||||
|
||||||||||||||
public static AdminType getMappedAdminType(final String adminType) { | ||||||||||||||
return Arrays.stream(values()) | ||||||||||||||
.filter(value -> value.name().toUpperCase().equals(adminType)) | ||||||||||||||
.findAny() | ||||||||||||||
.orElseThrow(() -> new AdminException(NULL_ADMIN_AUTHORITY)); | ||||||||||||||
} | ||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package hanglog.admin.dto.request; | ||
|
||
import static lombok.AccessLevel.PRIVATE; | ||
|
||
import jakarta.validation.constraints.NotNull; | ||
import jakarta.validation.constraints.Size; | ||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Getter | ||
@AllArgsConstructor | ||
@NoArgsConstructor(access = PRIVATE) | ||
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.
|
||
public class AdminLoginRequest { | ||
|
||
@NotNull(message = "사용자 이름을 입력해주세요.") | ||
@Size(max = 20, message = "사용자 이름은 20자를 초과할 수 없습니다.") | ||
private String userName; | ||
|
||
@NotNull(message = "비밀번호를 입력해주세요.") | ||
@Size(min = 4, max = 20, message = "비밀번호는 4자 이상, 20자 이하여야 합니다.") | ||
private String password; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package hanglog.admin.dto.request; | ||
|
||
|
||
import static lombok.AccessLevel.PRIVATE; | ||
|
||
import jakarta.validation.constraints.NotNull; | ||
import jakarta.validation.constraints.Size; | ||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Getter | ||
@AllArgsConstructor | ||
@NoArgsConstructor(access = PRIVATE) | ||
public class AdminMemberCreateRequest { | ||
|
||
@NotNull(message = "사용자 이름을 입력해주세요.") | ||
@Size(max = 20, message = "사용자 이름은 20자를 초과할 수 없습니다.") | ||
private String userName; | ||
|
||
@NotNull(message = "비밀번호를 입력해주세요.") | ||
@Size(min = 4, max = 20, message = "비밀번호는 4자 이상, 20자 이하여야 합니다.") | ||
private String password; | ||
Comment on lines
+17
to
+19
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. 얘 DB랑 max 조건 다른 이유 있나요?.? 궁금 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. 비밀번호 암호화하면서 60자로 늘어나기 때문입니다! |
||
|
||
@NotNull(message = "관리자 권한을 선택해 주세요.") | ||
private String adminType; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package hanglog.admin.dto.request; | ||
|
||
|
||
import static lombok.AccessLevel.PRIVATE; | ||
|
||
import jakarta.validation.constraints.NotNull; | ||
import jakarta.validation.constraints.Size; | ||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Getter | ||
@AllArgsConstructor | ||
@NoArgsConstructor(access = PRIVATE) | ||
public class PasswordUpdateRequest { | ||
|
||
@NotNull(message = "기존 비밀번호를 입력해주세요.") | ||
@Size(min = 4, max = 20, message = "비밀번호는 4자 이상, 20자 이하여야 합니다.") | ||
private String currentPassword; | ||
|
||
@NotNull(message = "새로운 비밀번호를 입력해주세요.") | ||
@Size(min = 4, max = 20, message = "비밀번호는 4자 이상, 20자 이하여야 합니다.") | ||
private String newPassword; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package hanglog.admin.dto.response; | ||
|
||
import hanglog.admin.domain.AdminMember; | ||
import lombok.Getter; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
@Getter | ||
@RequiredArgsConstructor | ||
public class AdminMemberResponse { | ||
|
||
private final Long id; | ||
private final String userName; | ||
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. 혹시 ... username이 더 익숙하지않으신가요? 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. ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ 나도 username이 좋아 헤헤 .. 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. 'userName' |
||
private final String adminType; | ||
|
||
public static AdminMemberResponse from(final AdminMember adminMember) { | ||
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. 생성자 private으로 막아놓고 정팩메만 사용하면 좋을거 같아요 ~~! 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. 1a12237 얍 |
||
return new AdminMemberResponse( | ||
adminMember.getId(), | ||
adminMember.getUserName(), | ||
adminMember.getAdminType().name() | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package hanglog.admin.infrastructure; | ||
|
||
import org.mindrot.jbcrypt.BCrypt; | ||
|
||
public class BCryptPasswordEncoder implements PasswordEncoder { | ||
@Override | ||
public String encode(final String password) { | ||
return BCrypt.hashpw(password, BCrypt.gensalt()); | ||
} | ||
|
||
@Override | ||
public boolean matches(final String password, final String hashed) { | ||
return BCrypt.checkpw(password, hashed); | ||
} | ||
Comment on lines
+6
to
+14
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. 오 생각보다 굉장히 간단하게 구현할 수 있군요 .. ! |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package hanglog.admin.infrastructure; | ||
|
||
public interface PasswordEncoder { | ||
String encode(String password); | ||
|
||
boolean matches(String password, String hashed); | ||
} |
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.
0.4이전 버전 취약점 경고뜨는데, 0.4로 바꾸는 건 어떤가요?
0.4도 호환 잘되는 것 같습니다!
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.
감사합니다 너무 좋아요 너무 깔끔해