Skip to content

Commit

Permalink
feat: 계정 관련 예외처리 (#203)
Browse files Browse the repository at this point in the history
* feat: 카카오 회원의 일반 로그인 시도 차단

* feat: 카카오 회원의 비밀번호 변경/확인 기능 차단

* feat: 관리자, 멤버 탈퇴 시 요청 차단(권한삭제/상태변경)

* feat: Admin, Member 수정 시 null check 추가

* feat: 회원 계정 탈퇴 시 액세스 토큰 만료 처리

* feat: 탈퇴한 회원 복구 기능 구현

* refactor: RedisConfig 위치 변경 (core-domain)

* refactor: RedisConfig 위치 변경 (core-infra)
  • Loading branch information
kimday0326 committed Jan 11, 2024
1 parent a0cddb6 commit 379c736
Show file tree
Hide file tree
Showing 27 changed files with 157 additions and 68 deletions.
4 changes: 2 additions & 2 deletions api/api-member/http/test.http
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@token = eyJhbGciOiJIUzI1NiJ9.eyJpZCI6Mywic3ViIjoiZGh4bDUwQG5hdmVyLmNvbSIsImlhdCI6MTcwNDYxNjU4NSwiZXhwIjoyMDY0NjE2NTg1LCJhdXRob3JpdHkiOiJST0xFX1VTRVIifQ.OIiQazdvmv-bsZp0VroiouW5gEqNAj7ROqRTOcLbWXQ
@token = eyJhbGciOiJIUzM4NCJ9.eyJpZCI6Mywic3ViIjoidGVzdEB0ZXN0LmNvbSIsImlhdCI6MTcwNDc5MjA5OCwiZXhwIjoxNzA0NzkzODk4LCJhdXRob3JpdHkiOiJST0xFX1VTRVIifQ.iPqLlu_2xIdZo-gN6PPW6xE9NB8ecWiinEcMyGsL39Q13rPz0FeqUWYJxpTKULD5
@adminToken = eyJhbGciOiJIUzI1NiJ9.eyJpZCI6Miwic3ViIjoiYWRtaW5AZXhhbXBsZS5jb20iLCJpYXQiOjE3MDQ2Mzc1NzgsImV4cCI6MjA2NDYzNzU3OCwiYXV0aG9yaXR5IjoiUk9MRV9BRE1JTiJ9.lAXg2NbcVeuSak6IW-AUNKC6zqd_0x_ER8RU3CMxNpk

## AUTH
Expand Down Expand Up @@ -142,7 +142,7 @@ http://localhost:8081/login

### 멤버 본인 정보 조회 (토큰 필요)
GET http://localhost:8081/api/v1/members/me
Authorization: Bearer {{token}}
Authorization: Bearer eyJhbGciOiJIUzM4NCJ9.eyJpZCI6Mywic3ViIjoidGVzdEB0ZXN0LmNvbSIsImlhdCI6MTcwNDc5Mjg3NSwiZXhwIjoxNzA0Nzk0Njc1LCJhdXRob3JpdHkiOiIifQ.pD0WG-Pp33x_di-lBmWxfoCH0PdTv35z3skVDSSgKo0kMp2Iie2GQwvk7XaW_I5u

### 멤버 본인 정보 수정 (토큰 필요)
PATCH http://localhost:8081/api/v1/members/me
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
@ComponentScan("com.pgms")
public class ApiMemberApplication {

public static void main(String[] args) {
SpringApplication.run(ApiMemberApplication.class, args);
}
public static void main(String[] args) {
SpringApplication.run(ApiMemberApplication.class, args);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import jakarta.validation.constraints.Size;

public record MemberInfoUpdateRequest(
@Size(min = 2, max = 50, message = "이름은 2자 이상 50자 이하로 입력해주세요.")
String name,

@Pattern(regexp = "\\d+", message = "전화번호는 숫자만 입력해주세요.")
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.pgms.apimember.dto.response.MemberSummaryGetResponse;
import com.pgms.apimember.exception.AdminException;
import com.pgms.coredomain.domain.member.Admin;
import com.pgms.coredomain.domain.member.redis.BlockedTokenRepository;
import com.pgms.coredomain.domain.member.repository.AdminRepository;
import com.pgms.coredomain.domain.member.repository.MemberRepository;

Expand All @@ -30,6 +31,7 @@ public class AdminService {

private final AdminRepository adminRepository;
private final MemberRepository memberRepository;
private final BlockedTokenRepository blockedTokenRepository;
private final PasswordEncoder passwordEncoder;

// 슈퍼 관리자 기능
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
import com.pgms.apimember.dto.request.RefreshTokenRequest;
import com.pgms.apimember.dto.response.AuthResponse;
import com.pgms.apimember.exception.CustomException;
import com.pgms.apimember.redis.RefreshToken;
import com.pgms.apimember.redis.RefreshTokenRepository;
import com.pgms.coredomain.domain.common.SecurityErrorCode;
import com.pgms.coredomain.domain.member.redis.RefreshToken;
import com.pgms.coredomain.domain.member.redis.RefreshTokenRepository;
import com.pgms.coresecurity.security.jwt.JwtTokenProvider;
import com.pgms.coresecurity.security.service.AdminUserDetailsService;
import com.pgms.coresecurity.security.service.MemberUserDetailsService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static com.pgms.coredomain.domain.common.MemberErrorCode.*;

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -13,6 +14,8 @@
import com.pgms.apimember.exception.MemberException;
import com.pgms.coredomain.domain.common.MemberErrorCode;
import com.pgms.coredomain.domain.member.Member;
import com.pgms.coredomain.domain.member.redis.BlockedToken;
import com.pgms.coredomain.domain.member.redis.BlockedTokenRepository;
import com.pgms.coredomain.domain.member.repository.MemberRepository;

import lombok.RequiredArgsConstructor;
Expand All @@ -23,6 +26,7 @@
public class MemberService {

private final MemberRepository memberRepository;
private final BlockedTokenRepository blockedTokenRepository;
private final PasswordEncoder passwordEncoder;

public Long signUp(MemberSignUpRequest requestDto) {
Expand All @@ -43,6 +47,7 @@ public MemberDetailGetResponse getMemberDetail(Long memberId) {
@Transactional(readOnly = true)
public void verifyPassword(Long memberId, String password) {
final Member member = getAvailableMember(memberId);
validateStandardMember(member);
validatePassword(password, member.getPassword());
}

Expand All @@ -61,6 +66,7 @@ public void updateMember(Long memberId, MemberInfoUpdateRequest requestDto) {

public void updatePassword(Long memberId, MemberPasswordUpdateRequest requestDto) {
final Member member = getAvailableMember(memberId);
validateStandardMember(member);
validatePassword(requestDto.originPassword(), member.getPassword());
validateNewPassword(requestDto.newPassword(), requestDto.newPasswordConfirm());
member.updatePassword(passwordEncoder.encode(requestDto.newPassword()));
Expand All @@ -69,6 +75,9 @@ public void updatePassword(Long memberId, MemberPasswordUpdateRequest requestDto
public void deleteMember(Long memberId) {
final Member member = getAvailableMember(memberId);
member.updateToDeleted();
blockedTokenRepository.save(
new BlockedToken(getCurrentAccessToken())
);
}

public Long restoreMember(Long memberId) {
Expand All @@ -90,8 +99,18 @@ private void validateNewPassword(String password, String passwordConfirm) {
}
}

private void validateStandardMember(Member member) {
if (member.isLoginByProvider()) {
throw new MemberException(NOT_ALLOWED_BY_PROVIDER);
}
}

private Member getAvailableMember(Long memberId) {
return memberRepository.findByIdAndIsDeletedFalse(memberId)
.orElseThrow(() -> new MemberException(MEMBER_NOT_FOUND));
}

private String getCurrentAccessToken() {
return (String)SecurityContextHolder.getContext().getAuthentication().getCredentials();
}
}
2 changes: 1 addition & 1 deletion api/api-member/src/main/resources/templates/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
})
.then(response => response.json())
.then(data => {
alert("로그인 성공. 토큰은 콘솔창에서 확인하세요.")
alert(JSON.stringify(data, null, 2))
console.log('Success:', data);
})
.catch((error) => {
Expand Down
1 change: 1 addition & 0 deletions core/core-domain/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ jar { enabled = true }
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// queryDsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;


@Configuration
@EnableTransactionManagement
@EntityScan("com.pgms.coredomain")
@EnableJpaRepositories("com.pgms.coredomain")
@EnableRedisRepositories("com.pgms.coredomain")
@EnableJpaAuditing
public class CoreDomainConfig {
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ public enum MemberErrorCode implements BaseErrorCode {
// ADMIN
ADMIN_NOT_FOUND("NOT FOUND", HttpStatus.NOT_FOUND, "존재하지 않는 관리자입니다."),
DUPLICATED_ADMIN_EMAIL("DUPLICATED ADMIN EMAIL", HttpStatus.BAD_REQUEST, "이미 존재하는 관리자 이메일입니다."),
NOT_ACTIVE_ADMIN("NOT ACTIVE ADMIN", HttpStatus.BAD_REQUEST, "활성화되지 않은 관리자입니다. 슈퍼 관리자에게 문의해주세요."),

// MEMBER
MEMBER_NOT_FOUND("NOT FOUND", HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."),
DUPLICATED_MEMBER_EMAIL("DUPLICATED MEMBER EMAIL", HttpStatus.BAD_REQUEST, "이미 존재하는 회원 이메일입니다."),
MEMBER_ALREADY_DELETED("MEMBER ALREADY DELETED", HttpStatus.BAD_REQUEST, "이미 탈퇴한 회원입니다."),
PASSWORD_NOT_MATCHED("PASSWORD NOT MATCHED", HttpStatus.BAD_REQUEST, "비밀번호가 일치하지 않습니다."),
PASSWORD_CONFIRM_NOT_MATCHED("PASSWORD CONFIRM NOT MATCHED", HttpStatus.BAD_REQUEST, "비밀번호 확인이 일치하지 않습니다.");
PASSWORD_CONFIRM_NOT_MATCHED("PASSWORD CONFIRM NOT MATCHED", HttpStatus.BAD_REQUEST, "비밀번호 확인이 일치하지 않습니다."),
NOT_ALLOWED_BY_PROVIDER("NOT ALLOWED BY PROVIDER", HttpStatus.BAD_REQUEST, "소셜 로그인 회원은 불가능한 요청입니다.");

private final String errorCode;
private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,29 @@ public Admin(String name, String password, String phoneNumber, String email, Rol
}

public void update(String name, String password, String phoneNumber, AccountStatus status, Role role) {
this.name = name;
this.password = password;
this.phoneNumber = phoneNumber;
this.status = status;
this.role = role;
this.name = name != null ? name : this.name;
this.password = password != null ? password : this.password;
this.phoneNumber = phoneNumber != null ? phoneNumber : this.phoneNumber;
this.status = status != null ? status : this.status;
this.role = role != null ? role : this.role;
super.updateLastPasswordUpdatedAt();
}

public boolean isDeleted() {
return this.status == AccountStatus.DELETED;
}

public boolean isNotActive() {
return this.status != AccountStatus.ACTIVE;
}

public void updateToDeleted() {
this.role = null;
this.status = AccountStatus.DELETED;
}

public void updateToLocked() {
this.status = AccountStatus.LOCKED;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -105,24 +105,30 @@ public void updateMemberInfo(
String streetAddress,
String detailAddress,
String zipCode) {
this.name = name;
this.phoneNumber = phoneNumber;
this.birthDate = birthDate;
this.gender = gender;
this.streetAddress = streetAddress;
this.detailAddress = detailAddress;
this.zipCode = zipCode;
this.name = name != null ? name : this.name;
this.phoneNumber = phoneNumber != null ? phoneNumber : this.phoneNumber;
this.birthDate = birthDate != null ? birthDate : this.birthDate;
this.gender = gender != null ? gender : this.gender;
this.streetAddress = streetAddress != null ? streetAddress : this.streetAddress;
this.detailAddress = detailAddress != null ? detailAddress : this.detailAddress;
this.zipCode = zipCode != null ? zipCode : this.zipCode;
}

public boolean isDeleted() {
return this.status == DELETED;
}

public boolean isLoginByProvider() {
return this.provider != null;
}

public void updateToDeleted() {
this.role = null;
this.status = DELETED;
}

public void updateToActive() {
this.role = Role.ROLE_USER;
this.status = ACTIVE;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package com.pgms.coredomain.domain.member.enums;

public enum AccountStatus {
ACTIVE, INACTIVE, LOCKED, DELETED;
ACTIVE, LOCKED, DELETED;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.pgms.coredomain.domain.member.redis;

import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
@RedisHash(value = "blockedToken", timeToLive = 30 * 60) // 30분(액세스 토큰 유효시간)
public class BlockedToken {
@Id
private String token;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.pgms.coredomain.domain.member.redis;

import org.springframework.data.repository.CrudRepository;

public interface BlockedTokenRepository extends CrudRepository<BlockedToken, String> {
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.pgms.apimember.redis;
package com.pgms.coredomain.domain.member.redis;

import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.pgms.apimember.redis;
package com.pgms.coredomain.domain.member.redis;

import org.springframework.data.repository.CrudRepository;

Expand Down
1 change: 1 addition & 0 deletions core/core-infra/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ dependencies {
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// queryDsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
package com.pgms.apibooking.config;
package com.pgms.coreinfra.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Configuration
public class RedisConfig {

@Bean
LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory();
}

@Bean
RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@

import com.pgms.coresecurity.security.resolver.CurrentAccountArgumentResolver;

import lombok.RequiredArgsConstructor;

@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

private final CurrentAccountArgumentResolver currentAccountArgumentResolver;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new CurrentAccountArgumentResolver());
resolvers.add(currentAccountArgumentResolver);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ private RequestMatcher[] requestPermitAll() {
List<RequestMatcher> requestMatchers = List.of(
antMatcher("/login"),
antMatcher("/api/v1/auth/**"),
antMatcher("/api/v1/members/signup"));
antMatcher("/api/v1/members/signup"),
antMatcher("/api/v1/members/restore"));
return requestMatchers.toArray(RequestMatcher[]::new);
}

Expand Down
Loading

0 comments on commit 379c736

Please sign in to comment.