Skip to content
Open
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
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ dependencies {

//Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

// 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 @@ -9,18 +9,28 @@
import ssu.cromi.umc9th.domain.user.dto.UserResDTO;
import ssu.cromi.umc9th.domain.user.exception.code.UserSuccessCode;
import ssu.cromi.umc9th.domain.user.service.UserCommandService;
import ssu.cromi.umc9th.domain.user.service.UserQueryService;
import ssu.cromi.umc9th.global.apiPayload.ApiResponse;

@RestController
@RequiredArgsConstructor
public class UserController {

private final UserCommandService userCommandService;
private final UserQueryService userQueryService;


@PostMapping("/sign-up")
public ApiResponse<UserResDTO.JoinDTO> signUp(
@RequestBody @Valid UserReqDTO.JoinDTO dto
){
return ApiResponse.onSuccess(UserSuccessCode.FOUND, userCommandService.signup(dto));
}

@PostMapping("/login")
public ApiResponse<UserResDTO.LoginDTO> login(
@RequestBody @Valid UserReqDTO.LoginDTO dto
){
return ApiResponse.onSuccess(UserSuccessCode.FOUND, userQueryService.login(dto));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import ssu.cromi.umc9th.domain.user.dto.UserReqDTO;
import ssu.cromi.umc9th.domain.user.dto.UserResDTO;
import ssu.cromi.umc9th.domain.user.entity.User;
import ssu.cromi.umc9th.global.auth.enums.Role;

import java.lang.reflect.Member;

Expand All @@ -19,14 +20,26 @@ public static UserResDTO.JoinDTO toJoinDTO(

//DTO -> Entity
public static User toUser(
UserReqDTO.JoinDTO dto
UserReqDTO.JoinDTO dto,
String password,
Role role
){
return User.builder()
.nickname(dto.nickname())
.email(dto.email())
.password(password)
.role(role)
.birthday(dto.birthday())
.address(dto.address())
.specAddress(dto.specAddress())
.gender(dto.gender())
.build();
}

public static UserResDTO.LoginDTO toLoginDTO(User user, String accessToken) {
return UserResDTO.LoginDTO.builder()
.userId(user.getUserId())
.accessToken(accessToken)
.build();
}
}
18 changes: 18 additions & 0 deletions src/main/java/ssu/cromi/umc9th/domain/user/dto/UserReqDTO.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package ssu.cromi.umc9th.domain.user.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import ssu.cromi.umc9th.domain.user.enums.Gender;
import ssu.cromi.umc9th.global.annotation.ExistFoods;

Expand All @@ -8,12 +11,27 @@

public class UserReqDTO {
public record JoinDTO(
@NotBlank
String nickname,
@Email
String email,
@NotBlank
String password,
@NotNull
Gender gender,
@NotNull
LocalDate birthday,
@NotNull
String address,
@NotNull
String specAddress,
@ExistFoods
List<Long> preferCategory
){}
public record LoginDTO(
@NotBlank
String email,
@NotBlank
String password
){}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,10 @@ public record JoinDTO(
Long userId,
LocalDateTime createdAt
){}
// 로그인
@Builder
public record LoginDTO(
Long userId,
String accessToken
){}
}
7 changes: 7 additions & 0 deletions src/main/java/ssu/cromi/umc9th/domain/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import ssu.cromi.umc9th.domain.review.entity.UserReview;
import ssu.cromi.umc9th.domain.user.enums.Gender;
import ssu.cromi.umc9th.domain.user.enums.UserStatus;
import ssu.cromi.umc9th.global.auth.enums.Role;
import ssu.cromi.umc9th.global.entity.BaseEntity;

import java.time.LocalDate;
Expand Down Expand Up @@ -46,6 +47,12 @@ public class User extends BaseEntity {
@Column(nullable = true, length = 255, unique = true)
private String email;

@Column(nullable = false)
private String password;

@Enumerated(EnumType.STRING)
private Role role;

@Column(nullable = true, length = 255)
private String phone;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
import ssu.cromi.umc9th.domain.user.entity.User;
import ssu.cromi.umc9th.domain.user.dto.UserProfileDto;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
// 이메일 조회 시 fetch join 추가
@Query("""
SELECT u
FROM User u
WHERE u.email = :email
""")
User findByEmail(@Param("email") String email);
Optional<User> findByEmail(@Param("email") String email);

@Query("""
SELECT new ssu.cromi.umc9th.domain.user.dto.UserProfileDto(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package ssu.cromi.umc9th.domain.user.service;

import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ssu.cromi.umc9th.domain.food.entity.FoodCategory;
import ssu.cromi.umc9th.domain.food.entity.UserFood;
import ssu.cromi.umc9th.domain.food.exception.FoodException.FoodException;
import ssu.cromi.umc9th.domain.food.exception.code.FoodErrorCode;
Expand All @@ -14,8 +14,8 @@
import ssu.cromi.umc9th.domain.user.dto.UserResDTO;
import ssu.cromi.umc9th.domain.user.entity.User;
import ssu.cromi.umc9th.domain.user.repository.UserRepository;
import ssu.cromi.umc9th.global.auth.enums.Role;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

Expand All @@ -26,14 +26,19 @@ public class UserCommandServiceImpl implements UserCommandService{
private final FoodRepository foodRepository;
private final UserFoodRepository userFoodRepository;

// Password Encoder
private final PasswordEncoder passwordEncoder;

//회원가입
@Override
@Transactional
public UserResDTO.JoinDTO signup(
UserReqDTO.JoinDTO dto
) {
// 솔트된 비밀번호 생성
String salt = passwordEncoder.encode(dto.password());
//사용자 생성
User user = UserConverter.toUser(dto);
User user = UserConverter.toUser(dto,salt, Role.ROLE_USER);
// DB적용
userRepository.save(user);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ssu.cromi.umc9th.domain.user.service;

import ssu.cromi.umc9th.domain.user.dto.UserReqDTO;
import ssu.cromi.umc9th.domain.user.dto.UserResDTO;

public interface UserQueryService {

// 로그인
UserResDTO.LoginDTO login(
UserReqDTO.LoginDTO dto
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package ssu.cromi.umc9th.domain.user.service;

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;
import ssu.cromi.umc9th.domain.user.converter.UserConverter;
import ssu.cromi.umc9th.domain.user.dto.UserReqDTO;
import ssu.cromi.umc9th.domain.user.dto.UserResDTO;
import ssu.cromi.umc9th.domain.user.entity.User;
import ssu.cromi.umc9th.domain.user.exception.UserException.UserException;
import ssu.cromi.umc9th.domain.user.exception.code.UserErrorCode;
import ssu.cromi.umc9th.domain.user.repository.UserRepository;
import ssu.cromi.umc9th.global.auth.entity.JwtUtil;
import ssu.cromi.umc9th.global.auth.service.CustomUserDetails;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserQueryServiceImpl implements UserQueryService {

private final UserRepository userRepository;
private final JwtUtil jwtUtil;
private final PasswordEncoder passwordEncoder;

@Override
public UserResDTO.LoginDTO login(UserReqDTO.@Valid LoginDTO dto) {
// 이메일로 사용자 조회
User user = userRepository.findByEmail(dto.email())
.orElseThrow(() -> new UserException(UserErrorCode.NOT_FOUND));

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

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

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

// DTO 조립
return UserConverter.toLoginDTO(user, accessToken);
}
}
93 changes: 93 additions & 0 deletions src/main/java/ssu/cromi/umc9th/global/auth/entity/JwtUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package ssu.cromi.umc9th.global.auth.entity;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import ssu.cromi.umc9th.global.auth.service.CustomUserDetails;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.stream.Collectors;

@Component
public class JwtUtil {

private final SecretKey secretKey;
private final Duration accessExpiration;

public JwtUtil(
@Value("${jwt.token.secretKey}") String secret,
@Value("${jwt.token.expiration.access}") Long accessExpiration
) {
this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
this.accessExpiration = Duration.ofMillis(accessExpiration);
}

// AccessToken 생성
public String createAccessToken(CustomUserDetails user) {
return createToken(user, accessExpiration);
}

/** 토큰에서 이메일 가져오기
*
* @param token 유저 정보를 추출할 토큰
* @return 유저 이메일을 토큰에서 추출합니다
*/
public String getEmail(String token) {
try {
return getClaims(token).getPayload().getSubject(); // Parsing해서 Subject 가져오기
} catch (JwtException e) {
return null;
}
}

/** 토큰 유효성 확인
*
* @param token 유효한지 확인할 토큰
* @return True, False 반환합니다
*/
public boolean isValid(String token) {
try {
getClaims(token);
return true;
} catch (JwtException e) {
return false;
}
}

// 토큰 생성
private String createToken(CustomUserDetails user, Duration expiration) {
Instant now = Instant.now();

// 인가 정보
String authorities = user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));

return Jwts.builder()
.subject(user.getUsername()) // User 이메일을 Subject로
.claim("role", authorities)
.claim("email", user.getUsername())
.issuedAt(Date.from(now)) // 언제 발급한지
.expiration(Date.from(now.plus(expiration))) // 언제까지 유효한지
.signWith(secretKey) // sign할 Key
.compact();
}

// 토큰 정보 가져오기
private Jws<Claims> getClaims(String token) throws JwtException {
return Jwts.parser()
.verifyWith(secretKey)
.clockSkewSeconds(60)
.build()
.parseSignedClaims(token);
}
}
5 changes: 5 additions & 0 deletions src/main/java/ssu/cromi/umc9th/global/auth/enums/Role.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ssu.cromi.umc9th.global.auth.enums;

public enum Role {
ROLE_ADMIN, ROLE_USER
}
Loading