Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 13 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,20 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'

// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'

// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

//oauth
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'

// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
Expand All @@ -33,10 +44,10 @@ dependencies {
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9'

// MYSQL
implementation 'com.mysql:mysql-connector-j:9.1.0'
//implementation 'com.mysql:mysql-connector-j:9.1.0'

// H2
//runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.h2database:h2'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.moongeul.backend.api.member.controller;

import com.moongeul.backend.api.member.dto.LoginResponseDTO;
import com.moongeul.backend.api.member.dto.LoginRequestDTO;
import com.moongeul.backend.api.member.dto.UserInfoDTO;
import com.moongeul.backend.api.member.entity.Member;
import com.moongeul.backend.api.member.service.MemeberService;
import com.moongeul.backend.common.exception.BadRequestException;
import com.moongeul.backend.common.response.ApiResponse;
import com.moongeul.backend.common.response.ErrorStatus;
import com.moongeul.backend.common.response.SuccessStatus;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

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

private final MemeberService memberService;

@Operation(
summary = "로그인 API",
description = "구글 인가코드을 통해 사용자의 정보를 등록 및 토큰 + 역할을 발급합니다. (ROLE -> 처음사용자 : GUEST, 일반사용자 : USER, 관리자 : ADMIN)"
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "로그인 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "구글 엑세스토큰이 입력되지 않았습니다."),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "유효하지 않은 엑세스토큰 입니다.")
})
@PostMapping("/login")
public ResponseEntity<ApiResponse<LoginResponseDTO>> loginWithGoogle(@RequestBody LoginRequestDTO loginRequestDTO) {
// 엑세스토큰이 입력되지 않았을 경우 예외 처리
if (loginRequestDTO == null || loginRequestDTO.getCode() == null || loginRequestDTO.getCode().isEmpty()) {
throw new BadRequestException(ErrorStatus.MISSING_GOOGLE_ACCESSTOKEN.getMessage());
}

LoginResponseDTO response = memberService.loginWithGoogle(loginRequestDTO.getCode());
return ApiResponse.success(SuccessStatus.SEND_LOGIN_SUCCESS, response);
}

@Operation(
summary = "사용자 정보 조회 API",
description = "토큰을 통해 인증된 사용자의 정보를 반환합니다."
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "사용자 정보 조회 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "해당 사용자를 찾을 수 없습니다.")
})
@GetMapping("/user-info")
public ResponseEntity<ApiResponse<UserInfoDTO>> getUserInfo(@AuthenticationPrincipal UserDetails userDetails){
Member member = memberService.getMemberByEmail(userDetails.getUsername());
UserInfoDTO response = memberService.getUserInfo(member);
return ApiResponse.success(SuccessStatus.GET_USERINFO_SUCCESS, response);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.moongeul.backend.api.member.dto;

import lombok.Getter;

@Getter
public class LoginRequestDTO {
private String code; // 인가코드
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.moongeul.backend.api.member.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LoginResponseDTO {

private String role;
private String accessToken; // JWT Access Token (우리 서버)
private String refreshToken; // JWT Refresh Token (우리 서버)
}
14 changes: 14 additions & 0 deletions src/main/java/com/moongeul/backend/api/member/dto/UserInfoDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.moongeul.backend.api.member.dto;

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class UserInfoDTO {

private final Long id;
private final String name; // 회원 이름(실명)
private final String profileImage; // 회원 이미지
private final String nickname; //닉네임 (초기랜덤생성)
}
59 changes: 59 additions & 0 deletions src/main/java/com/moongeul/backend/api/member/entity/Member.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.moongeul.backend.api.member.entity;

import com.moongeul.backend.common.entity.BaseTimeEntity;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@Builder // 빌더 패턴 사용을 위한 롬복 애너테이션
@NoArgsConstructor // 기본 생성자
@AllArgsConstructor // 모든 필드를 포함한 생성자
@Table(name = "MEMBER") // 데이터베이스 테이블 이름 지정
public class Member extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // 회원번호(PK)

@Column(unique = true, nullable = false)
private String email; // 이메일

private String name; // 회원 이름(실명)
private String profileImage; // 회원 이미지
private String nickname; //닉네임 (초기랜덤생성)
private String password;

@Column(unique = true)
private String socialId; // 소셜 로그인 ID (고유값)

private String socialType; // 소셜 로그인 제공자: Google, Kakao ...

@Enumerated(EnumType.STRING)
private Role role; // 권한, Role.valueOf(role)로 저장

private String refreshToken; // Refresh Token

/**
* 권한 가져오기
*/
public String getAuthorityKey() {
return this.role.getKey();
}

/**
* OAuth2 로그인 시 이름, 사진이 변경될 경우 Entity를 업데이트하는 메서드
*/
public Member update(String name, String picture) {
this.name = name;
this.profileImage = picture;
return this;
}

public void updateRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
}
14 changes: 14 additions & 0 deletions src/main/java/com/moongeul/backend/api/member/entity/Role.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.moongeul.backend.api.member.entity;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum Role {

GUEST("ROLE_GUEST"), USER("ROLE_USER"), ADMIN("ROLE_ADMIN");

private final String key;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.moongeul.backend.api.member.jwt.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

@Builder
@Data
@AllArgsConstructor
public class JwtTokenDTO {
private String grantType; //JWT에 대한 인증 타입
private String accessToken;
private String refreshToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.moongeul.backend.api.member.jwt.filter;

import com.moongeul.backend.common.config.jwt.JwtTokenProvider;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.FilterChain;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
import java.io.IOException;
import jakarta.servlet.ServletException;
import org.springframework.util.StringUtils;

/*
* 클라이언트 요청 시 JWT 인증을 하기 위해 설치하는 커스텀 필터
* 클라이언트로부터 들어오는 요청에서 JWT 토큰을 처리하고, 유효한 토큰인 경우 해당 토큰의 인증 정보(Authentication)를 SecurityContext에 저장하여 인증된 요청을 처리
* JWT를 통해 username + password 인증을 수행한다는 뜻!
*/
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends GenericFilterBean {
private final JwtTokenProvider jwtTokenProvider;

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 1. Request Header에서 JWT 토큰 추출
String token = resolveToken((HttpServletRequest) request);

// 2. validateToken으로 토큰 유효성 검사
if (token != null && jwtTokenProvider.validateToken(token)) {
// 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext에 저장
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}

// Request Header에서 토큰 정보 추출
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
log.info("Authorization Header Value: [{}]", bearerToken); // 💡 값 전체 출력 (로그 레벨 info 이상)

if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) {
return bearerToken.substring(7).trim();
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.moongeul.backend.api.member.repository;

import com.moongeul.backend.api.member.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {

Optional<Member> findByEmail(String email);

Optional<Member> findBySocialId(String socialId);
}
Loading