diff --git a/build.gradle b/build.gradle index 31053fe..fc404d5 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,15 @@ dependencies { implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'com.fasterxml.jackson.core:jackson-annotations' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + + // JWT (JJWT) 라이브러리 + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' // JSON 직렬화용 + + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + } tasks.named('test') { diff --git a/src/main/java/dev/admin/admin/controller/AdminAuthController.java b/src/main/java/dev/admin/admin/controller/AdminAuthController.java new file mode 100644 index 0000000..90d7c08 --- /dev/null +++ b/src/main/java/dev/admin/admin/controller/AdminAuthController.java @@ -0,0 +1,90 @@ +package dev.admin.admin.controller; + +import dev.admin.admin.dto.request.LoginRequestDto; +import dev.admin.admin.dto.response.AdminInfoResponse; +import dev.admin.admin.dto.response.JwtDto; +import dev.admin.admin.dto.response.JwtPayload; +import dev.admin.admin.service.command.AdminAuthCommandService; +import dev.admin.admin.utils.JwtUtil; +import dev.admin.global.apiPayload.ApiResponse; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseCookie; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/admin") +@RequiredArgsConstructor +public class AdminAuthController { + + private final AdminAuthCommandService authCommandService; + private final JwtUtil jwtUtil; + + @PostMapping("/login") + public ApiResponse login(@RequestBody LoginRequestDto requestDto, HttpServletResponse response) { + JwtDto tokenDto = authCommandService.login(requestDto); + + ResponseCookie accessTokenCookie = ResponseCookie.from("accessToken", tokenDto.getAccessToken()) + .httpOnly(true) + .secure(false) + .path("/") + .sameSite("Lax") + .maxAge(60 * 60) + .build(); + + ResponseCookie refreshTokenCookie = ResponseCookie.from("refreshToken", tokenDto.getRefreshToken()) + .httpOnly(true) + .secure(false) + .path("/") + .sameSite("Lax") + .maxAge(7 * 24 * 60 * 60) + .build(); + + response.addHeader("Set-Cookie", accessTokenCookie.toString()); + response.addHeader("Set-Cookie", refreshTokenCookie.toString()); + + return ApiResponse.onSuccess(tokenDto); + } + + @PostMapping("/logout") + public ApiResponse logout(@CookieValue("refreshToken") String refreshToken, + HttpServletResponse response) { + authCommandService.logout(refreshToken); + + ResponseCookie clearAccessToken = ResponseCookie.from("accessToken", "") + .httpOnly(true) + .secure(false) + .path("/") + .maxAge(0) + .sameSite("Lax") + .build(); + + ResponseCookie clearRefreshToken = ResponseCookie.from("refreshToken", "") + .httpOnly(true) + .secure(false) + .path("/") + .maxAge(0) + .sameSite("Lax") + .build(); + + response.addHeader("Set-Cookie", clearAccessToken.toString()); + response.addHeader("Set-Cookie", clearRefreshToken.toString()); + + return ApiResponse.onSuccess(null); + } + + @GetMapping("/me") + public ApiResponse me(@CookieValue("accessToken") String accessToken) { + JwtPayload payload = jwtUtil.parseToken(accessToken); + + AdminInfoResponse info = new AdminInfoResponse( + payload.getSub(), + payload.getEmail(), + payload.getName(), + payload.getRole() + ); + + return ApiResponse.onSuccess(info); + } +} diff --git a/src/main/java/dev/admin/admin/dto/request/LoginRequestDto.java b/src/main/java/dev/admin/admin/dto/request/LoginRequestDto.java new file mode 100644 index 0000000..ed1d95c --- /dev/null +++ b/src/main/java/dev/admin/admin/dto/request/LoginRequestDto.java @@ -0,0 +1,10 @@ +package dev.admin.admin.dto.request; + +import lombok.Getter; + +@Getter +public class LoginRequestDto { + + private String email; + private String password; +} diff --git a/src/main/java/dev/admin/admin/dto/response/AdminInfoResponse.java b/src/main/java/dev/admin/admin/dto/response/AdminInfoResponse.java new file mode 100644 index 0000000..6e4bad6 --- /dev/null +++ b/src/main/java/dev/admin/admin/dto/response/AdminInfoResponse.java @@ -0,0 +1,31 @@ +package dev.admin.admin.dto.response; + +public class AdminInfoResponse { + private String id; + private String email; + private String name; + private String role; + + public AdminInfoResponse(String id, String email, String name, String role) { + this.id = id; + this.email = email; + this.name = name; + this.role = role; + } + + public String getId() { + return id; + } + + public String getEmail() { + return email; + } + + public String getName() { + return name; + } + + public String getRole() { + return role; + } +} diff --git a/src/main/java/dev/admin/admin/dto/response/JwtDto.java b/src/main/java/dev/admin/admin/dto/response/JwtDto.java new file mode 100644 index 0000000..abb72dd --- /dev/null +++ b/src/main/java/dev/admin/admin/dto/response/JwtDto.java @@ -0,0 +1,12 @@ +package dev.admin.admin.dto.response; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class JwtDto { + private String accessToken; + private String refreshToken; +} \ No newline at end of file diff --git a/src/main/java/dev/admin/admin/dto/response/JwtPayload.java b/src/main/java/dev/admin/admin/dto/response/JwtPayload.java new file mode 100644 index 0000000..98c0f92 --- /dev/null +++ b/src/main/java/dev/admin/admin/dto/response/JwtPayload.java @@ -0,0 +1,17 @@ +package dev.admin.admin.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class JwtPayload { + private String sub; // subject (adminId) + private String email; + private String name; + private String role; +} diff --git a/src/main/java/dev/admin/admin/entity/Admin.java b/src/main/java/dev/admin/admin/entity/Admin.java index 08336a7..2e57b7b 100644 --- a/src/main/java/dev/admin/admin/entity/Admin.java +++ b/src/main/java/dev/admin/admin/entity/Admin.java @@ -3,6 +3,8 @@ import dev.admin.global.entity.BaseTimeEntity; import jakarta.persistence.*; import lombok.*; +import dev.admin.admin.entity.Role; + @Entity @Getter @@ -20,4 +22,12 @@ public class Admin extends BaseTimeEntity { @Column(nullable = false) private String password; + + @Column(nullable = false) + private String name; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private Role role; + } diff --git a/src/main/java/dev/admin/admin/entity/Role.java b/src/main/java/dev/admin/admin/entity/Role.java new file mode 100644 index 0000000..96344a2 --- /dev/null +++ b/src/main/java/dev/admin/admin/entity/Role.java @@ -0,0 +1,6 @@ +package dev.admin.admin.entity; + +public enum Role { + ADMIN, // 관리자 + // 필요하면 다른 역할도 추가 가능 +} diff --git a/src/main/java/dev/admin/admin/filter/JwtAuthenticationFilter.java b/src/main/java/dev/admin/admin/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..2dafd56 --- /dev/null +++ b/src/main/java/dev/admin/admin/filter/JwtAuthenticationFilter.java @@ -0,0 +1,68 @@ +package dev.admin.admin.filter; + +import dev.admin.admin.security.AdminDetailsService; +import dev.admin.admin.utils.JwtUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final AdminDetailsService adminDetailsService; + + /** + * 요청 헤더에서 JWT 꺼내서 → 유효하면 SecurityContext에 인증 정보 저장 + */ + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + String token = null; + + // 1. Authorization 헤더에서 Bearer 토큰 추출 + String authHeader = request.getHeader("Authorization"); + if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) { + token = authHeader.substring(7); + } + + // 2. Authorization 헤더가 없으면 쿠키에서 accessToken 추출 + if (token == null && request.getCookies() != null) { + for (var cookie : request.getCookies()) { + if (cookie.getName().equals("accessToken")) { + token = cookie.getValue(); + break; + } + } + } + + // 3. 토큰이 있고, 유효한 경우 + if (token != null && jwtUtil.validateToken(token)) { + + String email = jwtUtil.getEmailFromToken(token); + var adminDetails = adminDetailsService.loadUserByUsername(email); + + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(adminDetails, null, adminDetails.getAuthorities()); + + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/dev/admin/admin/repository/AdminRepository.java b/src/main/java/dev/admin/admin/repository/AdminRepository.java index 282236d..c851e66 100644 --- a/src/main/java/dev/admin/admin/repository/AdminRepository.java +++ b/src/main/java/dev/admin/admin/repository/AdminRepository.java @@ -3,5 +3,12 @@ import dev.admin.admin.entity.Admin; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface AdminRepository extends JpaRepository { + + /** + * 이메일로 관리자 정보 조회 (로그인용) + */ + Optional findByEmail(String email); } diff --git a/src/main/java/dev/admin/admin/repository/RedisTokenRepository.java b/src/main/java/dev/admin/admin/repository/RedisTokenRepository.java new file mode 100644 index 0000000..cc4b25f --- /dev/null +++ b/src/main/java/dev/admin/admin/repository/RedisTokenRepository.java @@ -0,0 +1,39 @@ +package dev.admin.admin.repository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.Duration; +import java.util.Optional; + +@Slf4j +@RequiredArgsConstructor +@Repository +public class RedisTokenRepository implements TokenRepository { + + private final StringRedisTemplate redis; + + @Override + public void save(String key, String value) { + redis.opsForValue().set(key, value, Duration.ofMinutes(30)); + } + + @Override + public Optional find(String key) { + return Optional.ofNullable(redis.opsForValue().get(key)); + } + + @Override + public void delete(String key) { + redis.delete(key); + } + + @Override + public boolean exists(String key) { + Boolean result = redis.hasKey(key); + return result != null && result; + } +} + diff --git a/src/main/java/dev/admin/admin/repository/TokenRepository.java b/src/main/java/dev/admin/admin/repository/TokenRepository.java new file mode 100644 index 0000000..b44e009 --- /dev/null +++ b/src/main/java/dev/admin/admin/repository/TokenRepository.java @@ -0,0 +1,10 @@ +package dev.admin.admin.repository; + +import java.util.Optional; + +public interface TokenRepository { + void save(String key, String value); + Optional find(String key); + void delete(String key); + boolean exists(String key); +} diff --git a/src/main/java/dev/admin/admin/security/AdminDetails.java b/src/main/java/dev/admin/admin/security/AdminDetails.java new file mode 100644 index 0000000..e0132d1 --- /dev/null +++ b/src/main/java/dev/admin/admin/security/AdminDetails.java @@ -0,0 +1,80 @@ +package dev.admin.admin.security; + +import java.util.List; +import dev.admin.admin.entity.Admin; +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; + +@RequiredArgsConstructor +public class AdminDetails implements UserDetails { + + // 실제 DB에서 조회된 Admin 객체를 담음 + private final Admin admin; + + /** + * 관리자 권한 반환 + */ + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority("ROLE_ADMIN")); + } + + /** + * 로그인에 사용할 비밀번호 반환 + */ + @Override + public String getPassword() { + return admin.getPassword(); + } + + /** + * 로그인에 사용할 ID(email) 반환 + */ + @Override + public String getUsername() { + return admin.getEmail(); + } + + /** + * 계정 만료 여부 (true = 만료되지 않음) + */ + @Override + public boolean isAccountNonExpired() { + return true; + } + + /** + * 계정 잠김 여부 (true = 잠기지 않음) + */ + @Override + public boolean isAccountNonLocked() { + return true; + } + + /** + * 자격 증명(비밀번호) 만료 여부 (true = 만료되지 않음) + */ + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + /** + * 계정 활성화 여부 (true = 활성 계정) + */ + @Override + public boolean isEnabled() { + return true; + } + + /** + * 인증이 완료된 관리자의 엔티티 객체를 외부에서 사용할 수 있도록 반환 + */ + public Admin getAdmin() { + return this.admin; + } +} diff --git a/src/main/java/dev/admin/admin/security/AdminDetailsService.java b/src/main/java/dev/admin/admin/security/AdminDetailsService.java new file mode 100644 index 0000000..cddcce6 --- /dev/null +++ b/src/main/java/dev/admin/admin/security/AdminDetailsService.java @@ -0,0 +1,24 @@ +package dev.admin.admin.security; + +import dev.admin.admin.entity.Admin; +import dev.admin.admin.repository.AdminRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AdminDetailsService implements UserDetailsService { + + private final AdminRepository adminRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Admin admin = adminRepository.findByEmail(email.trim()) + .orElseThrow(() -> new UsernameNotFoundException("관리자를 찾을 수 없습니다.")); + return new AdminDetails(admin); + } + +} diff --git a/src/main/java/dev/admin/admin/service/command/AdminAuthCommandService.java b/src/main/java/dev/admin/admin/service/command/AdminAuthCommandService.java new file mode 100644 index 0000000..f9e639f --- /dev/null +++ b/src/main/java/dev/admin/admin/service/command/AdminAuthCommandService.java @@ -0,0 +1,65 @@ +package dev.admin.admin.service.command; + +import dev.admin.admin.dto.request.LoginRequestDto; +import dev.admin.admin.dto.response.JwtDto; +import dev.admin.admin.entity.Admin; +import dev.admin.admin.repository.AdminRepository; +import dev.admin.admin.repository.TokenRepository; +import dev.admin.admin.utils.JwtUtil; +import dev.admin.global.apiPayload.code.status.ErrorStatus; +import dev.admin.global.apiPayload.exception.GeneralException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AdminAuthCommandService { + + private final AdminRepository adminRepository; + private final PasswordEncoder passwordEncoder; + private final JwtUtil jwtUtil; + private final TokenRepository tokenRepository; + + public JwtDto login(LoginRequestDto requestDto) { + Admin admin = adminRepository.findByEmail(requestDto.getEmail()) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 관리자입니다.")); + + if (!passwordEncoder.matches(requestDto.getPassword(), admin.getPassword())) { + throw new GeneralException(ErrorStatus.INVALID_PASSWORD); + } + + String accessToken = jwtUtil.generateAccessToken( + admin.getId().toString(), + admin.getEmail(), + admin.getName(), + admin.getRole().name() + ); + + String refreshToken = jwtUtil.generateRefreshToken(); + + tokenRepository.save(admin.getEmail() + "_refreshToken", refreshToken); // refreshToken 저장 + + return new JwtDto(accessToken, refreshToken); + } + + /** + * 로그아웃: refreshToken 삭제 + */ + public void logout(String refreshToken) { + // Redis에서 RefreshToken 삭제 + String redisRefreshKey = "refresh:" + refreshToken; + if (tokenRepository.exists(redisRefreshKey)) { + tokenRepository.delete(redisRefreshKey); + } + + // 이메일 추출 + String email = jwtUtil.getEmailFromToken(refreshToken); + + // 그러나 원하면 Redis에서 직접 삭제할 수 있음 (Access Token을 Redis에 저장한 경우) + String redisAccessKey = email + "_accessToken"; + if (tokenRepository.exists(redisAccessKey)) { + tokenRepository.delete(redisAccessKey); // Redis에서 AccessToken 삭제 + } + } +} diff --git a/src/main/java/dev/admin/admin/utils/JwtUtil.java b/src/main/java/dev/admin/admin/utils/JwtUtil.java new file mode 100644 index 0000000..7fcd4ec --- /dev/null +++ b/src/main/java/dev/admin/admin/utils/JwtUtil.java @@ -0,0 +1,109 @@ +package dev.admin.admin.utils; + +import dev.admin.admin.dto.response.JwtPayload; +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; + +@Component +public class JwtUtil { + + @Value("${spring.jwt.secret}") + private String secretKey; + + @Value("${spring.jwt.token.access-expiration-time}") + private long accessTokenExpiration; + + @Value("${spring.jwt.token.refresh-expiration-time}") + private long refreshTokenExpiration; + + private Key key; + + /** + * Bean 생성 직후 실행되어, 문자열 시크릿 키를 HMAC 키 객체로 변환 + */ + @PostConstruct + public void init() { + this.key = Keys.hmacShaKeyFor(secretKey.getBytes()); + } + + /** + * Access Token 생성 + */ + public String generateAccessToken(String id, String email, String name, String role) { + return Jwts.builder() + .setSubject(id) // sub = 사용자 id + .claim("email", email) + .claim("name", name) + .claim("role", role) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + accessTokenExpiration)) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + + /** + * Refresh Token 생성 + */ + public String generateRefreshToken() { + return Jwts.builder() + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + refreshTokenExpiration)) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + /** + * 토큰에서 사용자 이메일(subject) 추출 + */ + public String getEmailFromToken(String token) { + return parseClaims(token).get("email", String.class); + } + + /** + * 토큰 유효성 검증 + */ + public boolean validateToken(String token) { + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + return true; + } catch (ExpiredJwtException e) { + System.out.println("토큰 만료"); + } catch (UnsupportedJwtException e) { + System.out.println("지원되지 않는 토큰"); + } catch (MalformedJwtException e) { + System.out.println("잘못된 형식의 토큰"); + } catch (SecurityException | IllegalArgumentException e) { + System.out.println("토큰 검증 실패"); + } + return false; + } + + /** + * 토큰의 Claims (내부 정보)를 꺼내는 내부 메서드 + */ + private Claims parseClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public JwtPayload parseToken(String token) { + Claims claims = parseClaims(token); + return new JwtPayload( + claims.getSubject(), // id + (String) claims.get("email"), + (String) claims.get("name"), + (String) claims.get("role") + ); + } + +} \ No newline at end of file diff --git a/src/main/java/dev/admin/global/apiPayload/code/status/ErrorStatus.java b/src/main/java/dev/admin/global/apiPayload/code/status/ErrorStatus.java index e9738b5..eff84b9 100644 --- a/src/main/java/dev/admin/global/apiPayload/code/status/ErrorStatus.java +++ b/src/main/java/dev/admin/global/apiPayload/code/status/ErrorStatus.java @@ -21,6 +21,9 @@ public enum ErrorStatus implements BaseErrorCode { _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."), _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."), + // Login + INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "AUTH401", "비밀번호가 일치하지 않습니다."), + // Notice NOTICE_NOT_FOUND(HttpStatus.NOT_FOUND, "NOTICE404", "공지사항을 찾을 수 없습니다."), diff --git a/src/main/java/dev/admin/global/config/RedisConfig.java b/src/main/java/dev/admin/global/config/RedisConfig.java new file mode 100644 index 0000000..0cd5261 --- /dev/null +++ b/src/main/java/dev/admin/global/config/RedisConfig.java @@ -0,0 +1,22 @@ +package dev.admin.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; + +@Configuration +public class RedisConfig { + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(); + } + + // RedisTemplate 등록 + @Bean + public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) { + return new StringRedisTemplate(connectionFactory); + } +} \ No newline at end of file diff --git a/src/main/java/dev/admin/global/config/SecurityConfig.java b/src/main/java/dev/admin/global/config/SecurityConfig.java index a388576..5e0e3e7 100644 --- a/src/main/java/dev/admin/global/config/SecurityConfig.java +++ b/src/main/java/dev/admin/global/config/SecurityConfig.java @@ -1,22 +1,53 @@ package dev.admin.global.config; +import dev.admin.admin.filter.JwtAuthenticationFilter; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.Customizer; +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.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 JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http - .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) .csrf(csrf -> csrf.disable()) - .formLogin(Customizer.withDefaults()) - .httpBasic(Customizer.withDefaults()) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/test/**").permitAll() // ✅ 여기를 추가! + + .requestMatchers( + "/admin/login", + "/admin/logout", + "/swagger-ui.html", + "/swagger-ui/**", + "/v3/api-docs/**" + ).permitAll() .requestMatchers("/admin/me", "/admin-api/**").authenticated() + .anyRequest().denyAll() + ) + .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .logout(logout -> logout.disable()) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .build(); } }