Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d5472c8
[FEAT] AES 암/복호화 유틸 클래스 추가 (#68)
kmw2378 Jan 22, 2025
cc0968a
[FEAT] 암호화 비밀키 오류 시 발생시킬 예외 클래스 추가 (#68)
kmw2378 Jan 22, 2025
727877f
[REFACTOR] 소셜 로그인 응답값, 회원 가입/로그인 요청값에 암호화된 socialAccountId 를 사용하도록 수정…
kmw2378 Jan 22, 2025
633f04f
[REFACTOR] 암/복호화할 데이터 검증 로직 수정 (#68)
kmw2378 Jan 22, 2025
b94f3bc
[REFACTOR] 암호화 알고리즘을 의미하는 상수 네이밍 수정 (#68)
kmw2378 Jan 22, 2025
46e9968
[REFACTOR] 소셜 ID 관련 예외 클래스 메시지에 소셜 ID를 명시하지 않도록 수정 (#68)
kmw2378 Jan 22, 2025
5bbd283
[FEAT] InMemoryTokenProviderRepository 추가 (#68)
kmw2378 Jan 27, 2025
607bdf0
[FEAT] TokenAdapter 추가 (#68)
kmw2378 Jan 27, 2025
41ed986
[FEAT] TokenConfig 추가 (#68)
kmw2378 Jan 27, 2025
dbdad11
[FEAT] TokenProperties 추가 (#68)
kmw2378 Jan 27, 2025
a90e5f4
[FEAT] TokenProvider 추가 (#68)
kmw2378 Jan 27, 2025
8b4ef07
[FEAT] TokenType 추가 (#68)
kmw2378 Jan 27, 2025
cae3390
[FEAT] InvalidSocialAccountTokenException 추가 (#68)
kmw2378 Jan 27, 2025
1aa8cdd
[REFACTOR] InvalidEncryptionSecretKeyException 삭제 (#68)
kmw2378 Jan 27, 2025
3f08fe6
[REFACTOR] AESCryptography 삭제 (#68)
kmw2378 Jan 27, 2025
3d61be2
[REFACTOR] TokenProvider 클래스명 변경, 기능 수정 (#68)
kmw2378 Jan 27, 2025
5702049
[REFACTOR] AuthenticationInterceptor 수정 (#68)
kmw2378 Jan 27, 2025
f36bc3d
[REFACTOR] 소셜 로그인, 회원가입/로그인에 사용되는 DTO 필드명 수정 (#68)
kmw2378 Jan 27, 2025
4797361
[REFACTOR] 소셜 로그인, 회원가입/로그인 시 토큰 생성 로직 수정 (#68)
kmw2378 Jan 27, 2025
76d53f7
[TEST] AuthService, OAuthService 테스트 코드 수정 (#68)
kmw2378 Jan 27, 2025
3a0537d
Merge branch 'develop' of https://github.com/JECT-Study/Componote-BE …
kmw2378 Feb 9, 2025
75279e1
[REFACTOR] 소셜 토큰 Subject 추출 시 검증 로직 추가 (#68)
kmw2378 Feb 9, 2025
b2c3a8c
[REFACTOR] attribute-key 경로 변경 (#68)
kmw2378 Feb 9, 2025
75f8541
Merge pull request #18 from JECT-Study/J01-68-BE-소셜-로그인-응답값-암호화
kmw2378 Feb 9, 2025
88a0e16
Merge branch 'main' into develop
kmw2378 Feb 9, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import ject.componote.domain.auth.error.NotFoundSocialAccountException;
import ject.componote.domain.auth.model.AuthPrincipal;
import ject.componote.domain.auth.model.Nickname;
import ject.componote.domain.auth.util.TokenProvider;
import ject.componote.domain.auth.token.application.TokenService;
import ject.componote.infra.storage.application.StorageService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand All @@ -27,30 +27,29 @@ public class AuthService {
private final StorageService storageService;
private final MemberRepository memberRepository;
private final SocialAccountRepository socialAccountRepository;
private final TokenProvider tokenProvider;
private final TokenService tokenService;

@Transactional
public MemberSignupResponse signup(final MemberSignupRequest request) {
final Long socialAccountId = request.socialAccountId();
final Long socialAccountId = getSocialAccountId(request.socialAccountToken());
if (!socialAccountRepository.existsById(socialAccountId)) {
throw new NotFoundSocialAccountException(socialAccountId);
throw new NotFoundSocialAccountException();
}

if (memberRepository.existsBySocialAccountId(socialAccountId)) {
throw new DuplicatedSignupException(socialAccountId);
throw new DuplicatedSignupException();
}

final Member member = memberRepository.save(request.toMember());
final String accessToken = tokenProvider.createToken(AuthPrincipal.from(member));
// storageService.moveImage(member.getProfileImage());
final Member member = memberRepository.save(request.toMember(socialAccountId));
storageService.moveImage(member.getProfileImage());
final String accessToken = createAccessToken(member);
return MemberSignupResponse.of(accessToken, member);
}

// socialAccountId 만 가지고 로그인을 하는건 위험하지 않을까? 별도 암호화가 있으면 좋을 것 같음
public MemberLoginResponse login(final MemberLoginRequest memberLoginRequest) {
final Long socialAccountId = memberLoginRequest.socialAccountId();
public MemberLoginResponse login(final MemberLoginRequest request) {
final Long socialAccountId = getSocialAccountId(request.socialAccountToken());
final Member member = findMemberBySocialAccountId(socialAccountId);
final String accessToken = tokenProvider.createToken(AuthPrincipal.from(member));
final String accessToken = createAccessToken(member);
return MemberLoginResponse.of(accessToken, member);
}

Expand All @@ -63,7 +62,15 @@ public void validateNickname(final MemberNicknameValidateRequest request) {

private Member findMemberBySocialAccountId(final Long socialAccountId) {
return memberRepository.findBySocialAccountId(socialAccountId)
.orElseThrow(() -> NotFoundMemberException.createWhenInvalidSocialAccountId(socialAccountId));
.orElseThrow(NotFoundMemberException::createWhenInvalidSocialAccountId);
}

private String createAccessToken(final Member member) {
return tokenService.createAccessToken(AuthPrincipal.from(member));
}

private Long getSocialAccountId(final String socialAccountToken) {
return tokenService.extractSocialAccountTokenPayload(socialAccountToken);
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@

import ject.componote.domain.auth.dao.MemberRepository;
import ject.componote.domain.auth.domain.SocialAccount;
import ject.componote.domain.auth.dto.login.response.OAuthLoginResponse;
import ject.componote.domain.auth.dto.authorize.response.OAuthAuthorizationUrlResponse;
import ject.componote.domain.auth.dto.login.response.OAuthLoginResponse;
import ject.componote.domain.auth.token.application.TokenService;
import ject.componote.infra.oauth.application.OAuthClient;
import ject.componote.infra.oauth.dto.authorize.response.OAuthAuthorizePayload;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.scheduler.Schedulers;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class OAuthService {
private final MemberRepository memberRepository;
private final OAuthClient oAuthClient;
private final OAuthResultHandler oauthResultHandler;
private final TokenService tokenService;

public OAuthAuthorizationUrlResponse getOAuthAuthorizationCodeUrl(final String providerType) {
final OAuthAuthorizePayload oAuthAuthorizePayload = oAuthClient.getAuthorizePayload(providerType);
Expand All @@ -28,6 +32,7 @@ public OAuthLoginResponse login(final String providerType, final String code) {
.map(oauthResultHandler::saveOrGet)
.block();
final boolean isRegister = memberRepository.existsBySocialAccountId(socialAccount.getId());
return OAuthLoginResponse.of(isRegister, socialAccount);
final String socialAccountToken = tokenService.createSocialAccountToken(socialAccount);
return OAuthLoginResponse.of(isRegister, socialAccountToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

import jakarta.validation.constraints.NotNull;

public record MemberLoginRequest(@NotNull Long socialAccountId) {
public record MemberLoginRequest(@NotNull String socialAccountToken) {
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package ject.componote.domain.auth.dto.login.response;

import ject.componote.domain.auth.domain.SocialAccount;

public record OAuthLoginResponse(boolean isRegister, Long socialAccountId) {
public static OAuthLoginResponse of(final boolean isRegister, final SocialAccount socialAccount) {
return new OAuthLoginResponse(isRegister, socialAccount.getId());
public record OAuthLoginResponse(boolean isRegister, String socialAccountToken) {
public static OAuthLoginResponse of(final boolean isRegister, final String socialAccountToken) {
return new OAuthLoginResponse(isRegister, socialAccountToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ public record MemberSignupRequest(
@NotBlank String nickname,
@NotBlank String job,
@Nullable String profileImageObjectKey,
@NotNull Long socialAccountId) {
public Member toMember() {
@NotNull String socialAccountToken) {
public Member toMember(final Long socialAccountId) {
return Member.of(
nickname,
job,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import org.springframework.http.HttpStatus;

public class DuplicatedSignupException extends AuthException {
public DuplicatedSignupException(final Long socialAccountId) {
super("해당 소셜 ID로 이미 가입된 계정이 있습니다. 소셜 ID: " + socialAccountId, HttpStatus.BAD_REQUEST);
public DuplicatedSignupException() {
super("해당 소셜 ID로 이미 가입된 계정이 있습니다.", HttpStatus.BAD_REQUEST);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public static NotFoundMemberException createWhenInvalidMemberId(final Long membe
return new NotFoundMemberException("일치하는 회원을 찾을 수 없습니다. 회원 ID: " + memberId, HttpStatus.NOT_FOUND);
}

public static NotFoundMemberException createWhenInvalidSocialAccountId(final Long socialAccountId) {
return new NotFoundMemberException("소셜 ID에 해당하는 회원이 없습니다. 소셜 ID: " + socialAccountId, HttpStatus.NOT_FOUND);
public static NotFoundMemberException createWhenInvalidSocialAccountId() {
return new NotFoundMemberException("소셜 ID에 해당하는 회원이 없습니다.", HttpStatus.NOT_FOUND);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import org.springframework.http.HttpStatus;

public class NotFoundSocialAccountException extends AuthException {
public NotFoundSocialAccountException(final Long socialAccountId) {
super("일치하는 소셜 정보를 찾을 수 없습니다. 소셜 ID: " + socialAccountId, HttpStatus.NOT_FOUND);
public NotFoundSocialAccountException() {
super("일치하는 소셜 정보를 찾을 수 없습니다.", HttpStatus.NOT_FOUND);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package ject.componote.domain.auth.token.application;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import ject.componote.domain.auth.domain.SocialAccount;
import ject.componote.domain.auth.error.InvalidJWTException;
import ject.componote.domain.auth.token.error.InvalidSocialAccountTokenException;
import ject.componote.domain.auth.model.AuthPrincipal;
import ject.componote.domain.auth.token.model.TokenProvider;
import ject.componote.domain.auth.token.model.TokenType;
import ject.componote.domain.auth.token.repository.InMemoryTokenProviderRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
@Slf4j
@RequiredArgsConstructor
public class TokenService {
private final InMemoryTokenProviderRepository tokenProviderRepository;
private final ObjectMapper objectMapper;

public String createAccessToken(final AuthPrincipal authPrincipal) {
return createToken(authPrincipal, TokenType.ACCESS);
}

public String createSocialAccountToken(final SocialAccount socialAccount) {
return createToken(socialAccount.getId(), TokenType.SOCIAL_ACCOUNT);
}

public AuthPrincipal extractAccessTokenPayload(final String accessToken) {
try {
return objectMapper.readValue(
extractPayload(accessToken, TokenType.ACCESS),
AuthPrincipal.class
);
} catch (JsonProcessingException e) {
throw new InvalidJWTException();
}
}

public Long extractSocialAccountTokenPayload(final String socialAccountToken) {
try {
validateToken(socialAccountToken, TokenType.SOCIAL_ACCOUNT);
final String subject = extractPayload(socialAccountToken, TokenType.SOCIAL_ACCOUNT);
return Long.valueOf(subject);
} catch (NumberFormatException e) {
throw new InvalidSocialAccountTokenException(socialAccountToken);
}
}

public boolean validateAccessToken(final String accessToken) {
return validateToken(accessToken, TokenType.ACCESS);
}

private <T> String createToken(final T payload, final TokenType type) {
final TokenProvider provider = tokenProviderRepository.getProvider(type);
final String subject = createSubject(payload);
final Claims claims = Jwts.claims()
.setSubject(subject);
final Date now = new Date();
final Date expirationDate = new Date(now.getTime() + provider.expiration());
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(expirationDate)
.signWith(provider.key())
.compact();
}

private String extractPayload(final String token, final TokenType type) {
final TokenProvider provider = tokenProviderRepository.getProvider(type);
return Jwts.parserBuilder()
.setSigningKey(provider.key())
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}

private boolean validateToken(final String token, final TokenType type) {
try {
final TokenProvider provider = tokenProviderRepository.getProvider(type);
final Jws<Claims> claims = Jwts.parserBuilder()
.setSigningKey(provider.key())
.build()
.parseClaimsJws(token);
return !claims.getBody()
.getExpiration()
.before(new Date());
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}

private <T> String createSubject(final T payload) {
try {
return objectMapper.writeValueAsString(payload);
} catch (JsonProcessingException e) {
throw new IllegalStateException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package ject.componote.domain.auth.token.config;

import ject.componote.domain.auth.token.model.TokenAdapter;
import ject.componote.domain.auth.token.model.TokenProperties;
import ject.componote.domain.auth.token.repository.InMemoryTokenProviderRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(TokenProperties.class)
@RequiredArgsConstructor
public class TokenConfig {
private final TokenProperties properties;

@Bean
public InMemoryTokenProviderRepository inMemoryTokenProviderRepository() {
return new InMemoryTokenProviderRepository(
TokenAdapter.getTokenProviders(properties)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package ject.componote.domain.auth.token.error;

import ject.componote.domain.auth.error.AuthException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;

@Slf4j
public class InvalidSocialAccountTokenException extends AuthException {
public InvalidSocialAccountTokenException(final String socialAccountToken) {
super("유효하지 않은 socialAccountToken 입니다.", HttpStatus.BAD_REQUEST);
log.error("socialAccountToken: {}", socialAccountToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ject.componote.domain.auth.token.model;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import java.util.Map;
import java.util.stream.Collectors;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TokenAdapter {
public static Map<TokenType, TokenProvider> getTokenProviders(final TokenProperties properties) {
return properties.getToken()
.keySet()
.stream()
.collect(Collectors.toMap(
TokenType::from,
key -> TokenProvider.from(properties.getAttributeFrom(key))
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package ject.componote.domain.auth.token.model;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.HashMap;
import java.util.Map;

@ConfigurationProperties(prefix = "auth")
@Getter
@ToString
public class TokenProperties {
private final Map<String, Token> token = new HashMap<>();

public Token getAttributeFrom(final String key) {
return token.get(key);
}

@Getter
@Setter
@ToString
public static class Token {
private Long expiration;
private String secretKey;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ject.componote.domain.auth.token.model;

import io.jsonwebtoken.security.Keys;

import java.nio.charset.StandardCharsets;
import java.security.Key;

public record TokenProvider(Long expiration, Key key) {
public static TokenProvider from(final TokenProperties.Token attribute) {
return new TokenProvider(
attribute.getExpiration(),
Keys.hmacShaKeyFor(attribute.getSecretKey().getBytes(StandardCharsets.UTF_8))
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package ject.componote.domain.auth.token.model;

import lombok.Getter;

@Getter
public enum TokenType {
ACCESS,
REFRESH,
SOCIAL_ACCOUNT;

public static TokenType from(final String name) {
return valueOf(toConstantCase(name));
}

private static String toConstantCase(final String name) {
return name.replace("-", "_")
.toUpperCase();
}
}
Loading
Loading