Skip to content
Merged
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
2 changes: 1 addition & 1 deletion BEConfig
20 changes: 15 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,21 @@ dependencies {
testImplementation 'org.mockito:mockito-inline:5.2.0'

// QueryDSL : OpenFeign
implementation "io.github.openfeign.querydsl:querydsl-jpa:7.0"
implementation "io.github.openfeign.querydsl:querydsl-core:7.0"
annotationProcessor "io.github.openfeign.querydsl:querydsl-apt:7.0:jpa"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
implementation "io.github.openfeign.querydsl:querydsl-jpa:7.0"
implementation "io.github.openfeign.querydsl:querydsl-core:7.0"
annotationProcessor "io.github.openfeign.querydsl:querydsl-apt:7.0:jpa"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"

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

// Jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
implementation 'org.springframework.boot:spring-boot-configuration-processor'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,17 @@ public class MemberController {
@PostMapping("/signup")
@Operation(summary = "회원가입", description = "새로운 회원을 등록합니다.")
public ApiResponse<MemberResDTO.JoinDTO> signUp(
@RequestBody @Valid MemberReqDTO.JoinDTO dto
) {
@RequestBody @Valid MemberReqDTO.JoinDTO dto) {
MemberResDTO.JoinDTO response = memberCommandService.signup(dto);
return ApiResponse.onSuccess(MemberSuccessCode.MEMBER_CREATED, response);
}

// 로그인
@PostMapping("/login")
@Operation(summary = "로그인", description = "이메일과 비밀번호로 로그인합니다.")
public ApiResponse<MemberResDTO.LoginDTO> login(
@RequestBody @Valid MemberReqDTO.LoginDTO dto) {
MemberResDTO.LoginDTO response = memberCommandService.login(dto);
return ApiResponse.onSuccess(MemberSuccessCode.MEMBER_LOGIN_SUCCESS, response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.example.umc.domain.member.dto.MemberReqDTO;
import com.example.umc.domain.member.dto.MemberResDTO;
import com.example.umc.domain.user.entity.User;
import com.example.umc.global.auth.Role;

public class MemberConverter {

Expand All @@ -14,12 +15,41 @@ public static MemberResDTO.JoinDTO toJoinDTO(User member) {
.build();
}

// Entity -> LoginDTO
/*
public static MemberResDTO.LoginDTO toLoginDTO(User member) {
return MemberResDTO.LoginDTO.builder()
.memberId(member.getUserId())
.email(member.getEmail())
.name(member.getName())
.role(member.getRole())
.createdAt(member.getCreatedAt())
.build();
}
*/

// Entity + AccessToken -> LoginDTO
public static MemberResDTO.LoginDTO toLoginDTO(User member, String accessToken) {
return MemberResDTO.LoginDTO.builder()
.memberId(member.getUserId())
.email(member.getEmail())
.name(member.getName())
.role(member.getRole())
.accessToken(accessToken)
.createdAt(member.getCreatedAt())
.build();
}

// DTO -> Entity
public static User toMember(MemberReqDTO.JoinDTO dto) {
public static User toMember(MemberReqDTO.JoinDTO dto, String salt, Role role) {
return User.builder()
.name(dto.name())
.birth(dto.birth())
.address(dto.address() != null ? dto.address().toString() : null)
.password(salt)
.role(dto.role())
.email(dto.email())
.address(dto.address())
.gender(dto.gender())
.build();
}
Expand Down
13 changes: 11 additions & 2 deletions src/main/java/com/example/umc/domain/member/dto/MemberReqDTO.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.umc.domain.member.dto;

import com.example.umc.domain.user.enums.Gender;
import com.example.umc.global.auth.Role;

import java.time.LocalDate;
import java.util.List;
Expand All @@ -10,8 +11,16 @@ public record JoinDTO(
String name,
Gender gender,
LocalDate birth,
String password,
String email,
Role role,
String address,
String specAddress,
List<Long> preferCategory
) {}
List<Long> preferCategory) {
}

public record LoginDTO(
String email,
String password) {
}
}
15 changes: 13 additions & 2 deletions src/main/java/com/example/umc/domain/member/dto/MemberResDTO.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.umc.domain.member.dto;

import com.example.umc.global.auth.Role;
import lombok.Builder;

import java.time.LocalDateTime;
Expand All @@ -8,6 +9,16 @@ public class MemberResDTO {
@Builder
public record JoinDTO(
Long memberId,
LocalDateTime createdAt
) {}
LocalDateTime createdAt) {
}

@Builder
public record LoginDTO(
Long memberId,
String email,
String name,
Role role,
String accessToken,
LocalDateTime createdAt) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
public enum MemberErrorCode implements BaseErrorCode {

MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_1", "해당 사용자를 찾지 못했습니다."),
INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "MEMBER401_1", "비밀번호가 일치하지 않습니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
public enum MemberSuccessCode implements BaseCode {

MEMBER_CREATED(HttpStatus.CREATED, "MEMBER201_1", "성공적으로 사용자가 생성되었습니다."),
MEMBER_LOGIN_SUCCESS(HttpStatus.OK, "MEMBER200_1", "로그인에 성공했습니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface MemberRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@
public interface MemberCommandService {
// 회원가입
MemberResDTO.JoinDTO signup(MemberReqDTO.JoinDTO dto);

// 로그인
MemberResDTO.LoginDTO login(MemberReqDTO.LoginDTO dto);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.example.umc.domain.member.converter.MemberConverter;
import com.example.umc.domain.member.dto.MemberReqDTO;
import com.example.umc.domain.member.dto.MemberResDTO;
import com.example.umc.domain.member.exception.MemberException;
import com.example.umc.domain.member.exception.code.MemberErrorCode;
import com.example.umc.domain.member.repository.MemberRepository;
import com.example.umc.domain.user.entity.User;
import com.example.umc.domain.user.entity.UserPrefer;
Expand All @@ -11,7 +13,12 @@
import com.example.umc.domain.category.repository.PreferCategoryRepository;
import com.example.umc.domain.category.exception.CategoryException;
import com.example.umc.domain.category.exception.code.CategoryErrorCode;
import com.example.umc.global.auth.CustomUserDetails;
import com.example.umc.global.auth.JwtUtil;
import com.example.umc.global.auth.Role;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -25,12 +32,17 @@ public class MemberCommandServiceImpl implements MemberCommandService {
private final MemberRepository memberRepository;
private final UserPreferRepository userPreferRepository;
private final PreferCategoryRepository preferCategoryRepository;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;

@Override
@Transactional
public MemberResDTO.JoinDTO signup(MemberReqDTO.JoinDTO dto) {

// 비밀번호 암호화
String salt = passwordEncoder.encode(dto.password());
// 사용자 생성
User member = MemberConverter.toMember(dto);
User member = MemberConverter.toMember(dto, salt, Role.ROLE_USER);

// DB 적용
memberRepository.save(member);
Expand Down Expand Up @@ -58,4 +70,26 @@ public MemberResDTO.JoinDTO signup(MemberReqDTO.JoinDTO dto) {
// 응답 DTO 생성
return MemberConverter.toJoinDTO(member);
}

@Override
@Transactional(readOnly = true)
public MemberResDTO.LoginDTO login(@Valid MemberReqDTO.LoginDTO dto) {
// User 조회
User user = memberRepository.findByEmail(dto.email())
.orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));

// 비밀번호 검증
if (!passwordEncoder.matches(dto.password(), user.getPassword())) {
throw new MemberException(MemberErrorCode.INVALID_PASSWORD);
}

// JWT 토큰 발급용 UserDetails
CustomUserDetails userDetails = new CustomUserDetails(user);

// 엑세스 토큰 발급
String accessToken = jwtUtil.createAccessToken(userDetails);

// DTO 조립
return MemberConverter.toLoginDTO(user, accessToken);
}
}
7 changes: 7 additions & 0 deletions src/main/java/com/example/umc/domain/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.example.umc.domain.review.entity.Review;
import com.example.umc.domain.notification.entity.Notification;
import com.example.umc.global.common.BaseEntity;
import com.example.umc.global.auth.Role;
import jakarta.persistence.*;
import lombok.*;
import lombok.experimental.SuperBuilder;
Expand Down Expand Up @@ -67,6 +68,12 @@ public class User extends BaseEntity {
@Column(name = "email", length = 255)
private String email;

@Column(nullable = false)
private String password;

@Enumerated(EnumType.STRING)
private Role role;

@Column(name = "phone", length = 100)
private String phone;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.example.umc.global.auth;

import com.example.umc.global.apiPayload.ApiResponse;
import com.example.umc.global.apiPayload.code.status.ErrorStatus;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {

private final ObjectMapper objectMapper = new ObjectMapper();

@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

ApiResponse<Void> errorResponse = ApiResponse.onFailure(
ErrorStatus._UNAUTHORIZED.getReasonHttpStatus().getCode(),
ErrorStatus._UNAUTHORIZED.getReasonHttpStatus().getMessage(),
null);

objectMapper.writeValue(response.getOutputStream(), errorResponse);
}
}
54 changes: 54 additions & 0 deletions src/main/java/com/example/umc/global/auth/CustomUserDetails.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.example.umc.global.auth;

import com.example.umc.domain.user.entity.User;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {

private final User user;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(() -> user.getRole().toString());
}

@Override
public String getPassword() {
return user.getPassword();
}

@Override
public String getUsername() {
return user.getEmail();
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return user.getUserStatus().name().equals("ACTIVE");
}

public User getUser() {
return user;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.example.umc.global.auth;

import com.example.umc.domain.member.repository.MemberRepository;
import com.example.umc.domain.user.entity.User;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

private final MemberRepository memberRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 검증할 Member 조회
User user = memberRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("해당 사용자를 찾지 못했습니다."));

// CustomUserDetails 반환
return new CustomUserDetails(user);
}
}
Loading