Skip to content

Commit

Permalink
Merge pull request #69 from kssumin/feat/#184/logout
Browse files Browse the repository at this point in the history
[BE/FEAT] 로그아웃 및 회원 탈퇴 구현
  • Loading branch information
kssumin authored Mar 11, 2024
2 parents b82b403 + 02d7e3c commit 320b819
Show file tree
Hide file tree
Showing 23 changed files with 283 additions and 90 deletions.
Empty file removed BE/BE.md
Empty file.
25 changes: 9 additions & 16 deletions BE/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,16 @@
| 운영 환경 구축 | EC2, Docker |
| Build | Gradle |
| CI/CD | Github Actions |
| Library | Spring Open Feign, OAuth 2.0, Actuator |
| Library | Spring Open Feign, OAuth 2.0, spring actuator |

## 실행방법
1. git clone
1. git clone [repository url)
2. 환경변수를 설정해줍니다.(env.properties)
3. docker-compose를 통해 db를 실행합니다.
4. spring boot를 실행합니다.


## docs
[서브 도메인 적용하기](https://velog.io/@kssumin/%EC%84%9C%EB%B8%8C-%EB%8F%84%EB%A9%94%EC%9D%B8-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0)

[타임존 설정](https://velog.io/@kssumin/DB%EC%97%90-%EB%93%A4%EC%96%B4%EA%B0%80%EB%8A%94-%EC%8B%9C%EA%B0%84%EC%9D%B4-%EC%9D%B4%EC%83%81%ED%95%98%EB%8B%A4)

[분기 처리 코드 리팩터링하기](https://velog.io/@kssumin/%EB%B6%84%EA%B8%B0%EC%B2%98%EB%A6%AC-%EC%BD%94%EB%93%9C-%EB%A6%AC%ED%8C%A9%ED%84%B0%EB%A7%81-%ED%95%98%EA%B8%B0)

[슬랙 API 호출 코드를 추상화하기](https://velog.io/@kssumin/%EC%8A%AC%EB%9E%99-API-%ED%98%B8%EC%B6%9C-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%B6%94%EC%83%81%ED%99%94%ED%95%98%EA%B8%B0)

[200 status code만 사용하지 말자](https://velog.io/@kssumin/EEOS-%EC%BA%90%EC%8B%B1)
3. db를 실행합니다.
```shell
cd resources
cd local-develop-environment
docker-compose up
```
5. spring boot를 실행합니다.

Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package com.blackcompany.eeos.auth.application.domain.token;

import com.blackcompany.eeos.auth.application.exception.NotFoundCookieException;
import com.blackcompany.eeos.auth.application.exception.NotFoundHeaderTokenException;
import com.blackcompany.eeos.auth.application.exception.RtTokenExpiredException;
import com.blackcompany.eeos.auth.application.exception.TokenExpiredException;
import com.blackcompany.eeos.auth.application.exception.TokenParsingException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
Expand All @@ -27,61 +25,57 @@ public class TokenResolver {
public TokenResolver(
@Value("${security.jwt.access.secretKey}") String accessSecretKey,
@Value("${security.jwt.refresh.secretKey}") String refreshSecretKey) {
this.accessSecretKey = Keys.hmacShaKeyFor(accessSecretKey.getBytes(StandardCharsets.UTF_8));
this.refreshSecretKey = Keys.hmacShaKeyFor(refreshSecretKey.getBytes(StandardCharsets.UTF_8));
this.accessSecretKey = generateSecretKey(accessSecretKey);
this.refreshSecretKey = generateSecretKey(refreshSecretKey);
}

public Long getExpiredDateByHeader(final String token) {
Date expiration = getAccessClaims(token).getExpiration();

return expiration.getTime();
public Long getExpiredDateByAccessToken(final String token) {
return getExpirationTime(getAccessClaims(token));
}

public Long getUserInfoByCookie(final String token) {
return getRefreshClaims(token).get(MEMBER_ID_CLAIM_KEY, Long.class);
public Long getExpiredDateByRefreshToken(final String token) {
return getExpirationTime(getRefreshClaims(token));
}

public Long getExpiredDateByCookie(final String token) {
Date expiration = getRefreshClaims(token).getExpiration();
public Long getUserDataByAccessToken(final String token) {
return getClaimValue(getAccessClaims(token), MEMBER_ID_CLAIM_KEY);
}

return expiration.getTime();
public Long getUserDataByRefreshToken(final String token) {
return getClaimValue(getRefreshClaims(token), MEMBER_ID_CLAIM_KEY);
}

public Long getUserInfoByHeader(final String token) {
return getAccessClaims(token).get(MEMBER_ID_CLAIM_KEY, Long.class);
private SecretKey generateSecretKey(String key) {
return Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8));
}

private Claims getAccessClaims(final String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(accessSecretKey)
.build()
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
throw new TokenExpiredException();
} catch (SignatureException e) {
log.error("jwt 파싱에 에러가 발생했습니다.");
throw new NotFoundHeaderTokenException();
} catch (IllegalStateException e) {
throw new NotFoundHeaderTokenException();
}
return parseClaims(token, accessSecretKey);
}

private Claims getRefreshClaims(final String token) {
return parseClaims(token, refreshSecretKey);
}

private Claims parseClaims(final String token, final SecretKey secretKey) {
try {
return Jwts.parserBuilder()
.setSigningKey(refreshSecretKey)
.build()
.parseClaimsJws(token)
.getBody();
return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody();
} catch (ExpiredJwtException e) {
throw new RtTokenExpiredException();
throw new TokenExpiredException();
} catch (SignatureException e) {
log.error("jwt 파싱에 에러가 발생했습니다.");
throw new NotFoundCookieException();
} catch (IllegalStateException e) {
throw new NotFoundCookieException();
throw new TokenParsingException(e);
} catch (Exception e) {
log.error("JWT 파싱 중 오류 발생: {}", e.getMessage(), e);
throw new TokenParsingException(e);
}
}

private Long getExpirationTime(Claims claims) {
Date expiration = claims.getExpiration();
return expiration.getTime();
}

private Long getClaimValue(Claims claims, String claimKey) {
return claims.get(claimKey, Long.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.blackcompany.eeos.auth.application.event;

import lombok.Getter;

@Getter
public class DeletedMemberEvent {
private final Long memberId;
private final String token;

private DeletedMemberEvent(Long memberId, String token) {
this.memberId = memberId;
this.token = token;
}

/**
* 멤버 탈퇴 이벤트를 발행한다.
*
* @param memberId 회원 탈퇴를 원하는 멤버 id
* @param token 회원 탈퇴 시 사용한 토큰
* @return
*/
public static DeletedMemberEvent of(Long memberId, String token) {
return new DeletedMemberEvent(memberId, token);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.blackcompany.eeos.auth.application.event;

import com.blackcompany.eeos.auth.application.domain.token.TokenResolver;
import com.blackcompany.eeos.auth.persistence.BlackAuthenticationRepository;
import com.blackcompany.eeos.target.persistence.AttendRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
@RequiredArgsConstructor
@Slf4j
public class DeletedMemberEventListener {
private final AttendRepository attendRepository;
private final BlackAuthenticationRepository blackAuthenticationRepository;
private final TokenResolver tokenResolver;

@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleDeletedProgram(DeletedMemberEvent event) {
deleteTargetData(event.getMemberId());
saveUsedToken(event.getToken(), event.getMemberId());
}

private void deleteTargetData(Long memberId) {
attendRepository.deleteAllByMemberId(memberId);
}

private void saveUsedToken(String token, Long memberId) {
blackAuthenticationRepository.save(token, memberId, getExpiredDate(token));
}

private Long getExpiredDate(String token) {
return tokenResolver.getExpiredDateByRefreshToken(token);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.blackcompany.eeos.auth.application.exception;

import com.blackcompany.eeos.common.exception.BusinessException;
import org.springframework.http.HttpStatus;

/** JWT 파싱 중 발생한 예외 */
public class TokenParsingException extends BusinessException {
private static final String FAIL_CODE = "4007";
private String message;

public TokenParsingException(Exception e) {
super(FAIL_CODE, HttpStatus.BAD_REQUEST);
message = e.getMessage();
}

@Override
public String getMessage() {
return String.format("JWT 파싱 중 오류 발생: {}", message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.blackcompany.eeos.auth.application.service;

import com.blackcompany.eeos.auth.application.domain.token.TokenResolver;
import com.blackcompany.eeos.auth.application.event.DeletedMemberEvent;
import com.blackcompany.eeos.auth.application.usecase.LogOutUsecase;
import com.blackcompany.eeos.auth.application.usecase.WithDrawUsecase;
import com.blackcompany.eeos.auth.persistence.BlackAuthenticationRepository;
import com.blackcompany.eeos.auth.persistence.OAuthMemberRepository;
import com.blackcompany.eeos.member.persistence.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class DeactivateMemberService implements LogOutUsecase, WithDrawUsecase {
private final BlackAuthenticationRepository blackAuthenticationRepository;
private final TokenResolver tokenResolver;
private final ApplicationEventPublisher eventPublisher;
private final MemberRepository memberRepository;
private final OAuthMemberRepository oAuthMemberRepository;

@Override
@Transactional
public void logOut(final String token, final Long memberId) {
saveUsedToken(token, memberId);
}

@Override
@Transactional
public void withDraw(final String token, final Long memberId) {
if (isNotMember(memberId)) {
return;
}
deleteMemberData(memberId);
eventPublisher.publishEvent(DeletedMemberEvent.of(memberId, token));
}

private void saveUsedToken(final String token, final Long memberId) {
blackAuthenticationRepository.save(token, memberId, getExpiredToken(token));
}

private Long getExpiredToken(final String token) {
return tokenResolver.getExpiredDateByRefreshToken(token);
}

private boolean isNotMember(Long memberId) {
return memberRepository.findById(memberId).isEmpty();
}

private void deleteMemberData(Long memberId) {
memberRepository.deleteById(memberId);
oAuthMemberRepository.findByMemberId(memberId).ifPresent(oAuthMemberRepository::delete);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import com.blackcompany.eeos.auth.application.exception.InvalidTokenException;
import com.blackcompany.eeos.auth.application.support.AuthenticationTokenGenerator;
import com.blackcompany.eeos.auth.application.usecase.ReissueUsecase;
import com.blackcompany.eeos.auth.persistence.MemberAuthenticationRepository;
import com.blackcompany.eeos.auth.persistence.BlackAuthenticationRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -15,13 +15,13 @@
@Transactional(readOnly = true)
public class ReissueService implements ReissueUsecase {
private final AuthenticationTokenGenerator authenticationTokenGenerator;
private final MemberAuthenticationRepository memberAuthenticationRepository;
private final BlackAuthenticationRepository blackAuthenticationRepository;
private final TokenResolver tokenResolver;

@Transactional
@Override
public TokenModel execute(final String token) {
Long memberId = tokenResolver.getUserInfoByCookie(token);
Long memberId = tokenResolver.getUserDataByRefreshToken(token);

validateToken(token);
saveUsedToken(token, memberId);
Expand All @@ -30,18 +30,18 @@ public TokenModel execute(final String token) {
}

private void validateToken(final String token) {
boolean isExistToken = memberAuthenticationRepository.isExistToken(token);
boolean isExistToken = blackAuthenticationRepository.isExistToken(token);

if (isExistToken) {
throw new InvalidTokenException();
}
}

private void saveUsedToken(final String token, final Long memberId) {
memberAuthenticationRepository.save(token, memberId, getExpiredToken(token));
blackAuthenticationRepository.save(token, memberId, getExpiredToken(token));
}

private Long getExpiredToken(final String token) {
return tokenResolver.getExpiredDateByHeader(token);
return tokenResolver.getExpiredDateByRefreshToken(token);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ public TokenModel execute(final Long memberId) {

return tokenModelConverter.from(
accessToken,
tokenResolver.getExpiredDateByHeader(accessToken),
tokenResolver.getExpiredDateByAccessToken(accessToken),
refreshToken,
tokenResolver.getExpiredDateByCookie(refreshToken));
tokenResolver.getExpiredDateByRefreshToken(refreshToken));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.blackcompany.eeos.auth.application.usecase;

public interface LogOutUsecase {
void logOut(String token, final Long memberId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.blackcompany.eeos.auth.application.usecase;

public interface WithDrawUsecase {
void withDraw(String token, Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class MemberAuthenticationEntity {
public class BlackAuthenticationEntity {
@Id private String token;
private Long memberId;
@TimeToLive private Long expiration;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@

@Repository
@RequiredArgsConstructor
public class MemberAuthenticationRepository {
public class BlackAuthenticationRepository {

private final RedisTemplate<String, Object> redisTemplate;

public void save(String key, Long value, Long expiredTime) {
redisTemplate.opsForValue().set(key, value, expiredTime, TimeUnit.MILLISECONDS);
public void save(String token, Long memberId, Long expiration) {
redisTemplate.opsForValue().set(token, memberId, expiration, TimeUnit.MILLISECONDS);
}

public boolean isExistToken(String key) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@
public interface OAuthMemberRepository extends JpaRepository<OAuthMemberEntity, Long> {
@Query("SELECT o FROM OAuthMemberEntity o WHERE o.oauthId=:oauthId")
Optional<OAuthMemberEntity> findByOauthId(@Param("oauthId") String oauthId);

@Query("SELECT o FROM OAuthMemberEntity o WHERE o.memberId=:memberId")
Optional<OAuthMemberEntity> findByMemberId(@Param("memberId") Long memberId);
}
Loading

0 comments on commit 320b819

Please sign in to comment.