diff --git a/build.gradle b/build.gradle index a1dc7ce5..26a81784 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,12 @@ dependencies { // 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') { diff --git a/src/main/java/umc/domain/home/controller/HomeController.java b/src/main/java/umc/domain/home/controller/HomeController.java index 0c6bfa8e..d8197ef5 100644 --- a/src/main/java/umc/domain/home/controller/HomeController.java +++ b/src/main/java/umc/domain/home/controller/HomeController.java @@ -1,14 +1,15 @@ package umc.domain.home.controller; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import umc.domain.home.dto.HomeResDTO; import umc.domain.home.service.HomeService; import umc.domain.member.exception.code.MemberSuccessCode; import umc.global.apiPayload.ApiResponse; +import umc.global.security.entity.AuthMember; @RestController @Validated @@ -19,12 +20,12 @@ public class HomeController { private final HomeService homeService; //홈 화면 - @GetMapping("/v1/home/{memberId}") + @GetMapping("/v1/home") public ApiResponse getHome( @RequestParam(name = "region") @NotBlank String region, - @PathVariable @NotNull Long memberId + @AuthenticationPrincipal AuthMember member ){ - return ApiResponse.onSuccess(MemberSuccessCode.HOME_VIEW_SUCCESS, homeService.getHome(memberId, region)); + return ApiResponse.onSuccess(MemberSuccessCode.HOME_VIEW_SUCCESS, homeService.getHome(member, region)); } } diff --git a/src/main/java/umc/domain/home/service/HomeService.java b/src/main/java/umc/domain/home/service/HomeService.java index 7fdf01b1..9242292a 100644 --- a/src/main/java/umc/domain/home/service/HomeService.java +++ b/src/main/java/umc/domain/home/service/HomeService.java @@ -6,9 +6,7 @@ import umc.domain.home.converter.HomeConverter; import umc.domain.home.dto.HomeResDTO; 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.domain.mission.entity.Mission; import umc.domain.mission.repository.MemberMissionRepository; import umc.domain.mission.repository.MissionRepository; @@ -16,7 +14,7 @@ import umc.domain.store.exception.StoreException; import umc.domain.store.exception.code.RegionErrorCode; import umc.domain.store.repository.RegionRepository; -import umc.domain.store.repository.StoreRepository; +import umc.global.security.entity.AuthMember; import java.util.List; @@ -24,24 +22,20 @@ @Service public class HomeService { - private final MemberRepository memberRepository; private final MissionRepository missionRepository; - private final StoreRepository storeRepository; private final RegionRepository regionRepository; private final MemberMissionRepository memberMissionRepository; @Transactional(readOnly = true) - public HomeResDTO.HomeDTO getHome(Long memberId, String region) { - Member member = memberRepository.findById(memberId).orElseThrow( - ()->new MemberException(MemberErrorCode.MEMBER_NOT_FOUND) - ); + public HomeResDTO.HomeDTO getHome(AuthMember authMember, String region) { + Member member = authMember.getMember(); Region r = regionRepository.findByName(region).orElseThrow( ()-> new StoreException(RegionErrorCode.REGION_NOT_FOUND) ); Integer missionCount = missionRepository.countByRegion(r); - Integer missionSuccessCount = memberMissionRepository.countCompletedMissionsByRegion(memberId, r); + Integer missionSuccessCount = memberMissionRepository.countCompletedMissionsByRegion(member.getId(), r); List unstartedMissionList = missionRepository.findUnstartedMissions(r); Integer currentPoint = member.getPoint(); diff --git a/src/main/java/umc/domain/member/controller/MemberController.java b/src/main/java/umc/domain/member/controller/MemberController.java index 8ee8287d..3b490282 100644 --- a/src/main/java/umc/domain/member/controller/MemberController.java +++ b/src/main/java/umc/domain/member/controller/MemberController.java @@ -2,6 +2,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import umc.domain.member.dto.MemberReqDTO; import umc.domain.member.dto.MemberResDTO; @@ -10,6 +11,7 @@ import umc.global.apiPayload.ApiResponse; import umc.global.apiPayload.code.BaseSuccessCode; import umc.global.apiPayload.code.GeneralSuccessCode; +import umc.global.security.entity.AuthMember; @RestController @RequiredArgsConstructor @@ -20,18 +22,18 @@ public class MemberController { //마이 페이지 @PostMapping("/v1/members/me") public ApiResponse getInfo( - @RequestBody @Valid MemberReqDTO.MyPageReqDTO dto - ){ + @AuthenticationPrincipal AuthMember member + ){ BaseSuccessCode code = MemberSuccessCode.MEMBER_SUCCESS; - return ApiResponse.onSuccess(code, memberService.getInfo(dto)); + return ApiResponse.onSuccess(code, memberService.getInfo(member)); } //보유 포인트 조회 - @GetMapping("/v1/members/me/points/{memberId}") + @GetMapping("/v1/members/me/points") public ApiResponse getPoint( - @RequestParam(name = "memberId") @Valid Long id + @AuthenticationPrincipal AuthMember member ){ - return ApiResponse.onSuccess(GeneralSuccessCode.OK, memberService.getPoint(id)); + return ApiResponse.onSuccess(GeneralSuccessCode.OK, memberService.getPoint(member)); } //회원 가입 @@ -41,4 +43,12 @@ public ApiResponse signup( ){ return ApiResponse.onSuccess(MemberSuccessCode.CREATED, memberService.signUp(dto)); } + + //로그인 + @PostMapping("v1/auth/members/login") + public ApiResponse login( + @RequestBody @Valid MemberReqDTO.LoginReq dto + ){ + return ApiResponse.onSuccess(MemberSuccessCode.MEMBER_SUCCESS, memberService.login(dto)); + } } diff --git a/src/main/java/umc/domain/member/dto/MemberReqDTO.java b/src/main/java/umc/domain/member/dto/MemberReqDTO.java index 0b2cc94e..c7be5b3b 100644 --- a/src/main/java/umc/domain/member/dto/MemberReqDTO.java +++ b/src/main/java/umc/domain/member/dto/MemberReqDTO.java @@ -1,8 +1,6 @@ package umc.domain.member.dto; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Past; +import jakarta.validation.constraints.*; import umc.domain.member.enums.Gender; import java.time.LocalDate; @@ -10,13 +8,18 @@ public class MemberReqDTO { - public record MyPageReqDTO( - @NotNull - Long id - ){} + // id를 body로 받아서 조회 할 떄 사용 +// public record MyPageReqDTO( +// @NotNull +// Long id +// ){} public record SignUpReq( - List agreedTermsIds, + @NotBlank + @Email + String email, + @NotBlank + String password, @NotBlank String name, @NotNull @@ -26,11 +29,17 @@ public record SignUpReq( LocalDate birth, @NotBlank String address, + @NotEmpty + List agreedTermsIds, List userFood, + String phoneNumber + ){} + + public record LoginReq( @NotBlank + @Email String email, @NotBlank - String password, - String phoneNumber + String password ){} } diff --git a/src/main/java/umc/domain/member/dto/MemberResDTO.java b/src/main/java/umc/domain/member/dto/MemberResDTO.java index 99a6bcf5..0ddccf8c 100644 --- a/src/main/java/umc/domain/member/dto/MemberResDTO.java +++ b/src/main/java/umc/domain/member/dto/MemberResDTO.java @@ -28,4 +28,10 @@ public record SignUpRes( Long memberId, LocalDateTime createdAt ){} + + //로그인 + @Builder + public record LoginRes( + String accessToken + ){} } diff --git a/src/main/java/umc/domain/member/repository/TermRepository.java b/src/main/java/umc/domain/member/repository/TermRepository.java index 11a29327..19c18033 100644 --- a/src/main/java/umc/domain/member/repository/TermRepository.java +++ b/src/main/java/umc/domain/member/repository/TermRepository.java @@ -7,7 +7,7 @@ import java.util.List; public interface TermRepository extends JpaRepository { - List findAllByRequired(boolean isRequired); + List findAllByIsRequired(boolean isRequired); @Query(""" select t.id diff --git a/src/main/java/umc/domain/member/service/MemberService.java b/src/main/java/umc/domain/member/service/MemberService.java index 0940c7bc..a15b5674 100644 --- a/src/main/java/umc/domain/member/service/MemberService.java +++ b/src/main/java/umc/domain/member/service/MemberService.java @@ -2,6 +2,10 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,6 +24,8 @@ import umc.domain.member.exception.code.MemberErrorCode; import umc.domain.member.exception.code.TermErrorCode; import umc.domain.member.repository.*; +import umc.global.security.entity.AuthMember; +import umc.global.security.util.JwtUtil; import java.util.List; @@ -33,24 +39,17 @@ public class MemberService { private final MemberTermRepository memberTermRepository; private final FoodRepository foodRepository; private final MemberFoodRepository memberFoodRepository; + private final AuthenticationManager authenticationManager; + private final JwtUtil jwtUtil; @Transactional(readOnly = true) - public MemberResDTO.MyPageResDTO getInfo(MemberReqDTO.MyPageReqDTO dto) { - Long memberId = dto.id(); - Member member = memberRepository.findById(memberId).orElseThrow( - ()->new MemberException(MemberErrorCode.MEMBER_NOT_FOUND) - ); - - return MemberConverter.toGetInfo(member); + public MemberResDTO.MyPageResDTO getInfo(AuthMember member) { + return MemberConverter.toGetInfo(member.getMember()); } @Transactional(readOnly = true) - public MemberResDTO.PointResDTO getPoint(Long id) { - Member member = memberRepository.findById(id).orElseThrow( - ()->new MemberException(MemberErrorCode.MEMBER_NOT_FOUND) - ); - - return MemberConverter.toGetPoint(member); + public MemberResDTO.PointResDTO getPoint(AuthMember member) { + return MemberConverter.toGetPoint(member.getMember()); } @Transactional @@ -73,7 +72,7 @@ public MemberResDTO.SignUpRes signUp(MemberReqDTO.@Valid SignUpReq dto) { } //필수 정책 id 목록 - List requiredTermId = termRepository.findAllByRequired(true) + List requiredTermId = termRepository.findAllByIsRequired(true) .stream() .map(Term::getId) .toList(); @@ -128,4 +127,28 @@ public MemberResDTO.SignUpRes signUp(MemberReqDTO.@Valid SignUpReq dto) { return MemberConverter.toSignUpRes(savedMember); } + + public MemberResDTO.LoginRes login(MemberReqDTO.@Valid LoginReq dto) { + try{ + + //인증 과정을 authenticationManager에게 넘김 + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + dto.email(), + dto.password() + ) + ); + + AuthMember authMember = (AuthMember) authentication.getPrincipal(); + + String token = jwtUtil.createAccessToken(authMember); + + return MemberResDTO.LoginRes.builder() + .accessToken(token) + .build(); + + } catch (AuthenticationException e){ + throw new MemberException(MemberErrorCode.MEMBER_NOT_FOUND); + } + } } diff --git a/src/main/java/umc/domain/mission/controller/MissionController.java b/src/main/java/umc/domain/mission/controller/MissionController.java index 71e466bb..075e77ad 100644 --- a/src/main/java/umc/domain/mission/controller/MissionController.java +++ b/src/main/java/umc/domain/mission/controller/MissionController.java @@ -3,6 +3,7 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import umc.domain.mission.dto.MissionReqDTO; @@ -11,6 +12,7 @@ import umc.domain.mission.service.MissionService; import umc.global.apiPayload.ApiResponse; import umc.global.dto.PageResDTO; +import umc.global.security.entity.AuthMember; @RestController @RequiredArgsConstructor @@ -19,18 +21,18 @@ public class MissionController { private final MissionService missionService; - @GetMapping("/v1/missions/me/{memberId}") + @GetMapping("/v1/missions/me") public ApiResponse> getMissions( + @AuthenticationPrincipal AuthMember member, @RequestParam boolean isCompleted, @RequestParam Integer pageSize, @RequestParam Integer pageNumber, - @RequestParam(required = false) String sort, - @PathVariable Long memberId + @RequestParam(required = false) String sort ){ - return ApiResponse.onSuccess(MissionSuccessCode.MISSION_LIST_GET_SUCCESS, missionService.getMissions(memberId, isCompleted, pageSize, pageNumber, sort)); + return ApiResponse.onSuccess(MissionSuccessCode.MISSION_LIST_GET_SUCCESS, missionService.getMissions(member, isCompleted, pageSize, pageNumber, sort)); } - @PatchMapping("/v1/missions/{missionId}") + @PatchMapping("/v1/missions") public ApiResponse updateMission( @PathVariable @NotNull Long missionId, @RequestBody @Valid MissionReqDTO.MissionStatusUpdate req diff --git a/src/main/java/umc/domain/mission/service/MissionService.java b/src/main/java/umc/domain/mission/service/MissionService.java index 097e4053..14d2953a 100644 --- a/src/main/java/umc/domain/mission/service/MissionService.java +++ b/src/main/java/umc/domain/mission/service/MissionService.java @@ -15,6 +15,7 @@ import umc.domain.mission.entity.mapping.MemberMission; import umc.domain.mission.repository.MemberMissionRepository; import umc.global.dto.PageResDTO; +import umc.global.security.entity.AuthMember; @Service @@ -26,7 +27,7 @@ public class MissionService { @Transactional(readOnly = true) public PageResDTO.Pagination getMissions( - Long memberId, + AuthMember authMember, boolean isCompleted, Integer pageSize, Integer pageNumber, @@ -42,9 +43,7 @@ public PageResDTO.Pagination getMissions( PageRequest pageRequest = PageRequest.of(pageNumber, pageSize, sortInfo); - Member member = memberRepository.findById(memberId).orElseThrow( - ()->new MemberException(MemberErrorCode.MEMBER_NOT_FOUND) - ); + Member member = authMember.getMember(); Page missionList = memberMissionRepository.findAllByMemberAndIsCompleted(member, isCompleted, pageRequest); diff --git a/src/main/java/umc/domain/review/controller/ReviewController.java b/src/main/java/umc/domain/review/controller/ReviewController.java index 20a76fb2..081ea1d8 100644 --- a/src/main/java/umc/domain/review/controller/ReviewController.java +++ b/src/main/java/umc/domain/review/controller/ReviewController.java @@ -3,6 +3,7 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import umc.domain.review.dto.ReviewReqDTO; import umc.domain.review.dto.ReviewResDTO; @@ -11,6 +12,7 @@ import umc.domain.review.service.ReviewService; import umc.global.apiPayload.ApiResponse; import umc.global.dto.CusorResDTO; +import umc.global.security.entity.AuthMember; @RestController @RequiredArgsConstructor @@ -20,20 +22,23 @@ public class ReviewController { private final ReviewService reviewService; //리뷰 작성 - @PostMapping("/v1/reviews/{storeId}/{memberId}") + @PostMapping("/v1/reviews/{storeId}") public ApiResponse createReview( - @PathVariable @NotNull Long memberId, + @AuthenticationPrincipal AuthMember member, @PathVariable @NotNull Long storeId, @RequestBody @Valid ReviewReqDTO.CreateReviewReq req ){ - return ApiResponse.onSuccess(ReviewSuccessCode.REVIEW_CREATED, reviewService.createReview(memberId, storeId, req)); + return ApiResponse.onSuccess(ReviewSuccessCode.REVIEW_CREATED, reviewService.createReview(member, storeId, req)); } @GetMapping("/v1/reviews") public ApiResponse> getMyReviews( - @RequestBody @Valid ReviewReqDTO.MyReview req + @AuthenticationPrincipal AuthMember member, + @RequestParam(name = "cursor", defaultValue = "-1") String cursor, + @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, + @RequestParam(name = "sortType", defaultValue = "id") String sortType ){ - return ApiResponse.onSuccess(ReviewSuccessCode.REVIEW_FOUND, reviewService.getMyReviews(req)); + return ApiResponse.onSuccess(ReviewSuccessCode.REVIEW_FOUND, reviewService.getMyReviews(member, cursor, pageSize, sortType)); } } diff --git a/src/main/java/umc/domain/review/dto/ReviewReqDTO.java b/src/main/java/umc/domain/review/dto/ReviewReqDTO.java index db034926..8dcae57b 100644 --- a/src/main/java/umc/domain/review/dto/ReviewReqDTO.java +++ b/src/main/java/umc/domain/review/dto/ReviewReqDTO.java @@ -19,8 +19,6 @@ public record CreateReviewReq( ){} public record MyReview( - @NotNull - Long id, String cursor, Integer pageSize, String sort diff --git a/src/main/java/umc/domain/review/service/ReviewService.java b/src/main/java/umc/domain/review/service/ReviewService.java index 9b42e98d..49161c3f 100644 --- a/src/main/java/umc/domain/review/service/ReviewService.java +++ b/src/main/java/umc/domain/review/service/ReviewService.java @@ -21,20 +21,18 @@ import umc.domain.store.exception.code.StoreErrorCode; import umc.domain.store.repository.StoreRepository; import umc.global.dto.CusorResDTO; +import umc.global.security.entity.AuthMember; @Service @AllArgsConstructor public class ReviewService { private final ReviewRepository reviewRepository; - private final MemberRepository memberRepository; private final StoreRepository storeRepository; @Transactional - public ReviewResDTO.CreateReviewRes createReview(Long memberId, Long storeId, ReviewReqDTO.CreateReviewReq req) { - Member member = memberRepository.findById(memberId).orElseThrow( - ()->new MemberException(MemberErrorCode.MEMBER_NOT_FOUND) - ); + public ReviewResDTO.CreateReviewRes createReview(AuthMember authMember, Long storeId, ReviewReqDTO.CreateReviewReq req) { + Member member = authMember.getMember(); Store store = storeRepository.findById(storeId).orElseThrow( ()->new StoreException(StoreErrorCode.STORE_NOT_FOUND) @@ -48,23 +46,22 @@ public ReviewResDTO.CreateReviewRes createReview(Long memberId, Long storeId, Re return ReviewConverter.toCreateReviewRes(saved); } - public CusorResDTO.Pagination getMyReviews(ReviewReqDTO.MyReview req) { + public CusorResDTO.Pagination getMyReviews(AuthMember authMember, String cursor, Integer pageSize, String sort) { - PageRequest pageRequest = PageRequest.of(0, req.pageSize()); + PageRequest pageRequest = PageRequest.of(0, pageSize); Slice reviewList; String nextCursor=null; Long idCursor; - Member member = memberRepository.findById(req.id()) - .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + Member member = authMember.getMember(); - String sortType = req.sort()==null?"id":req.sort().toLowerCase(); + String sortType = sort==null?"id":sort.toLowerCase(); //cursor = id : rating - if (!"-1".equals(req.cursor())) { + if (!"-1".equals(cursor)) { - String[] cursorSplit = req.cursor().split(":"); + String[] cursorSplit = cursor.split(":"); switch (sortType) { @@ -129,4 +126,5 @@ public CusorResDTO.Pagination getMyReviews(ReviewReqDTO. , nextCursor ); } + } diff --git a/src/main/java/umc/global/config/SecurityConfig.java b/src/main/java/umc/global/config/SecurityConfig.java index b2d06997..0b84bf7c 100644 --- a/src/main/java/umc/global/config/SecurityConfig.java +++ b/src/main/java/umc/global/config/SecurityConfig.java @@ -1,19 +1,30 @@ package umc.global.config; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 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.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import umc.global.security.exception.CustomAccessDenied; import umc.global.security.exception.CustomEntryPoint; +import umc.global.security.filter.JwtAuthFilter; +import umc.global.security.service.CustomUserDetailsService; +import umc.global.security.util.JwtUtil; @EnableWebSecurity @Configuration +@RequiredArgsConstructor public class SecurityConfig { + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + private final String[] allowUris = { // Swagger 허용 "/swagger-ui/**", @@ -30,10 +41,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers(allowUris).permitAll() .anyRequest().authenticated() ) - .formLogin(form -> form - .defaultSuccessUrl("/swagger-ui/index.html", true) - .permitAll() - ) + .formLogin(AbstractHttpConfigurer::disable) + .sessionManagement(AbstractHttpConfigurer::disable) + .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") @@ -60,4 +70,16 @@ public CustomAccessDenied customAccessDenied(){ public CustomEntryPoint customEntryPoint(){ return new CustomEntryPoint(); } + + @Bean + public JwtAuthFilter jwtAuthFilter(){ + return new JwtAuthFilter(jwtUtil, customUserDetailsService); + } + + @Bean + public AuthenticationManager authenticationManager( + AuthenticationConfiguration configuration + )throws Exception{ + return configuration.getAuthenticationManager(); + } } diff --git a/src/main/java/umc/global/security/filter/JwtAuthFilter.java b/src/main/java/umc/global/security/filter/JwtAuthFilter.java new file mode 100644 index 00000000..70268fc5 --- /dev/null +++ b/src/main/java/umc/global/security/filter/JwtAuthFilter.java @@ -0,0 +1,66 @@ +package umc.global.security.filter; + +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 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.BaseErrorCode; +import umc.global.apiPayload.code.GeneralErrorCode; +import umc.global.security.service.CustomUserDetailsService; +import umc.global.security.util.JwtUtil; + +import java.io.IOException; + +@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"); + if (token == null || !token.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + token = token.replace("Bearer ", ""); + if (jwtUtil.isValid(token)) { + String email = jwtUtil.getEmail(token); + UserDetails user = customUserDetailsService.loadUserByUsername(email); + Authentication auth = new UsernamePasswordAuthenticationToken( + user, + null, + user.getAuthorities() + ); + SecurityContextHolder.getContext().setAuthentication(auth); + } + filterChain.doFilter(request, response); + } catch (Exception e) { + ObjectMapper mapper = new ObjectMapper(); + BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED; + + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + ApiResponse errorResponse = ApiResponse.onFailure(code,null); + + mapper.writeValue(response.getOutputStream(), errorResponse); + } + } +} diff --git a/src/main/java/umc/global/security/util/JwtUtil.java b/src/main/java/umc/global/security/util/JwtUtil.java new file mode 100644 index 00000000..694770cc --- /dev/null +++ b/src/main/java/umc/global/security/util/JwtUtil.java @@ -0,0 +1,76 @@ +package umc.global.security.util; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; +import umc.global.security.entity.AuthMember; + +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); + } + + public String createAccessToken(AuthMember member){ + return createToken(member, accessExpiration); + } + + private String createToken(AuthMember member, Duration accessExpiration) { + Instant now = Instant.now(); + + String authorities = member.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + return Jwts.builder() + .subject(member.getUsername()) + .claim("role", authorities) + .claim("email", member.getUsername()) + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plus(accessExpiration))) + .signWith(secretKey) + .compact(); + } + + public String getEmail (String token){ + try{ + return getClaims(token).getPayload().getSubject(); + } catch (JwtException e){ + return null; + } + } + + private Jws getClaims(String token) throws JwtException { + return Jwts.parser() + .verifyWith(secretKey) // 서명 검증 키 설정 + .clockSkewSeconds(60) // 최대 60초 시계 오차 허용 + .build() + .parseSignedClaims(token); // 파싱 + 검증 실행 + } + + public boolean isValid(String token){ + try{ + getClaims(token); + return true; + } catch (JwtException e){ + return false; + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e70ca3b3..967a4aa9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,19 +1,25 @@ spring: application: - name: "umc" # "umc10th" + name: "umc" datasource: - driver-class-name: com.mysql.cj.jdbc.Driver # MySQL JDBC ???? ??? ?? - url: ${DB_URL} # jdbc:mysql://localhost:3306/{???????} - username: ${DB_USER} # MySQL ?? ?? - password: ${DB_PW} # MySQL ???? + driver-class-name: com.mysql.cj.jdbc.Driver + url: ${DB_URL} + username: ${DB_USER} + password: ${DB_PW} jpa: - database: mysql # ??? ?????? ?? ?? (MySQL) - database-platform: org.hibernate.dialect.MySQLDialect # Hibernate?? ??? MySQL ??(dialect) ?? - show-sql: true # ??? SQL ??? ??? ???? ?? ?? + database: mysql + database-platform: org.hibernate.dialect.MySQLDialect + show-sql: true hibernate: - ddl-auto: update # ?????? ?? ? ?????? ???? ??? ?? + ddl-auto: update properties: hibernate: - format_sql: true # ???? SQL ??? ?? ?? ??? \ No newline at end of file + format_sql: true + +jwt: + token: + secretKey: ${JWT_SECRET_KEY} + expiration: + access: 1800000 # 30분 \ No newline at end of file