Skip to content
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

[BE] Feature/#388 refresh token 및 로그아웃 기능 구현 #411

Merged
merged 20 commits into from
Sep 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
51540a9
chore: redis 의존성 추가
junpakPark Sep 12, 2023
b6a1004
refactor: OauthService 필드에 final 추가
junpakPark Sep 12, 2023
e0c399f
feat: refreshToken 엔티티 및 레포지토리 구현
junpakPark Sep 14, 2023
be4e868
feat: JwtTokenProvider RefreshToken 발급 구현
junpakPark Sep 14, 2023
91cb549
feat: 로그인 시 RefreshToken 발급 기능 구현
junpakPark Sep 14, 2023
f083911
feat: Auth 패키지 커스텀 예외 추가
junpakPark Sep 14, 2023
ed4834b
refactor: validate 메서드 리팩터링
junpakPark Sep 14, 2023
f74600a
chore: refreshToken 만료 시간 추가
junpakPark Sep 14, 2023
50da580
test: Test를 위한 설정 변경
junpakPark Sep 14, 2023
d91e368
feat: 액세스 토큰 재발급 및 로그아웃 기능 구현
junpakPark Sep 14, 2023
2ea1e6c
chore: Redis 의존성 제거
junpakPark Sep 14, 2023
80db45f
test: TestTokenProvider 객체 구현
junpakPark Sep 15, 2023
edabfcd
refactor: /logout HttpMethod 변경, cookie 관련 cors설정 및 maxAge 설정,
junpakPark Sep 15, 2023
d11c397
test: DisplayName 추가
junpakPark Sep 15, 2023
4fc08de
feat: RTR 적용 및 OauthConntroller 제거, OauthService 및 TokenService 역할과 책…
junpakPark Sep 15, 2023
580b6b1
chore : merge Conflict 해결
junpakPark Sep 15, 2023
4236bd9
refactor : 피드백 반영
junpakPark Sep 15, 2023
5a269ef
refactor : 매직넘버 상수화
junpakPark Sep 15, 2023
400309a
refactor : 네이밍 수정
junpakPark Sep 15, 2023
2697b7e
feat: 쿠키 설정 추가
junpakPark Sep 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions backend/src/docs/asciidoc/auth.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
== 소셜 로그인

=== KAKAO 로그인 URL 반환

operation::login-controller-test/redirection[snippets='http-request,http-response']

=== KAKAO 로그인

operation::login-controller-test/login[snippets='http-request,http-response']

=== 로그아웃

operation::login-controller-test/logout[snippets='http-request,http-response']
2 changes: 1 addition & 1 deletion backend/src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ include::pin.adoc[]
include::atlas.adoc[]
include::member.adoc[]
include::permission.adoc[]
include::oauth.adoc[]
include::auth.adoc[]
include::bookmark.adoc[]
include::admin.adoc[]
9 changes: 0 additions & 9 deletions backend/src/docs/asciidoc/oauth.adoc

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.mapbefine.mapbefine.auth.application;

import static com.mapbefine.mapbefine.auth.exception.AuthException.AuthUnauthorizedException;

import com.mapbefine.mapbefine.auth.domain.AuthMember;
import com.mapbefine.mapbefine.auth.domain.member.Admin;
import com.mapbefine.mapbefine.auth.domain.member.User;
import com.mapbefine.mapbefine.auth.exception.AuthErrorCode;
import com.mapbefine.mapbefine.member.domain.Member;
import com.mapbefine.mapbefine.member.domain.MemberRepository;
import com.mapbefine.mapbefine.topic.domain.Topic;
Expand All @@ -22,11 +25,10 @@ public AuthService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}

public boolean isMember(Long memberId) {
if (Objects.isNull(memberId)) {
return false;
public void validateMember(Long memberId) {
if (Objects.isNull(memberId) || !memberRepository.existsById(memberId)) {
throw new AuthUnauthorizedException(AuthErrorCode.ILLEGAL_MEMBER_ID);
}
return memberRepository.existsById(memberId);
}

public AuthMember findAuthMemberByMemberId(Long memberId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.mapbefine.mapbefine.auth.application;

import static com.mapbefine.mapbefine.auth.exception.AuthErrorCode.ILLEGAL_TOKEN;

import com.mapbefine.mapbefine.auth.domain.token.RefreshToken;
import com.mapbefine.mapbefine.auth.domain.token.RefreshTokenRepository;
import com.mapbefine.mapbefine.auth.dto.LoginTokens;
import com.mapbefine.mapbefine.auth.exception.AuthException.AuthUnauthorizedException;
import com.mapbefine.mapbefine.auth.infrastructure.TokenProvider;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class TokenService {

private final TokenProvider tokenProvider;
private final RefreshTokenRepository refreshTokenRepository;

public TokenService(TokenProvider tokenProvider, RefreshTokenRepository refreshTokenRepository) {
this.tokenProvider = tokenProvider;
this.refreshTokenRepository = refreshTokenRepository;
}

public LoginTokens issueTokens(Long memberId) {
String accessToken = tokenProvider.createAccessToken(String.valueOf(memberId));
String refreshToken = tokenProvider.createRefreshToken();

refreshTokenRepository.findByMemberId(memberId)
.ifPresent(refreshTokenRepository::delete);

refreshTokenRepository.save(new RefreshToken(refreshToken, memberId));
junpakPark marked this conversation as resolved.
Show resolved Hide resolved

return new LoginTokens(accessToken, refreshToken);
}

public LoginTokens reissueToken(String refreshToken, String accessToken) {
tokenProvider.validateTokensForReissue(refreshToken, accessToken);
Long memberId = findMemberIdByRefreshToken(refreshToken);

return issueTokens(memberId);
}
Comment on lines +37 to +42
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issueTokens() 랑 위치가 바뀌었으면 좋겠네요!

Copy link
Collaborator Author

@junpakPark junpakPark Sep 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issueTokens()도 퍼블릭 메서드이고, LoginController에서 가장 먼저 사용하기 때문에 현재처럼 배치했습니당ㅋㅋ

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

흠 그렇군요.

Token Service 만 봤을 때에는 reissueToken() 이 issueTokens() 메서드를 사용해서 헷갈렸던 것 같아요~

위와 같은 이유라면 현재 배치가 더 나을 것 같네용


private Long findMemberIdByRefreshToken(String token) {
RefreshToken refreshToken = refreshTokenRepository.findById(token)
.orElseThrow(() -> new AuthUnauthorizedException(ILLEGAL_TOKEN));

return refreshToken.getMemberId();
}

public void removeRefreshToken(String refreshToken, String accessToken) {
tokenProvider.validateTokensForRemoval(refreshToken, accessToken);

String payload = tokenProvider.getPayload(accessToken);
Long memberId = Long.valueOf(payload);

refreshTokenRepository.deleteByMemberId(memberId);
}



}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.mapbefine.mapbefine.auth.domain.token;

import static lombok.AccessLevel.PROTECTED;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor(access = PROTECTED)
@Getter
public class RefreshToken {

@Id
private String token;

@Column(nullable = false, unique = true)
private Long memberId;

public RefreshToken(String token, Long memberId) {
this.token = token;
this.memberId = memberId;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.mapbefine.mapbefine.auth.domain.token;

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface RefreshTokenRepository extends JpaRepository<RefreshToken, String> {

Optional<RefreshToken> findByMemberId(Long memberId);

void deleteByMemberId(Long memberId);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.mapbefine.mapbefine.auth.dto;

public record AccessToken(
String accessToken
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.mapbefine.mapbefine.auth.dto;

public record LoginTokens(
String accessToken,
String refreshToken
) {

public AccessToken toAccessToken() {
return new AccessToken(accessToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.mapbefine.mapbefine.auth.dto.response;

import com.mapbefine.mapbefine.member.dto.response.MemberDetailResponse;

public record LoginInfoResponse(
String accessToken,
MemberDetailResponse member
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

@Getter
public enum AuthErrorCode {
ILLEGAL_MEMBER_ID("03100", "로그인에 실패하였습니다."),
ILLEGAL_TOKEN("03101", "로그인에 실패하였습니다."),
FORBIDDEN_ADMIN_ACCESS("03102", "로그인에 실패하였습니다."),
BLOCKING_MEMBER_ACCESS("03103", "로그인에 실패하였습니다."),
ILLEGAL_MEMBER_ID("01100", "로그인에 실패하였습니다."),
ILLEGAL_TOKEN("01101", "로그인에 실패하였습니다."),
FORBIDDEN_ADMIN_ACCESS("01102", "로그인에 실패하였습니다."),
BLOCKING_MEMBER_ACCESS("01103", "로그인에 실패하였습니다."),
EXPIRED_TOKEN("01104", "기간이 만료된 토큰입니다.")
;

private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package com.mapbefine.mapbefine.auth.infrastructure;

import static com.mapbefine.mapbefine.auth.exception.AuthErrorCode.EXPIRED_TOKEN;
import static com.mapbefine.mapbefine.auth.exception.AuthErrorCode.ILLEGAL_TOKEN;

import com.mapbefine.mapbefine.auth.exception.AuthException.AuthUnauthorizedException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
Expand All @@ -10,13 +15,36 @@
import org.springframework.stereotype.Component;

@Component
public class JwtTokenProvider {
@Value("${security.jwt.token.secret-key}")
private String secretKey;
@Value("${security.jwt.token.expire-length}")
private long validityInMilliseconds;
public class JwtTokenProvider implements TokenProvider {

private static final String EMPTY = "";

private final String secretKey;
private final long accessExpirationTime;
private final long refreshExpirationTime;

public JwtTokenProvider(
@Value("${security.jwt.token.secret-key}")
String secretKey,
@Value("${security.jwt.token.access-expire-length}")
long accessExpirationTime,
@Value("${security.jwt.token.refresh-expire-length}")
long refreshExpirationTime
) {
this.secretKey = secretKey;
this.accessExpirationTime = accessExpirationTime;
this.refreshExpirationTime = refreshExpirationTime;
}

public String createToken(String payload) {
public String createAccessToken(String payload) {
return createToken(payload, accessExpirationTime);
}

public String createRefreshToken() {
return createToken(EMPTY, refreshExpirationTime);
}

private String createToken(String payload, Long validityInMilliseconds) {
Claims claims = Jwts.claims().setSubject(payload);
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
Expand All @@ -33,14 +61,40 @@ public String getPayload(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
junpakPark marked this conversation as resolved.
Show resolved Hide resolved
}

public boolean validateToken(String token) {
public void validateTokensForReissue(String refreshToken, String accessToken) {
boolean canReissueAccessToken = !isExpired(refreshToken) && isExpired(accessToken);
if (canReissueAccessToken) {
return;
}
throw new AuthUnauthorizedException(ILLEGAL_TOKEN);
}

public void validateTokensForRemoval(String refreshToken, String accessToken) {
boolean canRemoveRefreshToken = !isExpired(refreshToken) && !isExpired(accessToken);
if (canRemoveRefreshToken) {
return;
}
throw new AuthUnauthorizedException(EXPIRED_TOKEN);
}

public void validateAccessToken(String accessToken) {
if (isExpired(accessToken)) {
throw new AuthUnauthorizedException(EXPIRED_TOKEN);
}
}

private boolean isExpired(String token) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
Date expiration = claims.getBody().getExpiration();

return !claims.getBody().getExpiration().before(new Date());
return expiration.before(new Date());
} catch (ExpiredJwtException e) {
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
throw new AuthUnauthorizedException(ILLEGAL_TOKEN);
}
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.mapbefine.mapbefine.auth.infrastructure;

public interface TokenProvider {
junpakPark marked this conversation as resolved.
Show resolved Hide resolved

String createAccessToken(String payload);

String createRefreshToken();

String getPayload(String token);

void validateTokensForReissue(String refreshToken, String accessToken);

void validateTokensForRemoval(String refreshToken, String accessToken);

void validateAccessToken(String accessToken);
}
Loading