diff --git a/build.gradle b/build.gradle index 93f642e..10977da 100644 --- a/build.gradle +++ b/build.gradle @@ -45,6 +45,16 @@ dependencies { //Validation implementation 'org.springframework.boot:spring-boot-starter-validation' + + // Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + + // Jwt + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' + implementation 'org.springframework.boot:spring-boot-configuration-processor' } tasks.named('test') { diff --git a/src/main/java/ssu/cromi/umc9th/domain/user/controller/UserController.java b/src/main/java/ssu/cromi/umc9th/domain/user/controller/UserController.java index c6e5c22..03de0e1 100644 --- a/src/main/java/ssu/cromi/umc9th/domain/user/controller/UserController.java +++ b/src/main/java/ssu/cromi/umc9th/domain/user/controller/UserController.java @@ -9,6 +9,7 @@ import ssu.cromi.umc9th.domain.user.dto.UserResDTO; import ssu.cromi.umc9th.domain.user.exception.code.UserSuccessCode; import ssu.cromi.umc9th.domain.user.service.UserCommandService; +import ssu.cromi.umc9th.domain.user.service.UserQueryService; import ssu.cromi.umc9th.global.apiPayload.ApiResponse; @RestController @@ -16,6 +17,8 @@ public class UserController { private final UserCommandService userCommandService; + private final UserQueryService userQueryService; + @PostMapping("/sign-up") public ApiResponse signUp( @@ -23,4 +26,11 @@ public ApiResponse signUp( ){ return ApiResponse.onSuccess(UserSuccessCode.FOUND, userCommandService.signup(dto)); } + + @PostMapping("/login") + public ApiResponse login( + @RequestBody @Valid UserReqDTO.LoginDTO dto + ){ + return ApiResponse.onSuccess(UserSuccessCode.FOUND, userQueryService.login(dto)); + } } diff --git a/src/main/java/ssu/cromi/umc9th/domain/user/converter/UserConverter.java b/src/main/java/ssu/cromi/umc9th/domain/user/converter/UserConverter.java index c78dd81..1d6d89a 100644 --- a/src/main/java/ssu/cromi/umc9th/domain/user/converter/UserConverter.java +++ b/src/main/java/ssu/cromi/umc9th/domain/user/converter/UserConverter.java @@ -3,6 +3,7 @@ import ssu.cromi.umc9th.domain.user.dto.UserReqDTO; import ssu.cromi.umc9th.domain.user.dto.UserResDTO; import ssu.cromi.umc9th.domain.user.entity.User; +import ssu.cromi.umc9th.global.auth.enums.Role; import java.lang.reflect.Member; @@ -19,14 +20,26 @@ public static UserResDTO.JoinDTO toJoinDTO( //DTO -> Entity public static User toUser( - UserReqDTO.JoinDTO dto + UserReqDTO.JoinDTO dto, + String password, + Role role ){ return User.builder() .nickname(dto.nickname()) + .email(dto.email()) + .password(password) + .role(role) .birthday(dto.birthday()) .address(dto.address()) .specAddress(dto.specAddress()) .gender(dto.gender()) .build(); } + + public static UserResDTO.LoginDTO toLoginDTO(User user, String accessToken) { + return UserResDTO.LoginDTO.builder() + .userId(user.getUserId()) + .accessToken(accessToken) + .build(); + } } diff --git a/src/main/java/ssu/cromi/umc9th/domain/user/dto/UserReqDTO.java b/src/main/java/ssu/cromi/umc9th/domain/user/dto/UserReqDTO.java index ee5b158..9266ba8 100644 --- a/src/main/java/ssu/cromi/umc9th/domain/user/dto/UserReqDTO.java +++ b/src/main/java/ssu/cromi/umc9th/domain/user/dto/UserReqDTO.java @@ -1,5 +1,8 @@ package ssu.cromi.umc9th.domain.user.dto; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import ssu.cromi.umc9th.domain.user.enums.Gender; import ssu.cromi.umc9th.global.annotation.ExistFoods; @@ -8,12 +11,27 @@ public class UserReqDTO { public record JoinDTO( + @NotBlank String nickname, + @Email + String email, + @NotBlank + String password, + @NotNull Gender gender, + @NotNull LocalDate birthday, + @NotNull String address, + @NotNull String specAddress, @ExistFoods List preferCategory ){} + public record LoginDTO( + @NotBlank + String email, + @NotBlank + String password + ){} } diff --git a/src/main/java/ssu/cromi/umc9th/domain/user/dto/UserResDTO.java b/src/main/java/ssu/cromi/umc9th/domain/user/dto/UserResDTO.java index 0078d9c..116eddd 100644 --- a/src/main/java/ssu/cromi/umc9th/domain/user/dto/UserResDTO.java +++ b/src/main/java/ssu/cromi/umc9th/domain/user/dto/UserResDTO.java @@ -11,4 +11,10 @@ public record JoinDTO( Long userId, LocalDateTime createdAt ){} + // 로그인 + @Builder + public record LoginDTO( + Long userId, + String accessToken + ){} } diff --git a/src/main/java/ssu/cromi/umc9th/domain/user/entity/User.java b/src/main/java/ssu/cromi/umc9th/domain/user/entity/User.java index d275aff..bace87f 100644 --- a/src/main/java/ssu/cromi/umc9th/domain/user/entity/User.java +++ b/src/main/java/ssu/cromi/umc9th/domain/user/entity/User.java @@ -10,6 +10,7 @@ import ssu.cromi.umc9th.domain.review.entity.UserReview; import ssu.cromi.umc9th.domain.user.enums.Gender; import ssu.cromi.umc9th.domain.user.enums.UserStatus; +import ssu.cromi.umc9th.global.auth.enums.Role; import ssu.cromi.umc9th.global.entity.BaseEntity; import java.time.LocalDate; @@ -46,6 +47,12 @@ public class User extends BaseEntity { @Column(nullable = true, length = 255, unique = true) private String email; + @Column(nullable = false) + private String password; + + @Enumerated(EnumType.STRING) + private Role role; + @Column(nullable = true, length = 255) private String phone; diff --git a/src/main/java/ssu/cromi/umc9th/domain/user/repository/UserRepository.java b/src/main/java/ssu/cromi/umc9th/domain/user/repository/UserRepository.java index e89fc22..1aefac3 100644 --- a/src/main/java/ssu/cromi/umc9th/domain/user/repository/UserRepository.java +++ b/src/main/java/ssu/cromi/umc9th/domain/user/repository/UserRepository.java @@ -6,6 +6,8 @@ import ssu.cromi.umc9th.domain.user.entity.User; import ssu.cromi.umc9th.domain.user.dto.UserProfileDto; +import java.util.Optional; + public interface UserRepository extends JpaRepository { // 이메일 조회 시 fetch join 추가 @Query(""" @@ -13,7 +15,7 @@ public interface UserRepository extends JpaRepository { FROM User u WHERE u.email = :email """) - User findByEmail(@Param("email") String email); + Optional findByEmail(@Param("email") String email); @Query(""" SELECT new ssu.cromi.umc9th.domain.user.dto.UserProfileDto( diff --git a/src/main/java/ssu/cromi/umc9th/domain/user/service/UserCommandServiceImpl.java b/src/main/java/ssu/cromi/umc9th/domain/user/service/UserCommandServiceImpl.java index 3a3002b..9690b10 100644 --- a/src/main/java/ssu/cromi/umc9th/domain/user/service/UserCommandServiceImpl.java +++ b/src/main/java/ssu/cromi/umc9th/domain/user/service/UserCommandServiceImpl.java @@ -1,9 +1,9 @@ package ssu.cromi.umc9th.domain.user.service; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import ssu.cromi.umc9th.domain.food.entity.FoodCategory; import ssu.cromi.umc9th.domain.food.entity.UserFood; import ssu.cromi.umc9th.domain.food.exception.FoodException.FoodException; import ssu.cromi.umc9th.domain.food.exception.code.FoodErrorCode; @@ -14,8 +14,8 @@ import ssu.cromi.umc9th.domain.user.dto.UserResDTO; import ssu.cromi.umc9th.domain.user.entity.User; import ssu.cromi.umc9th.domain.user.repository.UserRepository; +import ssu.cromi.umc9th.global.auth.enums.Role; -import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -26,14 +26,19 @@ public class UserCommandServiceImpl implements UserCommandService{ private final FoodRepository foodRepository; private final UserFoodRepository userFoodRepository; + // Password Encoder + private final PasswordEncoder passwordEncoder; + //회원가입 @Override @Transactional public UserResDTO.JoinDTO signup( UserReqDTO.JoinDTO dto ) { + // 솔트된 비밀번호 생성 + String salt = passwordEncoder.encode(dto.password()); //사용자 생성 - User user = UserConverter.toUser(dto); + User user = UserConverter.toUser(dto,salt, Role.ROLE_USER); // DB적용 userRepository.save(user); diff --git a/src/main/java/ssu/cromi/umc9th/domain/user/service/UserQueryService.java b/src/main/java/ssu/cromi/umc9th/domain/user/service/UserQueryService.java new file mode 100644 index 0000000..ebc228c --- /dev/null +++ b/src/main/java/ssu/cromi/umc9th/domain/user/service/UserQueryService.java @@ -0,0 +1,12 @@ +package ssu.cromi.umc9th.domain.user.service; + +import ssu.cromi.umc9th.domain.user.dto.UserReqDTO; +import ssu.cromi.umc9th.domain.user.dto.UserResDTO; + +public interface UserQueryService { + + // 로그인 + UserResDTO.LoginDTO login( + UserReqDTO.LoginDTO dto + ); +} \ No newline at end of file diff --git a/src/main/java/ssu/cromi/umc9th/domain/user/service/UserQueryServiceImpl.java b/src/main/java/ssu/cromi/umc9th/domain/user/service/UserQueryServiceImpl.java new file mode 100644 index 0000000..6b37090 --- /dev/null +++ b/src/main/java/ssu/cromi/umc9th/domain/user/service/UserQueryServiceImpl.java @@ -0,0 +1,47 @@ +package ssu.cromi.umc9th.domain.user.service; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ssu.cromi.umc9th.domain.user.converter.UserConverter; +import ssu.cromi.umc9th.domain.user.dto.UserReqDTO; +import ssu.cromi.umc9th.domain.user.dto.UserResDTO; +import ssu.cromi.umc9th.domain.user.entity.User; +import ssu.cromi.umc9th.domain.user.exception.UserException.UserException; +import ssu.cromi.umc9th.domain.user.exception.code.UserErrorCode; +import ssu.cromi.umc9th.domain.user.repository.UserRepository; +import ssu.cromi.umc9th.global.auth.entity.JwtUtil; +import ssu.cromi.umc9th.global.auth.service.CustomUserDetails; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserQueryServiceImpl implements UserQueryService { + + private final UserRepository userRepository; + private final JwtUtil jwtUtil; + private final PasswordEncoder passwordEncoder; + + @Override + public UserResDTO.LoginDTO login(UserReqDTO.@Valid LoginDTO dto) { + // 이메일로 사용자 조회 + User user = userRepository.findByEmail(dto.email()) + .orElseThrow(() -> new UserException(UserErrorCode.NOT_FOUND)); + + // 비밀번호 검증 + if (!passwordEncoder.matches(dto.password(), user.getPassword())) { + throw new UserException(UserErrorCode.NOT_FOUND); + } + + // JWT 토큰 발급용 UserDetails + CustomUserDetails userDetails = new CustomUserDetails(user); + + // 엑세스 토큰 발급 + String accessToken = jwtUtil.createAccessToken(userDetails); + + // DTO 조립 + return UserConverter.toLoginDTO(user, accessToken); + } +} \ No newline at end of file diff --git a/src/main/java/ssu/cromi/umc9th/global/auth/entity/JwtUtil.java b/src/main/java/ssu/cromi/umc9th/global/auth/entity/JwtUtil.java new file mode 100644 index 0000000..86d4471 --- /dev/null +++ b/src/main/java/ssu/cromi/umc9th/global/auth/entity/JwtUtil.java @@ -0,0 +1,93 @@ +package ssu.cromi.umc9th.global.auth.entity; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; +import ssu.cromi.umc9th.global.auth.service.CustomUserDetails; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.stream.Collectors; + +@Component +public class JwtUtil { + + private final SecretKey secretKey; + private final Duration accessExpiration; + + public JwtUtil( + @Value("${jwt.token.secretKey}") String secret, + @Value("${jwt.token.expiration.access}") Long accessExpiration + ) { + this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + this.accessExpiration = Duration.ofMillis(accessExpiration); + } + + // AccessToken 생성 + public String createAccessToken(CustomUserDetails user) { + return createToken(user, accessExpiration); + } + + /** 토큰에서 이메일 가져오기 + * + * @param token 유저 정보를 추출할 토큰 + * @return 유저 이메일을 토큰에서 추출합니다 + */ + public String getEmail(String token) { + try { + return getClaims(token).getPayload().getSubject(); // Parsing해서 Subject 가져오기 + } catch (JwtException e) { + return null; + } + } + + /** 토큰 유효성 확인 + * + * @param token 유효한지 확인할 토큰 + * @return True, False 반환합니다 + */ + public boolean isValid(String token) { + try { + getClaims(token); + return true; + } catch (JwtException e) { + return false; + } + } + + // 토큰 생성 + private String createToken(CustomUserDetails user, Duration expiration) { + Instant now = Instant.now(); + + // 인가 정보 + String authorities = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + return Jwts.builder() + .subject(user.getUsername()) // User 이메일을 Subject로 + .claim("role", authorities) + .claim("email", user.getUsername()) + .issuedAt(Date.from(now)) // 언제 발급한지 + .expiration(Date.from(now.plus(expiration))) // 언제까지 유효한지 + .signWith(secretKey) // sign할 Key + .compact(); + } + + // 토큰 정보 가져오기 + private Jws 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/ssu/cromi/umc9th/global/auth/enums/Role.java b/src/main/java/ssu/cromi/umc9th/global/auth/enums/Role.java new file mode 100644 index 0000000..33ee7b7 --- /dev/null +++ b/src/main/java/ssu/cromi/umc9th/global/auth/enums/Role.java @@ -0,0 +1,5 @@ +package ssu.cromi.umc9th.global.auth.enums; + +public enum Role { + ROLE_ADMIN, ROLE_USER +} diff --git a/src/main/java/ssu/cromi/umc9th/global/auth/filter/AuthenticationEntryPointImpl.java b/src/main/java/ssu/cromi/umc9th/global/auth/filter/AuthenticationEntryPointImpl.java new file mode 100644 index 0000000..4851a56 --- /dev/null +++ b/src/main/java/ssu/cromi/umc9th/global/auth/filter/AuthenticationEntryPointImpl.java @@ -0,0 +1,33 @@ +package ssu.cromi.umc9th.global.auth.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import ssu.cromi.umc9th.global.apiPayload.ApiResponse; +import ssu.cromi.umc9th.global.apiPayload.code.GeneralErrorCode; + +import java.io.IOException; + +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/ssu/cromi/umc9th/global/auth/filter/JwtAuthFilter.java b/src/main/java/ssu/cromi/umc9th/global/auth/filter/JwtAuthFilter.java new file mode 100644 index 0000000..e0d333a --- /dev/null +++ b/src/main/java/ssu/cromi/umc9th/global/auth/filter/JwtAuthFilter.java @@ -0,0 +1,74 @@ +package ssu.cromi.umc9th.global.auth.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 ssu.cromi.umc9th.global.apiPayload.ApiResponse; +import ssu.cromi.umc9th.global.apiPayload.code.GeneralErrorCode; +import ssu.cromi.umc9th.global.auth.entity.JwtUtil; +import ssu.cromi.umc9th.global.auth.service.CustomUserDetailsService; + +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"); + // 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/ssu/cromi/umc9th/global/auth/service/CustomUserDetails.java b/src/main/java/ssu/cromi/umc9th/global/auth/service/CustomUserDetails.java new file mode 100644 index 0000000..c0e568a --- /dev/null +++ b/src/main/java/ssu/cromi/umc9th/global/auth/service/CustomUserDetails.java @@ -0,0 +1,29 @@ +package ssu.cromi.umc9th.global.auth.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import ssu.cromi.umc9th.domain.user.entity.User; + +import java.util.Collection; +import java.util.List; + +@RequiredArgsConstructor +public class CustomUserDetails implements UserDetails { + private final User user; + + @Override + public Collection getAuthorities() { + return List.of(() -> user.getRole().toString()); + } + + @Override + public String getPassword() { + return user.getPassword(); + } + + @Override + public String getUsername() { + return user.getEmail(); + } +} \ No newline at end of file diff --git a/src/main/java/ssu/cromi/umc9th/global/auth/service/CustomUserDetailsService.java b/src/main/java/ssu/cromi/umc9th/global/auth/service/CustomUserDetailsService.java new file mode 100644 index 0000000..1cca351 --- /dev/null +++ b/src/main/java/ssu/cromi/umc9th/global/auth/service/CustomUserDetailsService.java @@ -0,0 +1,27 @@ +package ssu.cromi.umc9th.global.auth.service; + +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 ssu.cromi.umc9th.domain.user.entity.User; +import ssu.cromi.umc9th.domain.user.exception.UserException.UserException; +import ssu.cromi.umc9th.domain.user.exception.code.UserErrorCode; +import ssu.cromi.umc9th.domain.user.repository.UserRepository; + +@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); + } +} diff --git a/src/main/java/ssu/cromi/umc9th/global/config/SecurityConfig.java b/src/main/java/ssu/cromi/umc9th/global/config/SecurityConfig.java new file mode 100644 index 0000000..b5003a4 --- /dev/null +++ b/src/main/java/ssu/cromi/umc9th/global/config/SecurityConfig.java @@ -0,0 +1,69 @@ +package ssu.cromi.umc9th.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 ssu.cromi.umc9th.global.auth.entity.JwtUtil; +import ssu.cromi.umc9th.global.auth.filter.AuthenticationEntryPointImpl; +import ssu.cromi.umc9th.global.auth.filter.JwtAuthFilter; +import ssu.cromi.umc9th.global.auth.service.CustomUserDetailsService; + +@EnableWebSecurity +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + + private final String[] allowUris = { + "/sign-up", + "/login", + "/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(); + } +} + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c368d0b..b3a998c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,4 +16,10 @@ spring: ddl-auto: update # ?????? ?? ? ?????? ???? ??? ?? properties: hibernate: - format_sql: true # ???? SQL ??? ?? ?? ??? \ No newline at end of file + format_sql: true # ???? SQL ??? ?? ?? ??? + +jwt: + token: + secretKey: ZGh3YWlkc2F2ZXdhZXZ3b2EgMTM5ZXUgMDMxdWMyIHEyMiBAIDAgKTJFVio= + expiration: + access: 14400000 \ No newline at end of file