diff --git a/build.gradle b/build.gradle index 641d052..90509d2 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,16 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation "org.springframework.boot:spring-boot-starter-webflux" + // 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' + // QueryDSl implementation "io.github.openfeign.querydsl:querydsl-jpa:7.0" implementation "io.github.openfeign.querydsl:querydsl-core:7.0" @@ -39,9 +49,9 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' + //Lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' - testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/umc/domain/member/controller/MemberController.java b/src/main/java/umc/domain/member/controller/MemberController.java index aa3a5db..92546de 100644 --- a/src/main/java/umc/domain/member/controller/MemberController.java +++ b/src/main/java/umc/domain/member/controller/MemberController.java @@ -7,6 +7,7 @@ import umc.domain.member.dto.MemberResDTO; import umc.domain.member.exception.code.MemberSuccessCode; import umc.domain.member.service.common.MemberCommandService; +import umc.domain.member.service.query.MemberQueryService; import umc.global.apiPayload.ApiResponse; import umc.global.apiPayload.code.GeneralSuccessCode; @@ -16,7 +17,7 @@ public class MemberController { private final MemberCommandService memberCommandService; - + private final MemberQueryService memberQueryService; @DeleteMapping("/{memberId}/hard") ApiResponse hardDelete( @@ -33,7 +34,15 @@ ApiResponse hardDelete( @PostMapping("/sign-up") ApiResponse signUp( - @RequestBody @Valid MemberReqDTO.JoinDTO dto) { + @RequestBody @Valid MemberReqDTO.JoinDTO dto + ) { return ApiResponse.onSuccess(MemberSuccessCode.CREATED, memberCommandService.signUp(dto)); } + + @PostMapping("/login") + ApiResponse login( + @RequestBody @Valid MemberReqDTO.LoginDTO dto + ) { + return ApiResponse.onSuccess(MemberSuccessCode.FOUND, memberQueryService.login(dto)); + } } diff --git a/src/main/java/umc/domain/member/converter/MemberConverter.java b/src/main/java/umc/domain/member/converter/MemberConverter.java index 78bb1cb..f370d90 100644 --- a/src/main/java/umc/domain/member/converter/MemberConverter.java +++ b/src/main/java/umc/domain/member/converter/MemberConverter.java @@ -5,6 +5,7 @@ import umc.domain.member.entity.Member; import umc.domain.member.entity.MemberStatus; import umc.domain.member.entity.MemberType; +import umc.global.security.CustomUserDetails; public class MemberConverter { @@ -17,20 +18,34 @@ public static MemberResDTO.JoinDTO toJoinDTO( .build(); } + public static MemberResDTO.LoginDTO toLoginDTO( + Member member, + String accessToken + ){ + return MemberResDTO.LoginDTO.builder() + .memberId(member.getId()) + .accessToken(accessToken) + .build(); + } + public static Member toMember( - MemberReqDTO.JoinDTO dto + MemberReqDTO.JoinDTO dto, + String password, + MemberType memberType ){ return Member.builder() .name(dto.name()) + .email(dto.email()) + .password(password) .gender(dto.gender()) .birth(dto.birth()) .address(dto.address()) - .email(dto.email()) .point(0) - .memberType(MemberType.USER) + .memberType(memberType) .phoneNumber(dto.phoneNumber()) .memberStatus(MemberStatus.ACTIVE) .build(); } + } diff --git a/src/main/java/umc/domain/member/dto/MemberReqDTO.java b/src/main/java/umc/domain/member/dto/MemberReqDTO.java index c016ba7..3e5aa60 100644 --- a/src/main/java/umc/domain/member/dto/MemberReqDTO.java +++ b/src/main/java/umc/domain/member/dto/MemberReqDTO.java @@ -45,6 +45,13 @@ public record JoinDTO( List preferredFoods ) {} + public record LoginDTO( + @NotBlank + String email, + @NotBlank + String password + ){} + public record AgreementDTO( Long policyId, boolean agreed diff --git a/src/main/java/umc/domain/member/dto/MemberResDTO.java b/src/main/java/umc/domain/member/dto/MemberResDTO.java index 4d9b442..2b38e56 100644 --- a/src/main/java/umc/domain/member/dto/MemberResDTO.java +++ b/src/main/java/umc/domain/member/dto/MemberResDTO.java @@ -12,4 +12,10 @@ public record JoinDTO( LocalDateTime createAt ) { } + + @Builder + public record LoginDTO( + Long memberId, + String accessToken + ){} } diff --git a/src/main/java/umc/domain/member/entity/Member.java b/src/main/java/umc/domain/member/entity/Member.java index 566eb7c..c385ebd 100644 --- a/src/main/java/umc/domain/member/entity/Member.java +++ b/src/main/java/umc/domain/member/entity/Member.java @@ -43,6 +43,9 @@ public class Member extends BaseEntity { @Column(nullable = false, length = 100) private String email; + @Column(nullable = false) + private String password; + @Column(nullable = false) private Integer point; @@ -86,6 +89,7 @@ public Member( LocalDate birth, @NonNull String address, @NonNull String email, + @NonNull String password, Integer point, SocialType socialType, String socialUid, @@ -98,6 +102,7 @@ public Member( this.birth = birth; this.address = address; this.email = email; + this.password = password; this.point = (point != null) ? point : 0; this.socialType = socialType; this.socialUid = socialUid; diff --git a/src/main/java/umc/domain/member/exception/code/MemberErrorCode.java b/src/main/java/umc/domain/member/exception/code/MemberErrorCode.java index 0989728..d5e548c 100644 --- a/src/main/java/umc/domain/member/exception/code/MemberErrorCode.java +++ b/src/main/java/umc/domain/member/exception/code/MemberErrorCode.java @@ -9,8 +9,21 @@ @AllArgsConstructor public enum MemberErrorCode implements BaseErrorCode { - NOT_FOUND_MEMBER(HttpStatus.NOT_FOUND, "MEMBER404_1", "존재하지 않는 회원입니다."), - NOT_OWNER(HttpStatus.FORBIDDEN, "MEMBER403_1", "해당 회원은 OWNER 권한이 아닙니다."); + NOT_FOUND_MEMBER( + HttpStatus.NOT_FOUND, + "MEMBER404_1", + "존재하지 않는 회원입니다."), + + NOT_OWNER( + HttpStatus.FORBIDDEN, + "MEMBER403_1", + "해당 회원은 OWNER 권한이 아닙니다."), + + INVALID_PASSWORD( + HttpStatus.UNAUTHORIZED, + "MEMBER401_1", + "비밀번호가 올바르지 않습니다." + ); private final HttpStatus status; private final String code; diff --git a/src/main/java/umc/domain/member/repository/MemberRepository.java b/src/main/java/umc/domain/member/repository/MemberRepository.java index fd60ef5..e5ab30e 100644 --- a/src/main/java/umc/domain/member/repository/MemberRepository.java +++ b/src/main/java/umc/domain/member/repository/MemberRepository.java @@ -36,4 +36,5 @@ public interface MemberRepository extends JpaRepository { @Query("delete from Member m where m.id = :id") int hardDeleteById(@Param("id") Long id); + Optional findByEmail(String email); } diff --git a/src/main/java/umc/domain/member/service/common/MemberCommandServiceImpl.java b/src/main/java/umc/domain/member/service/common/MemberCommandServiceImpl.java index 5d10234..706f22f 100644 --- a/src/main/java/umc/domain/member/service/common/MemberCommandServiceImpl.java +++ b/src/main/java/umc/domain/member/service/common/MemberCommandServiceImpl.java @@ -7,6 +7,7 @@ import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import umc.domain.inquiry.repository.InquiryRepository; @@ -15,6 +16,7 @@ import umc.domain.member.dto.MemberResDTO; import umc.domain.member.entity.Food; import umc.domain.member.entity.Member; +import umc.domain.member.entity.MemberType; import umc.domain.member.entity.Policy; import umc.domain.member.entity.PolicyType; import umc.domain.member.entity.mapping.MemberFood; @@ -49,18 +51,15 @@ public class MemberCommandServiceImpl implements MemberCommandService { private final PolicyRepository policyRepository; private final MemberPolicyRepository memberPolicyRepository; - @Transactional - @Override - public MemberResDTO.JoinDTO signUp( - MemberReqDTO.JoinDTO dto - ) { + private final PasswordEncoder passwordEncoder; - Member member = MemberConverter.toMember(dto); - memberRepository.save(member); + @Transactional + @Override + public MemberResDTO.JoinDTO signUp(MemberReqDTO.JoinDTO dto) { - // 약관 동의 처리 - if(dto.agreements()== null || dto.agreements().isEmpty()){ + // 1) 약관 동의 처리 (검증 먼저) + if (dto.agreements() == null || dto.agreements().isEmpty()) { throw new PolicyException(PolicyErrorCode.REQUIRED_POLICY_NOT_ACCEPTED); } @@ -84,6 +83,24 @@ public MemberResDTO.JoinDTO signUp( throw new PolicyException(PolicyErrorCode.REQUIRED_POLICY_NOT_ACCEPTED); } + // 2) 선호 음식 검증도 save 전에 수행 + List foods = List.of(); + List foodIds = dto.preferredFoods(); + + if (foodIds != null && !foodIds.isEmpty()) { + foods = foodRepository.findAllById(foodIds); + + if (foods.size() != foodIds.size()) { + throw new FoodException(FoodErrorCode.NOT_FOUND_FOOD); + } + } + + // 3) 모든 검증 끝난 후 회원 저장 + String salt = passwordEncoder.encode(dto.password()); + Member member = MemberConverter.toMember(dto, salt, MemberType.USER); + memberRepository.save(member); + + // 4) 약관 매핑 저장 List memberPolicies = policies.stream() .map(policy -> MemberPolicy.builder() .member(member) @@ -94,17 +111,8 @@ public MemberResDTO.JoinDTO signUp( memberPolicyRepository.saveAll(memberPolicies); - - // 선호 음식 매핑 - if (dto.preferredFoods() != null && !dto.preferredFoods().isEmpty()) { - - List foodIds = dto.preferredFoods(); - List foods = foodRepository.findAllById(foodIds); - - if (foods.size() != foodIds.size()) { - throw new FoodException(FoodErrorCode.NOT_FOUND_FOOD); - } - + // 5) 선호 음식 매핑 저장 (검증 때 조회한 foods 재사용) + if (!foods.isEmpty()) { List memberFoods = foods.stream() .map(food -> MemberFood.builder() .member(member) diff --git a/src/main/java/umc/domain/member/service/query/MemberQueryService.java b/src/main/java/umc/domain/member/service/query/MemberQueryService.java index 7d3d810..fa7deac 100644 --- a/src/main/java/umc/domain/member/service/query/MemberQueryService.java +++ b/src/main/java/umc/domain/member/service/query/MemberQueryService.java @@ -1,7 +1,12 @@ package umc.domain.member.service.query; +import jakarta.validation.Valid; import umc.domain.member.dto.GetMemberResponse; +import umc.domain.member.dto.MemberReqDTO; +import umc.domain.member.dto.MemberResDTO.LoginDTO; public interface MemberQueryService { GetMemberResponse getMember(Long memberId); + + LoginDTO login(MemberReqDTO.@Valid LoginDTO dto); } diff --git a/src/main/java/umc/domain/member/service/query/MemberQueryServiceImpl.java b/src/main/java/umc/domain/member/service/query/MemberQueryServiceImpl.java index 8f4164b..2481faf 100644 --- a/src/main/java/umc/domain/member/service/query/MemberQueryServiceImpl.java +++ b/src/main/java/umc/domain/member/service/query/MemberQueryServiceImpl.java @@ -1,10 +1,21 @@ package umc.domain.member.service.query; +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 umc.domain.member.converter.MemberConverter; import umc.domain.member.dto.GetMemberResponse; +import umc.domain.member.dto.MemberReqDTO; +import umc.domain.member.dto.MemberResDTO; +import umc.domain.member.dto.MemberResDTO.LoginDTO; +import umc.domain.member.entity.Member; +import umc.domain.member.exception.MemberException; +import umc.domain.member.exception.code.MemberErrorCode; import umc.domain.member.repository.MemberRepository; +import umc.global.security.CustomUserDetails; +import umc.global.security.JwtUtil; @Transactional(readOnly = true) @Service @@ -12,10 +23,29 @@ public class MemberQueryServiceImpl implements MemberQueryService{ private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + private final JwtUtil jwtUtil; public GetMemberResponse getMember(Long memberId) { return memberRepository.findMemberInfo(memberId); } + @Override + public MemberResDTO.LoginDTO login(MemberReqDTO.@Valid LoginDTO dto) { + + Member member = memberRepository.findByEmail(dto.email()) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND_MEMBER)); + + if (!passwordEncoder.matches(dto.password(), member.getPassword())){ + throw new MemberException(MemberErrorCode.INVALID_PASSWORD); + } + + CustomUserDetails userDetails = new CustomUserDetails(member); + + String accessToken = jwtUtil.createAccessToken(userDetails); + + return MemberConverter.toLoginDTO(member, accessToken); + } + } diff --git a/src/main/java/umc/global/config/SecurityConfig.java b/src/main/java/umc/global/config/SecurityConfig.java new file mode 100644 index 0000000..008f179 --- /dev/null +++ b/src/main/java/umc/global/config/SecurityConfig.java @@ -0,0 +1,75 @@ +package umc.global.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import umc.global.security.AuthenticationEntryPointImpl; +import umc.global.security.CustomUserDetailsService; +import umc.global.security.JwtAuthFilter; +import umc.global.security.JwtUtil; + +@EnableWebSecurity +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + + private final String[] allowUris = { + "/api/members/sign-up", + "/api/members/login", + // Swagger 허용 + "/swagger-ui/**", + "/swagger-resources/**", + "/v3/api-docs/**", + }; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(requests -> requests + .requestMatchers(allowUris).permitAll() + .requestMatchers("/admin/**").hasRole("ADMIN") + .anyRequest().authenticated() + ) + // 폼로그인 비활성화 + .formLogin(AbstractHttpConfigurer::disable) + // JwtAuthFilter를 UsernamePasswordAuthenticationFilter + .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class) + .csrf(AbstractHttpConfigurer::disable) + .logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/login?logout") + .permitAll() + ) + .exceptionHandling(exception -> exception.authenticationEntryPoint(authenticationEntryPoint())) + + ; + + return http.build(); + } + + @Bean + public JwtAuthFilter jwtAuthFilter() { + return new JwtAuthFilter(jwtUtil, customUserDetailsService); + } + + @Bean + public PasswordEncoder passwordEncoder(){ + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationEntryPoint authenticationEntryPoint() { + return new AuthenticationEntryPointImpl(); + } +} \ No newline at end of file diff --git a/src/main/java/umc/global/security/AuthenticationEntryPointImpl.java b/src/main/java/umc/global/security/AuthenticationEntryPointImpl.java new file mode 100644 index 0000000..efcbf52 --- /dev/null +++ b/src/main/java/umc/global/security/AuthenticationEntryPointImpl.java @@ -0,0 +1,32 @@ +package umc.global.security; + +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; +import umc.global.apiPayload.ApiResponse; +import umc.global.apiPayload.code.GeneralErrorCode; + +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 errorResponse = ApiResponse.onFailure( + GeneralErrorCode.UNAUTHORIZED, + null + ); + + objectMapper.writeValue(response.getOutputStream(), errorResponse); + } +} \ No newline at end of file diff --git a/src/main/java/umc/global/security/CustomUserDetails.java b/src/main/java/umc/global/security/CustomUserDetails.java new file mode 100644 index 0000000..e56c035 --- /dev/null +++ b/src/main/java/umc/global/security/CustomUserDetails.java @@ -0,0 +1,29 @@ +package umc.global.security; + +import java.util.Collection; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import umc.domain.member.entity.Member; + +@RequiredArgsConstructor +public class CustomUserDetails implements UserDetails { + + private final Member member; + + @Override + public Collection getAuthorities() { + return List.of(() -> "ROLE_" + member.getMemberType().name()); + } + + @Override + public String getPassword() { + return member.getPassword(); + } + + @Override + public String getUsername() { + return member.getEmail(); + } +} \ No newline at end of file diff --git a/src/main/java/umc/global/security/CustomUserDetailsService.java b/src/main/java/umc/global/security/CustomUserDetailsService.java new file mode 100644 index 0000000..e1443e8 --- /dev/null +++ b/src/main/java/umc/global/security/CustomUserDetailsService.java @@ -0,0 +1,29 @@ +package umc.global.security; + +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; +import umc.domain.member.entity.Member; +import umc.domain.member.exception.MemberException; +import umc.domain.member.exception.code.MemberErrorCode; +import umc.domain.member.repository.MemberRepository; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername( + String username + ) throws UsernameNotFoundException { + // 검증할 Member 조회 + Member member = memberRepository.findByEmail(username) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND_MEMBER)); + // CustomUserDetails 반환 + return new CustomUserDetails(member); + } +} diff --git a/src/main/java/umc/global/security/JwtAuthFilter.java b/src/main/java/umc/global/security/JwtAuthFilter.java new file mode 100644 index 0000000..5335f43 --- /dev/null +++ b/src/main/java/umc/global/security/JwtAuthFilter.java @@ -0,0 +1,70 @@ +package umc.global.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.filter.OncePerRequestFilter; +import umc.global.apiPayload.ApiResponse; +import umc.global.apiPayload.code.GeneralErrorCode; + +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + + try { + // 토큰 가져오기 + String token = request.getHeader("Authorization"); + // token이 없거나 Bearer가 아니면 넘기기 + if (token == null || !token.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + // Bearer이면 추출 + token = token.replace("Bearer ", ""); + // AccessToken 검증하기: 올바른 토큰이면 + if (jwtUtil.isValid(token)) { + // 토큰에서 이메일 추출 + String email = jwtUtil.getEmail(token); + // 인증 객체 생성: 이메일로 찾아온 뒤, 인증 객체 생성 + UserDetails user = customUserDetailsService.loadUserByUsername(email); + Authentication auth = new UsernamePasswordAuthenticationToken( + user, + null, + user.getAuthorities() + ); + // 인증 완료 후 SecurityContextHolder에 넣기 + SecurityContextHolder.getContext().setAuthentication(auth); + } + filterChain.doFilter(request, response); + } catch (Exception e) { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + ApiResponse errorResponse = ApiResponse.onFailure( + GeneralErrorCode.UNAUTHORIZED, + null + ); + + ObjectMapper mapper = new ObjectMapper(); + mapper.writeValue(response.getOutputStream(), errorResponse); + } + } +} \ No newline at end of file diff --git a/src/main/java/umc/global/security/JwtUtil.java b/src/main/java/umc/global/security/JwtUtil.java new file mode 100644 index 0000000..c8642a6 --- /dev/null +++ b/src/main/java/umc/global/security/JwtUtil.java @@ -0,0 +1,91 @@ +package umc.global.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.stream.Collectors; +import javax.crypto.SecretKey; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +@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 getClaims(String token) throws JwtException { + return Jwts.parser() + .verifyWith(secretKey) + .clockSkewSeconds(60) + .build() + .parseSignedClaims(token); + } +} \ No newline at end of file diff --git a/src/main/java/umc/global/validator/GenderValidator.java b/src/main/java/umc/global/validator/GenderValidator.java index b460a15..a4c6cd9 100644 --- a/src/main/java/umc/global/validator/GenderValidator.java +++ b/src/main/java/umc/global/validator/GenderValidator.java @@ -2,36 +2,39 @@ import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; -import java.util.Arrays; +import java.util.EnumSet; import java.util.Set; import java.util.stream.Collectors; import umc.domain.member.entity.MemberGender; import umc.global.annotation.ValidGender; -public class GenderValidator implements ConstraintValidator { +public class GenderValidator implements ConstraintValidator { - private Set allowedValues; + private Set allowedValues; @Override public void initialize(ValidGender constraintAnnotation) { - allowedValues = Arrays.stream(MemberGender.values()) - .map(Enum::name) - .collect(Collectors.toSet()); + allowedValues = EnumSet.allOf(MemberGender.class); } @Override - public boolean isValid(String value, ConstraintValidatorContext context) { - if (value == null) return true; + public boolean isValid(MemberGender value, ConstraintValidatorContext context) { + if (value == null) { + return true; // 필수면 false로 바꾸거나 @NotNull 추가 + } boolean valid = allowedValues.contains(value); if (!valid) { context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate( - "성별은 " + String.join(", ", allowedValues) + " 중 하나여야 합니다." + "성별은 " + allowedValues.stream() + .map(Enum::name) + .collect(Collectors.joining(", ")) + + " 중 하나여야 합니다." ).addConstraintViolation(); } return valid; } -} \ No newline at end of file +}