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 @@ -46,6 +46,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 @@ -4,7 +4,9 @@
import com.example.UMC9th.domain.user.dto.UserResDTO;
import com.example.UMC9th.domain.user.exception.code.UserSuccessCode;
import com.example.UMC9th.domain.user.service.command.UserCommandService;
import com.example.UMC9th.domain.user.service.query.UserQueryService;
import com.example.UMC9th.global.apiPayload.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -15,6 +17,7 @@
public class UserController {

private final UserCommandService userCommandService;
private final UserQueryService userQueryService;


//회원가입
Expand All @@ -24,4 +27,12 @@ public ApiResponse <UserResDTO.JoinDTO> signUp(
){
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 @@ -21,6 +21,9 @@ public static User toUser(
){
return User.builder()
.name(dto.name())
.email(dto.email())
.password(password)
.role(role)
.birth(dto.birth().toString())
.address(dto.address())
.detailAddress(dto.specAddress())
Expand Down
14 changes: 13 additions & 1 deletion src/main/java/com/example/UMC9th/domain/user/dto/UserReqDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.example.UMC9th.domain.store.enums.Address;
import com.example.UMC9th.domain.user.enums.Gender;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

import java.time.LocalDate;
import java.util.List;
Expand All @@ -13,7 +15,17 @@ public record JoinDTO(
LocalDate birth,
Address address,
String specAddress,
@Email
String email, // 추가된 속성
@NotBlank
String password, // 추가된 속성
List<Long> preferCategory
){}

// 로그인
public record LoginDTO(
@NotBlank
String email,
@NotBlank
String password
){}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,11 @@ public record JoinDTO(
Long memberId,
LocalDateTime createdAt
){}

// 로그인
@Builder
public record LoginDTO(
Long memberId,
String accessToken
){}
}
9 changes: 9 additions & 0 deletions src/main/java/com/example/UMC9th/domain/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.example.UMC9th.domain.store.enums.Address;
import com.example.UMC9th.global.auth.enums.SocialType;
import com.example.UMC9th.global.auth.entity.BaseEntity;
import com.example.UMC9th.global.auth.enums.Role;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
Expand Down Expand Up @@ -69,6 +70,14 @@ public class User extends BaseEntity{
@Enumerated(EnumType.STRING)
private SocialType socialType;

@Column(nullable = false, unique = true)
private String email;

@Column(nullable = false)
private String password;

@Enumerated(EnumType.STRING)
private Role role;


//연관관계
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.lang.reflect.Member;
import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {

Optional<Member> findByEmail(String email);

@Query("SELECT DISTINCT u FROM User u LEFT JOIN FETCH u.missionList WHERE u.userId = :userId")
User finduserIdWithMissionList(@Param("userId") Long usesrId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.example.UMC9th.domain.user.service.Detail;

import com.example.UMC9th.domain.user.entity.User;
import com.example.UMC9th.domain.user.exception.UserException;
import com.example.UMC9th.domain.user.exception.code.UserErrorCode;
import com.example.UMC9th.domain.user.repository.UserRepository;
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 UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(
String username
) throws UsernameNotFoundException {
// 검증할 Member 조회
User user = userRepository.findByEmail(username)
.orElseThrow(() -> new UserException(UserErrorCode.NOT_FOUND));
// CustomUserDetails 반환
return new CustomUserDetails(user);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.example.UMC9th.domain.user.service.Detail;

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

import java.lang.reflect.Member;
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
import com.example.UMC9th.domain.user.repository.FoodRepository;
import com.example.UMC9th.domain.user.repository.UserFoodRepository;

import com.example.UMC9th.global.auth.enums.Role;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -27,6 +29,7 @@ public class UserCommandServiceImpl implements UserCommandService {
private final UserRepository userRepository;
private final UserFoodRepository userFoodRepository;
private final FoodRepository foodRepository;
private final PassworddEncoder passwordEncoder;

//회원가입
@Override
Expand All @@ -35,7 +38,10 @@ public UserResDTO.JoinDTO signup(
UserReqDTO.JoinDTO dto
){
// 사용자 생성
User user = UserConverter.toUser(dto);
String salt = passwordEncoder.encode(dto.password());

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
@@ -1,4 +1,9 @@
package com.example.UMC9th.domain.user.service.query;

import com.example.UMC9th.domain.user.dto.UserReqDTO;
import com.example.UMC9th.domain.user.dto.UserResDTO;
import jakarta.validation.Valid;

public interface UserQueryService {
UserResDTO.LoginDTO login(UserReqDTO.@Valid LoginDTO dto);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,48 @@
package com.example.UMC9th.domain.user.service.query;

public class UserQueryServiceImpl {
}
import com.example.UMC9th.domain.user.converter.UserConverter;
import com.example.UMC9th.domain.user.dto.UserReqDTO;
import com.example.UMC9th.domain.user.dto.UserResDTO;
import com.example.UMC9th.domain.user.entity.User;
import com.example.UMC9th.domain.user.exception.UserException;
import com.example.UMC9th.domain.user.exception.code.UserErrorCode;
import com.example.UMC9th.domain.user.repository.UserRepository;
import com.example.UMC9th.domain.user.service.Detail.CustomUserDetails;
import com.example.UMC9th.global.auth.entity.JwtUtil;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserQueryServiceImpl implements UserQueryService{

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

@Override
public UserResDTO.LoginDTO login(
UserReqDTO.@Valid LoginDTO dto
) {

// Member 조회
User user = userRepository.findByEmail(dto.email())
.orElseThrow(() -> new UserException(UserErrorCode.NOT_FOUND));

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

// 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/com/example/UMC9th/global/auth/entity/JwtUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.example.UMC9th.global.auth.entity;

import com.example.UMC9th.domain.user.service.Detail.CustomUserDetails;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.Value;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

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/com/example/UMC9th/global/auth/enums/Role.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example.UMC9th.global.auth.enums;

public enum Role {
ROLE_ADMIN, ROLE_USER
}
Loading