diff --git a/mintori/Umc10th/build.gradle b/mintori/Umc10th/build.gradle index 669d118..9baed77 100644 --- a/mintori/Umc10th/build.gradle +++ b/mintori/Umc10th/build.gradle @@ -30,8 +30,17 @@ dependencies { testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' - // Swagger (현재 버전 유지) + // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1' + + // Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' } tasks.named('test') { diff --git a/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/controller/UserController.java b/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/controller/UserController.java index 5a35f89..b37351e 100644 --- a/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/controller/UserController.java +++ b/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/controller/UserController.java @@ -1,7 +1,9 @@ package com.example.umc10th.domain.user.controller; +import com.example.umc10th.domain.user.dto.request.LoginRequest; import com.example.umc10th.domain.user.dto.request.UserRequest; import com.example.umc10th.domain.user.dto.response.MyPageResponse; +import com.example.umc10th.domain.user.dto.response.TokenResponse; import com.example.umc10th.domain.user.dto.response.UserResponse; import com.example.umc10th.domain.user.service.UserService; import com.example.umc10th.global.apiPayload.ApiResponse; @@ -27,6 +29,11 @@ public ApiResponse signUp(@Valid @RequestBody UserRequest request) return ApiResponse.onSuccess(GeneralSuccessCode.CREATED, userService.signUp(request)); } + @PostMapping("/login") + public ApiResponse login(@Valid @RequestBody LoginRequest request) { + return ApiResponse.onSuccess(GeneralSuccessCode.OK, userService.login(request)); + } + @GetMapping("/{userId}/mypage") public ApiResponse getMyPage(@PathVariable Long userId) { return ApiResponse.onSuccess(GeneralSuccessCode.OK, userService.getMyPage(userId)); diff --git a/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/converter/UserConverter.java b/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/converter/UserConverter.java index 31604bf..fdc0ff5 100644 --- a/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/converter/UserConverter.java +++ b/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/converter/UserConverter.java @@ -10,11 +10,11 @@ public class UserConverter { private UserConverter() { } - public static User toUser(UserRequest request) { + public static User toUser(UserRequest request, String encodedPassword) { return User.builder() .name(request.name()) .email(request.email()) - .password(request.password()) + .password(encodedPassword) .gender(request.gender()) .address(request.address()) .point(0L) diff --git a/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/dto/request/LoginRequest.java b/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/dto/request/LoginRequest.java new file mode 100644 index 0000000..e9add9a --- /dev/null +++ b/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/dto/request/LoginRequest.java @@ -0,0 +1,14 @@ +package com.example.umc10th.domain.user.dto.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record LoginRequest( + @NotBlank(message = "이메일을 입력해주세요") + @Email + String email, + + @NotBlank(message = "비밀번호를 입력해주세요") + String password +) { +} diff --git a/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/dto/response/TokenResponse.java b/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/dto/response/TokenResponse.java new file mode 100644 index 0000000..d659967 --- /dev/null +++ b/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/dto/response/TokenResponse.java @@ -0,0 +1,9 @@ +package com.example.umc10th.domain.user.dto.response; + +public record TokenResponse( + Long userId, + String tokenType, + String accessToken, + Long expiresIn +) { +} diff --git a/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/exception/code/UserErrorCode.java b/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/exception/code/UserErrorCode.java index 0ce52ff..85c3876 100644 --- a/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/exception/code/UserErrorCode.java +++ b/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/exception/code/UserErrorCode.java @@ -12,6 +12,7 @@ public enum UserErrorCode implements BaseErrorCode { USER_NOT_FOUND("USER_404", "존재하지 않는 유저입니다.", HttpStatus.NOT_FOUND), DUPLICATE_EMAIL("USER_409", "이미 가입된 이메일입니다.", HttpStatus.CONFLICT), INVALID_USER_REQUEST("USER_400", "유저 요청이 올바르지 않습니다.", HttpStatus.BAD_REQUEST), + LOGIN_FAILED("USER_401", "이메일 또는 비밀번호가 올바르지 않습니다.", HttpStatus.UNAUTHORIZED), ; private final String code; diff --git a/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/service/UserService.java b/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/service/UserService.java index b316519..646024f 100644 --- a/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/service/UserService.java +++ b/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/service/UserService.java @@ -1,12 +1,16 @@ package com.example.umc10th.domain.user.service; +import com.example.umc10th.domain.user.dto.request.LoginRequest; import com.example.umc10th.domain.user.dto.request.UserRequest; import com.example.umc10th.domain.user.dto.response.MyPageResponse; +import com.example.umc10th.domain.user.dto.response.TokenResponse; import com.example.umc10th.domain.user.dto.response.UserResponse; public interface UserService { UserResponse signUp(UserRequest request); + TokenResponse login(LoginRequest request); + MyPageResponse getMyPage(Long userId); } diff --git a/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/service/UserServiceImpl.java b/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/service/UserServiceImpl.java index 146145b..a761fee 100644 --- a/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/service/UserServiceImpl.java +++ b/mintori/Umc10th/src/main/java/com/example/umc10th/domain/user/service/UserServiceImpl.java @@ -4,14 +4,23 @@ import com.example.umc10th.domain.mission.repository.UserMissionRepository; import com.example.umc10th.domain.review.repository.ReviewRepository; import com.example.umc10th.domain.user.converter.UserConverter; +import com.example.umc10th.domain.user.dto.request.LoginRequest; import com.example.umc10th.domain.user.dto.request.UserRequest; import com.example.umc10th.domain.user.dto.response.MyPageResponse; +import com.example.umc10th.domain.user.dto.response.TokenResponse; import com.example.umc10th.domain.user.dto.response.UserResponse; import com.example.umc10th.domain.user.entity.User; import com.example.umc10th.domain.user.exception.UserException; import com.example.umc10th.domain.user.exception.code.UserErrorCode; import com.example.umc10th.domain.user.repository.UserRepository; +import com.example.umc10th.global.jwt.JwtTokenProvider; +import com.example.umc10th.global.security.CustomUserDetails; 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.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,6 +32,9 @@ public class UserServiceImpl implements UserService { private final UserRepository userRepository; private final UserMissionRepository userMissionRepository; private final ReviewRepository reviewRepository; + private final PasswordEncoder passwordEncoder; + private final AuthenticationManager authenticationManager; + private final JwtTokenProvider jwtTokenProvider; @Override @Transactional @@ -30,11 +42,38 @@ public UserResponse signUp(UserRequest request) { if (userRepository.existsByEmail(request.email())) { throw new UserException(UserErrorCode.DUPLICATE_EMAIL); } - User user = UserConverter.toUser(request); + // BCrypt 로 비밀번호를 솔트와 함께 해시 처리 + String encodedPassword = passwordEncoder.encode(request.password()); + User user = UserConverter.toUser(request, encodedPassword); User saved = userRepository.save(user); return UserConverter.toUserResponse(saved); } + @Override + public TokenResponse login(LoginRequest request) { + // AuthenticationManager 가 CustomUserDetailsService + PasswordEncoder 로 자격 증명을 검증 + Authentication authentication; + try { + authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.email(), request.password()) + ); + } catch (AuthenticationException e) { + // 사용자 없음 / 비밀번호 불일치 등 모든 인증 실패를 동일하게 처리 + throw new UserException(UserErrorCode.LOGIN_FAILED); + } + + CustomUserDetails principal = (CustomUserDetails) authentication.getPrincipal(); + String accessToken = jwtTokenProvider.createAccessToken( + principal.getUserId(), principal.getUsername()); + + return new TokenResponse( + principal.getUserId(), + "Bearer", + accessToken, + jwtTokenProvider.getAccessTokenValidityInSeconds() + ); + } + @Override public MyPageResponse getMyPage(Long userId) { User user = userRepository.findById(userId) diff --git a/mintori/Umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/mintori/Umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java new file mode 100644 index 0000000..65afaa8 --- /dev/null +++ b/mintori/Umc10th/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -0,0 +1,91 @@ +package com.example.umc10th.global.config; + +import com.example.umc10th.global.jwt.JwtAuthenticationFilter; +import com.example.umc10th.global.jwt.JwtTokenProvider; +import com.example.umc10th.global.security.CustomAccessDeniedHandler; +import com.example.umc10th.global.security.CustomAuthenticationEntryPoint; +import com.example.umc10th.global.security.CustomUserDetailsService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + + private final CustomAuthenticationEntryPoint authenticationEntryPoint; + private final CustomAccessDeniedHandler accessDeniedHandler; + private final CustomUserDetailsService userDetailsService; + private final JwtTokenProvider jwtTokenProvider; + + private static final String[] PUBLIC_ENDPOINTS = { + "/swagger-ui/**", + "/swagger-ui.html", + "/v3/api-docs/**", + "/error", + }; + + @Bean + public PasswordEncoder passwordEncoder() { + // BCrypt: 해시할 때마다 salt 를 자동 생성해 해시 문자열에 함께 저장 + return new BCryptPasswordEncoder(); + } + + /** + * 로그인(이메일/비밀번호) 인증을 처리하는 AuthenticationManager. + * - CustomUserDetailsService 로 사용자를 로드하고 PasswordEncoder 로 비밀번호를 비교 + */ + @Bean + public AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder) { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService); + provider.setPasswordEncoder(passwordEncoder); + return new ProviderManager(provider); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + // REST API 이므로 CSRF / 폼 로그인 / HTTP Basic 비활성화 + .csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + // JWT 기반이므로 세션을 사용하지 않는 STATELESS 정책 + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + // 인가 설정: 회원가입 / 로그인만 Public, 그 외 모든 API 는 Private + .authorizeHttpRequests(auth -> auth + .requestMatchers(PUBLIC_ENDPOINTS).permitAll() + // 회원가입 API (Public) + .requestMatchers(HttpMethod.POST, "/api/users/signup").permitAll() + // 로그인 API (Public) + .requestMatchers(HttpMethod.POST, "/api/users/login").permitAll() + // 그 외 모든 요청은 인증 필요 (Private) + .anyRequest().authenticated() + ) + // 인증/인가 실패 시 통일된 응답 형식으로 반환 + .exceptionHandling(handler -> handler + // 인증 실패 (미인증 사용자) -> 401 + .authenticationEntryPoint(authenticationEntryPoint) + // 인가 실패 (권한 없음) -> 403 + .accessDeniedHandler(accessDeniedHandler) + ) + // UsernamePasswordAuthenticationFilter 앞에 JWT 인증 필터 추가 + .addFilterBefore( + new JwtAuthenticationFilter(jwtTokenProvider, userDetailsService), + UsernamePasswordAuthenticationFilter.class + ); + + return http.build(); + } +} diff --git a/mintori/Umc10th/src/main/java/com/example/umc10th/global/jwt/JwtAuthenticationFilter.java b/mintori/Umc10th/src/main/java/com/example/umc10th/global/jwt/JwtAuthenticationFilter.java new file mode 100644 index 0000000..cc45731 --- /dev/null +++ b/mintori/Umc10th/src/main/java/com/example/umc10th/global/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,68 @@ +package com.example.umc10th.global.jwt; + +import com.example.umc10th.global.security.CustomUserDetailsService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +/** + * 매 요청마다 Authorization 헤더의 JWT 를 검증해 SecurityContext 에 인증 정보를 등록한다. + * 토큰이 없거나 유효하지 않으면 인증을 설정하지 않고 통과시키며, + * 이후 Private API 라면 SecurityConfig 의 AuthenticationEntryPoint 가 401 응답을 반환한다. + */ +@Slf4j +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String BEARER_PREFIX = "Bearer "; + + private final JwtTokenProvider jwtTokenProvider; + private final CustomUserDetailsService userDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + try { + String token = resolveToken(request); + if (token != null && jwtTokenProvider.validateToken(token)) { + String email = jwtTokenProvider.getEmail(token); + UserDetails userDetails = userDetailsService.loadUserByUsername(email); + + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + authentication.setDetails( + new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception e) { + // 토큰 처리 중 문제가 생기면 인증을 설정하지 않고 진행 -> EntryPoint 가 401 응답 + SecurityContextHolder.clearContext(); + log.warn("JWT 인증 처리 실패: {}", e.getMessage()); + } + + filterChain.doFilter(request, response); + } + + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(AUTHORIZATION_HEADER); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) { + return bearerToken.substring(BEARER_PREFIX.length()); + } + return null; + } +} diff --git a/mintori/Umc10th/src/main/java/com/example/umc10th/global/jwt/JwtTokenProvider.java b/mintori/Umc10th/src/main/java/com/example/umc10th/global/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..53de820 --- /dev/null +++ b/mintori/Umc10th/src/main/java/com/example/umc10th/global/jwt/JwtTokenProvider.java @@ -0,0 +1,83 @@ +package com.example.umc10th.global.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +/** + * JWT(Access Token) 생성 및 검증 담당. + */ +@Slf4j +@Component +public class JwtTokenProvider { + + private final SecretKey key; + private final long accessTokenValidityInSeconds; + + public JwtTokenProvider( + @Value("${jwt.secret}") String secret, + @Value("${jwt.access-token-validity-in-seconds}") long accessTokenValidityInSeconds + ) { + this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + this.accessTokenValidityInSeconds = accessTokenValidityInSeconds; + } + + /** 로그인 성공 시 Access Token 발급 */ + public String createAccessToken(Long userId, String email) { + Date now = new Date(); + Date expiry = new Date(now.getTime() + accessTokenValidityInSeconds * 1000); + + return Jwts.builder() + .subject(email) // 토큰 주체(username = email) + .claim("userId", userId) + .issuedAt(now) + .expiration(expiry) + .signWith(key) + .compact(); + } + + /** 토큰 서명/만료 검증 */ + public boolean validateToken(String token) { + try { + Jwts.parser() + .verifyWith(key) + .build() + .parseSignedClaims(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + log.warn("유효하지 않은 JWT 입니다: {}", e.getMessage()); + return false; + } + } + + /** 토큰에서 email(subject) 추출 */ + public String getEmail(String token) { + return parseClaims(token).getSubject(); + } + + /** 토큰에서 userId 추출 */ + public Long getUserId(String token) { + Object userId = parseClaims(token).get("userId"); + return userId == null ? null : Long.valueOf(userId.toString()); + } + + public long getAccessTokenValidityInSeconds() { + return accessTokenValidityInSeconds; + } + + private Claims parseClaims(String token) { + return Jwts.parser() + .verifyWith(key) + .build() + .parseSignedClaims(token) + .getPayload(); + } +} diff --git a/mintori/Umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDeniedHandler.java b/mintori/Umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDeniedHandler.java new file mode 100644 index 0000000..97133d0 --- /dev/null +++ b/mintori/Umc10th/src/main/java/com/example/umc10th/global/security/CustomAccessDeniedHandler.java @@ -0,0 +1,39 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@Slf4j +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void handle(HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException { + log.warn("[인가 실패] {} {} - {}", + request.getMethod(), request.getRequestURI(), accessDeniedException.getMessage()); + + GeneralErrorCode errorCode = GeneralErrorCode.FORBIDDEN; + + response.setStatus(errorCode.getStatus().value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.getWriter().write( + objectMapper.writeValueAsString(ApiResponse.onFailure(errorCode, null)) + ); + } +} diff --git a/mintori/Umc10th/src/main/java/com/example/umc10th/global/security/CustomAuthenticationEntryPoint.java b/mintori/Umc10th/src/main/java/com/example/umc10th/global/security/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..d36ea08 --- /dev/null +++ b/mintori/Umc10th/src/main/java/com/example/umc10th/global/security/CustomAuthenticationEntryPoint.java @@ -0,0 +1,39 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@Slf4j +@Component +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public void commence(HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException) throws IOException { + log.warn("[인증 실패] {} {} - {}", + request.getMethod(), request.getRequestURI(), authException.getMessage()); + + GeneralErrorCode errorCode = GeneralErrorCode.UNAUTHORIZED; + + response.setStatus(errorCode.getStatus().value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + response.getWriter().write( + objectMapper.writeValueAsString(ApiResponse.onFailure(errorCode, null)) + ); + } +} diff --git a/mintori/Umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetails.java b/mintori/Umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetails.java new file mode 100644 index 0000000..0c6fc16 --- /dev/null +++ b/mintori/Umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetails.java @@ -0,0 +1,56 @@ +package com.example.umc10th.global.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@RequiredArgsConstructor +public class CustomUserDetails implements UserDetails { + + private final Long userId; + private final String email; + private final String password; + + public Long getUserId() { + return userId; + } + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority("ROLE_USER")); + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return email; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/mintori/Umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java b/mintori/Umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java new file mode 100644 index 0000000..e95f2d3 --- /dev/null +++ b/mintori/Umc10th/src/main/java/com/example/umc10th/global/security/CustomUserDetailsService.java @@ -0,0 +1,27 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.domain.user.entity.User; +import com.example.umc10th.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; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + @Override + @Transactional(readOnly = true) + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + User user = userRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException( + "해당 이메일의 사용자를 찾을 수 없습니다: " + email)); + + return new CustomUserDetails(user.getId(), user.getEmail(), user.getPassword()); + } +} diff --git a/mintori/Umc10th/src/main/resources/application.yml b/mintori/Umc10th/src/main/resources/application.yml index deedb28..e175518 100644 --- a/mintori/Umc10th/src/main/resources/application.yml +++ b/mintori/Umc10th/src/main/resources/application.yml @@ -17,3 +17,7 @@ spring: properties: hibernate: format_sql: true + +jwt: + secret: ${JWT_SECRET:umc10th-default-dev-secret-key-change-me-please-1234567890} + access-token-validity-in-seconds: 3600