Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@Slf4j
@SpringBootApplication(scanBasePackageClasses = {
Expand All @@ -16,8 +14,6 @@
AcneLogInfraRoot.class,
ApiModuleApplication.class
})
@EntityScan(basePackages = "hongik.triple.domainmodule") // 도메인 모듈의 엔티티 경로
@EnableJpaRepositories(basePackages = "hongik.triple.domainmodule") // 도메인 모듈의 레포지토리 경로
public class ApiModuleApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package hongik.triple.apimodule.application.member;

import hongik.triple.apimodule.global.security.jwt.TokenProvider;
import hongik.triple.commonmodule.dto.member.MemberReq;
import hongik.triple.commonmodule.dto.member.MemberRes;
import hongik.triple.commonmodule.enumerate.MemberType;
import hongik.triple.domainmodule.domain.member.Member;
import hongik.triple.domainmodule.domain.member.repository.MemberRepository;
import hongik.triple.inframodule.oauth.google.GoogleClient;
Expand All @@ -22,26 +24,32 @@ public class MemberService {
private final MemberRepository memberRepository;
private final KakaoClient kakaoClient;
private final GoogleClient googleClient;
private final TokenProvider tokenProvider;

@Transactional
public void withdrawal(Member member) {
memberRepository.delete(member);
public String getKakaoLoginUrl(String redirectUri) {
return kakaoClient.getKakaoAuthUrl(redirectUri);
}

public MemberRes loginWithKakao(String code, String redirectUri) {
KakaoToken kakaoToken = kakaoClient.getKakaoAccessToken(code, redirectUri);
KakaoProfile kakaoProfile = kakaoClient.getMemberInfo(kakaoToken);
public String getGoogleLoginUrl(String redirectUri) {
return googleClient.getGoogleAuthUrl(redirectUri);
}

return register(kakaoProfile.kakao_account().email(), kakaoProfile.properties().nickname());
public KakaoProfile loginWithKakao(String authorizationCode, String redirectUri) {
KakaoToken kakaoToken = kakaoClient.getKakaoAccessToken(authorizationCode, redirectUri);
return kakaoClient.getMemberInfo(kakaoToken);
}

public MemberRes loginWithGoogle(String accessToken, String redirectUri) {
GoogleToken googleToken = googleClient.getGoogleAccessToken(accessToken, redirectUri);
GoogleProfile googleProfile = googleClient.getMemberInfo(googleToken);
public GoogleProfile loginWithGoogle(String authorizationCode, String redirectUri) {
GoogleToken googleToken = googleClient.getGoogleAccessToken(authorizationCode, redirectUri);
return googleClient.getMemberInfo(googleToken);
}

return register(googleProfile.email(), googleProfile.name());
@Transactional
public void withdrawal(Member member) {
memberRepository.delete(member);
}

@Transactional
public void logout() {

}
Expand All @@ -56,7 +64,7 @@ public MemberRes getProfile(Member member) {

@Transactional
public MemberRes updateProfile(Member member, MemberReq memberReq) {
member.update(memberReq.name(), memberReq.skin_type());
member.updateSkinType(memberReq.skin_type());
Member updateMember = memberRepository.save(member);

return MemberRes.builder()
Expand All @@ -66,30 +74,29 @@ public MemberRes updateProfile(Member member, MemberReq memberReq) {
.build();
}

@Transactional
protected MemberRes register(String email, String nickname) {
// TODO: DB 회원가입 실패 시, 카카오에서도 회원 가입 실패로 보상 트랜잭션 처리 필요
@Transactional // 독립적인 트랜잭션으로 실행, 상위 트랜잭션은 읽기 트랜잭션으로 유지
public MemberRes register(String email, String nickname, MemberType memberType) {
if (email == null || email.isEmpty()) {
throw new IllegalArgumentException("Email cannot be null or empty");
}
if (nickname == null || nickname.isEmpty()) {
throw new IllegalArgumentException("Nickname cannot be null or empty");
}

return memberRepository.findByEmail(email)
.map(member ->
MemberRes.builder()
.id(member.getMemberId())
.email(member.getEmail())
.name(member.getName())
.build())
Member member = memberRepository.findByEmail(email)
.orElseGet(() -> {
Member newMember = new Member(email, nickname);
Member saveMember = memberRepository.save(newMember);
return MemberRes.builder()
.id(saveMember.getMemberId())
.email(saveMember.getEmail())
.name(saveMember.getName())
.build();
Member newMember = new Member(nickname, email, memberType);
return memberRepository.save(newMember);
});

String accessToken = tokenProvider.createToken(member).accessToken();

return MemberRes.builder()
.id(member.getMemberId())
.email(member.getEmail())
.name(member.getName())
.accessToken(accessToken)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers("/error").permitAll()
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/api/v1/member/**").authenticated()
.requestMatchers("/api/v1/contest/{contest_id}/team/**").authenticated()
.requestMatchers("/api/v2/apply/**").authenticated()
// 이외의 모든 요청은 인증 정보 필요
.anyRequest().permitAll());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ public PrincipalDetails(Member member, Map<String, Object> attributes) {
// 권한 정보 반환 (GENERAL, ADMIN 중 하나)
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority(member.getMemberType()));
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_" + member.getMemberType().name()));

return authorities;
}
Expand Down Expand Up @@ -70,15 +70,4 @@ public boolean isCredentialsNonExpired() {
public boolean isEnabled() {
return true;
}

// OAuth2User 인터페이스 메서드 (필요시 구현)
// @Override
// public String getName() {
// return member.getName();
// }
//
// @Override
// public Map<String, Object> getAttributes() {
// return attributes;
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ public class JwtFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = resolveToken(request);
// String requestURI = request.getRequestURI();

// 토큰이 존재할 경우, Authentication에 인증 정보 저장 및 로그 출력
if (StringUtils.hasText(token) && tokenProvider.validateToken(token)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package hongik.triple.apimodule.global.security.jwt;

import hongik.triple.apimodule.global.security.PrincipalDetails;
import hongik.triple.commonmodule.enumerate.MemberType;
import hongik.triple.commonmodule.exception.ApplicationException;
import hongik.triple.commonmodule.exception.ErrorCode;
import hongik.triple.domainmodule.domain.member.Member;
Expand Down Expand Up @@ -46,12 +48,14 @@ protected void init() {
* @return 생성된 액세스 토큰 정보 반환
*/
private String createAccessToken(Member member) {
Claims claims = getClaims(member);
Date now = new Date();
Date expirationDate = new Date(now.getTime() + accessTokenExpirationTime);

return Jwts.builder()
.claims(claims)
.subject(member.getEmail())
.claim("memberType", member.getMemberType().name())
.issuedAt(now)
.expiration(new Date(now.getTime() + accessTokenExpirationTime))
.expiration(expirationDate)
.signWith(key)
.compact();
}
Expand Down Expand Up @@ -84,34 +88,21 @@ public boolean validateToken(String token) {
}
}

/**
* 리프레쉬 토큰 기반으로 액세스 토큰 재발급 + 리프레쉬 토큰의 유효기간이 액세스 토큰의 유효기간보다 짧을 경우, 리프레쉬 토큰도 재발급
* @param member - 재발급을 요청한 사용자 정보
* @param refreshToken - 재발급을 요청했던 리프레쉬 토큰
* @return 재발급된 액세스 토큰을 담은 TokenDto 객체 반환
*/
public TokenDto reissue(Member member, String refreshToken) {
// 액세스 토큰 재발급
String accessToken = createAccessToken(member);

return TokenDto.builder()
.accessToken(accessToken)
.build();
}

/**
* 토큰에서 정보를 추출해서 Authentication 객체를 반환
* @param token - 액세스 토큰으로, 해당 토큰에서 정보를 추출해서 사용
* @return 토큰 정보와 일치하는 Authentication 객체 반환
*/
public Authentication getAuthentication(String token) {
String email = getEmail(token);
// Member member = memberRepository.findMemberByEmailAndMemberTypeAndDeletedAtIsNull(email, memberType)
// .orElseThrow(() -> new ApplicationException(ErrorCode.NOT_FOUND_EXCEPTION));
// PrincipalDetails principalDetails = new PrincipalDetails(member);
//
// return new UsernamePasswordAuthenticationToken(principalDetails, "", principalDetails.getAuthorities());
return null; // TODO: update
MemberType memberType = getMemberType(token);

Member member = memberRepository.findMemberByEmailAndMemberTypeAndDeletedAtIsNull(email, memberType)
.orElseThrow(() -> new ApplicationException(ErrorCode.NOT_FOUND_EXCEPTION));

PrincipalDetails principalDetails = new PrincipalDetails(member);

return new UsernamePasswordAuthenticationToken(principalDetails, "", principalDetails.getAuthorities());
}

/**
Expand All @@ -128,28 +119,14 @@ public String getEmail(String token) {
.getSubject();
}

/**
* 토큰의 만료기한 반환
* @param token - 일반적으로 액세스 토큰 / 토큰 재발급 요청 시에는 리프레쉬 토큰이 들어옴
* @return 해당 토큰의 만료정보를 반환
*/
public Date getExpiration(String token) {
return Jwts.parser()
public MemberType getMemberType(String token) {
String memberTypeStr = Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.getPayload()
.getExpiration();
}
.get("memberType", String.class);

/**
* Claims 정보 생성
* @param member - 사용자 정보 중 사용자를 구분할 수 있는 정보 두 개를 활용함
* @return 사용자 구분 정보인 이메일과 역할을 저장한 Claims 객체 반환
*/
private Claims getClaims(Member member) {
return Jwts.claims()
.subject(member.getEmail())
.build();
return MemberType.valueOf(memberTypeStr);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,58 +5,85 @@
import hongik.triple.apimodule.global.security.PrincipalDetails;
import hongik.triple.commonmodule.dto.member.MemberReq;
import hongik.triple.commonmodule.dto.member.MemberRes;
import hongik.triple.commonmodule.enumerate.MemberType;
import hongik.triple.inframodule.oauth.google.GoogleProfile;
import hongik.triple.inframodule.oauth.kakao.KakaoProfile;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/member")
@RequestMapping("/api/v1")
@RequiredArgsConstructor
@Tag(name = "Member", description = "회원 관련 API")
public class MemberController {

private final MemberService memberService;

/**
* 로그인/회원가입 API - OAuth2 제공자에 따라 진행되며, 기존 로그인 정보 유무에 따라 회원가입 또는 로그인 처리
* @param provider OAuth2 제공자 (예: kakao, google 등)
* @return 회원 정보 응답 (MemberRes)
*/
@PostMapping("/login")
public ApplicationResponse<MemberRes> login(@RequestParam(name = "provider") String provider,
@RequestParam(name = "redirect-uri") String redirectUri) {
// 회원가입 로직
if(provider.equals("kakao")) {
// 카카오 로그인 로직
return ApplicationResponse.ok(memberService.loginWithKakao(provider, redirectUri));
} else if(provider.equals("google")) {
// 구글 로그인 로직
return ApplicationResponse.ok(memberService.loginWithGoogle(provider, redirectUri));
@GetMapping("/auth/login")
public ResponseEntity<?> redirectLoginPage(
@RequestParam(name = "provider") String provider,
@RequestParam(name = "redirect-uri", required = false) String redirectUri) {
String authUrl = switch (provider) {
case "kakao" -> memberService.getKakaoLoginUrl(redirectUri);
case "google" -> memberService.getGoogleLoginUrl(redirectUri);
default -> throw new IllegalArgumentException("지원하지 않는 로그인 제공자입니다.");
};

if(redirectUri == null || redirectUri.isEmpty()) {
HttpHeaders headers = new HttpHeaders();
headers.add("Location", authUrl);
return new ResponseEntity<>(headers, HttpStatus.FOUND);
} else {
throw new IllegalArgumentException("지원하지 않는 로그인 제공자입니다.");
return ResponseEntity.ok(ApplicationResponse.ok(authUrl));
}
}

@PostMapping("/withdrawal")
public void withdrawal(@AuthenticationPrincipal PrincipalDetails principalDetails) {
// 회원탈퇴 로직
memberService.withdrawal(principalDetails.getMember());
/**
* 카카오 로그인/회원가입 API - 기존 로그인 정보 유무에 따라 회원가입 또는 로그인 처리
* @return 회원 정보 응답 (MemberRes)
*/
@GetMapping("/auth/kakao/login")
public ApplicationResponse<MemberRes> loginWithKakao(
@RequestParam(name = "code") String authorizationCode,
@RequestParam(name = "redirect-uri", required = false) String redirectUri) {
KakaoProfile kakaoProfile = memberService.loginWithKakao(authorizationCode, redirectUri);
return ApplicationResponse.ok(memberService.register(kakaoProfile.kakao_account().email(), kakaoProfile.properties().nickname(), MemberType.KAKAO));
}

/**
* 구글 로그인/회원가입 API - 기존 로그인 정보 유무에 따라 회원가입 또는 로그인 처리
* @return 회원 정보 응답 (MemberRes)
*/
@GetMapping("/auth/google/login")
public ApplicationResponse<MemberRes> loginWithGoogle(
@RequestParam(name = "code") String authorizationCode,
@RequestParam(name = "redirect-uri", required = false) String redirectUri) {
GoogleProfile googleProfile = memberService.loginWithGoogle(authorizationCode, redirectUri);
return ApplicationResponse.ok(memberService.register(googleProfile.email(), googleProfile.name(), MemberType.GOOGLE));
}

@PostMapping("/logout")
public void logout(@AuthenticationPrincipal PrincipalDetails principalDetails) {
memberService.logout();
@PostMapping("/member/withdrawal")
public void withdrawal(
@AuthenticationPrincipal PrincipalDetails principalDetails) {
// 회원탈퇴 로직
memberService.withdrawal(principalDetails.getMember());
}

@PostMapping("/profile")
public void getProfile(@AuthenticationPrincipal PrincipalDetails principalDetails) {
memberService.getProfile(principalDetails.getMember());
@GetMapping("/member/profile")
public ApplicationResponse<MemberRes> getProfile(
@AuthenticationPrincipal PrincipalDetails principalDetails) {
return ApplicationResponse.ok(memberService.getProfile(principalDetails.getMember()));
}

@PatchMapping("/update")
public void updateProfile(@AuthenticationPrincipal PrincipalDetails principalDetails, @RequestBody MemberReq req) {
@PatchMapping("/member/update")
public void updateProfile(
@AuthenticationPrincipal PrincipalDetails principalDetails,
@RequestBody MemberReq req) {
memberService.updateProfile(principalDetails.getMember(), req);
}
}
Loading