diff --git a/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Controller/newsignUpController.java b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Controller/newsignUpController.java new file mode 100644 index 0000000..9f1f0b6 --- /dev/null +++ b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Controller/newsignUpController.java @@ -0,0 +1,53 @@ +package com.gsm._8th.class4.backend.task12.domain.auth.Controller; + +import com.gsm._8th.class4.backend.task12.domain.auth.Service.DeleteUserService; +import com.gsm._8th.class4.backend.task12.domain.auth.Service.signinService; +import com.gsm._8th.class4.backend.task12.domain.auth.Service.SignUpService; +import com.gsm._8th.class4.backend.task12.domain.auth.Service.RefreshTokenService; +import com.gsm._8th.class4.backend.task12.domain.auth.dto.UserLoginRequest; +import com.gsm._8th.class4.backend.task12.domain.auth.dto.UserSignupRequest; +import com.gsm._8th.class4.backend.task12.global.security.TokenResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; +import java.net.URISyntaxException; + +@RestController +@RequestMapping("/api/v1/auth") // ✅ API 명세서에 맞춰 변경 +@RequiredArgsConstructor +public class newsignUpController { // ✅ 클래스명 변경 + + private final SignUpService authService; + private final DeleteUserService deleteUserService; + private final signinService signinService; + private final RefreshTokenService refreshTokenService; + + // 회원가입 + @PostMapping("/signup") + public ResponseEntity signup(@RequestBody UserSignupRequest request) throws URISyntaxException { + authService.signup(request); + URI location = new URI("http://localhost:8081/api/v1/auth/signin"); + return ResponseEntity.created(location).build(); + } + + // 회원 삭제 + @DeleteMapping("/delete") + public ResponseEntity deleteUser(@RequestBody UserLoginRequest request) { + deleteUserService.deleteUser(request.getUsername()); + return ResponseEntity.ok("계정 삭제 완료"); + } + + // 로그인 + @PostMapping("/signin") // ✅ API 경로 수정 + public ResponseEntity login(@RequestBody UserLoginRequest request) { + return ResponseEntity.ok(signinService.login(request.getUsername(), request.getPassword())); + } + + // 토큰 갱신 + @PostMapping("/refresh") // ✅ API 경로 유지 + public ResponseEntity refreshToken(@RequestHeader("Refresh-Token") String refreshToken) { + return ResponseEntity.ok(refreshTokenService.refreshToken(refreshToken)); + } +} diff --git a/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Entity/NewSign.java b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Entity/NewSign.java new file mode 100644 index 0000000..e8181e3 --- /dev/null +++ b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Entity/NewSign.java @@ -0,0 +1,28 @@ +package com.gsm._8th.class4.backend.task12.domain.auth.Entity; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Entity +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Table(name = "new_sign") +public class NewSign { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String username; + private String password; + private String role; + private String email; +} \ No newline at end of file diff --git a/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Repository/UserRepository.java b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Repository/UserRepository.java new file mode 100644 index 0000000..6e3e3f9 --- /dev/null +++ b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Repository/UserRepository.java @@ -0,0 +1,15 @@ +package com.gsm._8th.class4.backend.task12.domain.auth.Repository; + +import com.gsm._8th.class4.backend.task12.domain.auth.Entity.NewSign; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + + Optional findByUsername(String username); + + boolean existsByUsername(String username); + + void deleteByUsername(String username); +} diff --git a/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/DeleteUserService.java b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/DeleteUserService.java new file mode 100644 index 0000000..6279058 --- /dev/null +++ b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/DeleteUserService.java @@ -0,0 +1,5 @@ +package com.gsm._8th.class4.backend.task12.domain.auth.Service; + +public interface DeleteUserService { + void deleteUser(String username); +} diff --git a/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/GlobalExceptionHandler.java b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/GlobalExceptionHandler.java new file mode 100644 index 0000000..168a97c --- /dev/null +++ b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/GlobalExceptionHandler.java @@ -0,0 +1,16 @@ +package com.gsm._8th.class4.backend.task12.domain.auth.Service; + + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } +} \ No newline at end of file diff --git a/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/RefreshTokenService.java b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/RefreshTokenService.java new file mode 100644 index 0000000..0283182 --- /dev/null +++ b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/RefreshTokenService.java @@ -0,0 +1,7 @@ +package com.gsm._8th.class4.backend.task12.domain.auth.Service; + +import com.gsm._8th.class4.backend.task12.global.security.TokenResponse; + +public interface RefreshTokenService { + TokenResponse refreshToken(String refreshToken); +} diff --git a/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/RefreshTokenServiceImpl.java b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/RefreshTokenServiceImpl.java new file mode 100644 index 0000000..cf730b8 --- /dev/null +++ b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/RefreshTokenServiceImpl.java @@ -0,0 +1,28 @@ +package com.gsm._8th.class4.backend.task12.domain.auth.Service; + +import com.gsm._8th.class4.backend.task12.global.security.JwtTokenService; +import com.gsm._8th.class4.backend.task12.global.security.TokenResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class RefreshTokenServiceImpl implements RefreshTokenService { + private final JwtTokenService jwtTokenService; + + @Override + public TokenResponse refreshToken(String refreshToken) { + String username = Optional.ofNullable(jwtTokenService.getUsernameFromToken(refreshToken)) + .filter(u -> jwtTokenService.validateRefreshToken(u, refreshToken)) + .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 리프레시 토큰입니다.")); + + + jwtTokenService.revokeRefreshToken(username); + return new TokenResponse( + jwtTokenService.createAccessToken(username), + jwtTokenService.createRefreshToken(username) + ); + } +} diff --git a/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/SignUpService.java b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/SignUpService.java new file mode 100644 index 0000000..2b6d69d --- /dev/null +++ b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/SignUpService.java @@ -0,0 +1,7 @@ +package com.gsm._8th.class4.backend.task12.domain.auth.Service; + +import com.gsm._8th.class4.backend.task12.domain.auth.dto.UserSignupRequest; + +public interface SignUpService { + void signup(UserSignupRequest request); +} diff --git a/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/deleteUserImpl.java b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/deleteUserImpl.java new file mode 100644 index 0000000..34ccac3 --- /dev/null +++ b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/deleteUserImpl.java @@ -0,0 +1,30 @@ +package com.gsm._8th.class4.backend.task12.domain.auth.Service; + +import com.gsm._8th.class4.backend.task12.domain.auth.Entity.NewSign; +import com.gsm._8th.class4.backend.task12.global.security.JwtTokenService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.gsm._8th.class4.backend.task12.domain.auth.Repository.UserRepository; +import org.springframework.web.bind.annotation.ControllerAdvice; + +import java.util.Optional; + +@Service +@Transactional +@RequiredArgsConstructor +public class deleteUserImpl implements DeleteUserService { + private final UserRepository userRepository; + + @Override + @Transactional + + public void deleteUser(String username) { + userRepository.findByUsername(username) + .ifPresentOrElse( + userRepository::delete, + () -> { throw new IllegalArgumentException("존재하지 않는 사용자입니다."); } + ); +} +} diff --git a/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/signUpServiceImpl.java b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/signUpServiceImpl.java new file mode 100644 index 0000000..b5b33ca --- /dev/null +++ b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/signUpServiceImpl.java @@ -0,0 +1,33 @@ +package com.gsm._8th.class4.backend.task12.domain.auth.Service; + +import com.gsm._8th.class4.backend.task12.domain.auth.dto.UserSignupRequest; +import com.gsm._8th.class4.backend.task12.domain.auth.Entity.NewSign; +import com.gsm._8th.class4.backend.task12.domain.auth.Repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class signUpServiceImpl implements SignUpService { + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + @Override + @Transactional + public void signup(UserSignupRequest request) { + if (userRepository.existsByUsername(request.getUsername())) { + throw new IllegalArgumentException("이미 존재하는 사용자입니다."); + } + + NewSign newUser = NewSign.builder() + .username(request.getUsername()) + .password(passwordEncoder.encode(request.getPassword())) + .email(request.getEmail()) + .role("ROLE_USER") + .build(); + + userRepository.save(newUser); + } +} diff --git a/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/signinService.java b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/signinService.java new file mode 100644 index 0000000..39fbd80 --- /dev/null +++ b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/signinService.java @@ -0,0 +1,7 @@ +package com.gsm._8th.class4.backend.task12.domain.auth.Service; + +import com.gsm._8th.class4.backend.task12.global.security.TokenResponse; + +public interface signinService { + TokenResponse login(String username, String rawPassword); +} diff --git a/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/signinServiceImpl.java b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/signinServiceImpl.java new file mode 100644 index 0000000..2db7644 --- /dev/null +++ b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/Service/signinServiceImpl.java @@ -0,0 +1,28 @@ +package com.gsm._8th.class4.backend.task12.domain.auth.Service; + +import com.gsm._8th.class4.backend.task12.domain.auth.Entity.NewSign; +import com.gsm._8th.class4.backend.task12.domain.auth.Repository.UserRepository; +import com.gsm._8th.class4.backend.task12.global.security.JwtTokenService; +import com.gsm._8th.class4.backend.task12.global.security.TokenResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class signinServiceImpl implements signinService { + private final UserRepository userRepository; + private final JwtTokenService jwtTokenService; + private final PasswordEncoder passwordEncoder; + + @Override + public TokenResponse login(String username, String rawPassword) { + NewSign user = userRepository.findByUsername(username) + .filter(u -> passwordEncoder.matches(rawPassword, u.getPassword())) + .orElseThrow(() -> new IllegalArgumentException("아이디 또는 비밀번호가 틀렸습니다.")); + return new TokenResponse( + jwtTokenService.createAccessToken(username), + jwtTokenService.createRefreshToken(username) + ); + } +} diff --git a/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/dto/UserLoginRequest.java b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/dto/UserLoginRequest.java new file mode 100644 index 0000000..9708797 --- /dev/null +++ b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/dto/UserLoginRequest.java @@ -0,0 +1,12 @@ +package com.gsm._8th.class4.backend.task12.domain.auth.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter + +public class UserLoginRequest { + private String username; + private String password; +} \ No newline at end of file diff --git a/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/dto/UserSignupRequest.java b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/dto/UserSignupRequest.java new file mode 100644 index 0000000..68cd5d1 --- /dev/null +++ b/src/main/java/com/gsm/_8th/class4/backend/task12/domain/auth/dto/UserSignupRequest.java @@ -0,0 +1,24 @@ +package com.gsm._8th.class4.backend.task12.domain.auth.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UserSignupRequest { + + @NotBlank(message = "사용자 이름은 필수 입력값입니다.") + @Size(min = 3, max = 20, message = "사용자 이름은 3~20자로 입력해야 합니다.") + private String username; + + @NotBlank(message = "비밀번호는 필수 입력값입니다.") + @Size(min = 6, message = "비밀번호는 최소 6자 이상이어야 합니다.") + private String password; + + @NotBlank(message = "이메일은 필수 입력값입니다.") + @Email(message = "올바른 이메일 형식을 입력해주세요.") + private String email; +} \ No newline at end of file diff --git a/src/main/java/com/gsm/_8th/class4/backend/task12/global/security/JwtAuthenticationFilter.java b/src/main/java/com/gsm/_8th/class4/backend/task12/global/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..f9f351b --- /dev/null +++ b/src/main/java/com/gsm/_8th/class4/backend/task12/global/security/JwtAuthenticationFilter.java @@ -0,0 +1,52 @@ +package com.gsm._8th.class4.backend.task12.global.security; + +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.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenService jwtTokenService; + private final UserDetailsService userDetailsService; // 추가 + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + String authHeader = request.getHeader("Authorization"); + + // null 체크 + Bearer 형식인지 확인 + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + String token = authHeader.substring(7); + + if (jwtTokenService.validateToken(token)) { + String username = jwtTokenService.getUsernameFromToken(token); + + // UserDetailsService를 활용하여 사용자 정보 로드 + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + // 인증 객체 생성 + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + + // Spring Security 컨텍스트에 저장 + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/gsm/_8th/class4/backend/task12/global/security/JwtTokenService.java b/src/main/java/com/gsm/_8th/class4/backend/task12/global/security/JwtTokenService.java new file mode 100644 index 0000000..339deb8 --- /dev/null +++ b/src/main/java/com/gsm/_8th/class4/backend/task12/global/security/JwtTokenService.java @@ -0,0 +1,71 @@ +package com.gsm._8th.class4.backend.task12.global.security; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Date; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Service +public class JwtTokenService { + + @Value("${jwt.secret}") + private String secretKey; + + private final long accessTokenValidity = 60 * 60 * 1000L; + private final long refreshTokenValidity = 120 * 60 * 1000L; + private final ConcurrentHashMap refreshTokenStore = new ConcurrentHashMap<>(); + private Key getSigningKey() { + return Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); + } + public String createAccessToken(String username) { + return createToken(username, accessTokenValidity); + } + public String createRefreshToken(String username) { + String refreshToken = createToken(username, refreshTokenValidity); + refreshTokenStore.put(username, refreshToken); + return refreshToken; + } + private String createToken(String subject, long validity) { + Date now = new Date(); + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + validity)) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) // 변경됨 + .compact(); + } + public boolean validateToken(String token) { + try { + Jws claims = Jwts.parser() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token); + + return !claims.getBody().getExpiration().before(new Date()); // 만료 확인 + } catch (JwtException | IllegalArgumentException e) { + log.error("유효하지 않은 토큰: {}", e.getMessage()); + return false; + } + } + public String getUsernameFromToken(String token) { + return Jwts.parser() // 변경됨 + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody() + .getSubject(); + } + public boolean validateRefreshToken(String username, String refreshToken) { + return refreshToken.equals(refreshTokenStore.get(username)); + } + public void revokeRefreshToken(String username) { + refreshTokenStore.remove(username); + } +} diff --git a/src/main/java/com/gsm/_8th/class4/backend/task12/global/security/SecurityConfig.java b/src/main/java/com/gsm/_8th/class4/backend/task12/global/security/SecurityConfig.java index 457a7f7..d26512c 100644 --- a/src/main/java/com/gsm/_8th/class4/backend/task12/global/security/SecurityConfig.java +++ b/src/main/java/com/gsm/_8th/class4/backend/task12/global/security/SecurityConfig.java @@ -1,35 +1,46 @@ package com.gsm._8th.class4.backend.task12.global.security; 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.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.stereotype.Component; -@Component -@EnableWebSecurity +import static org.springframework.security.config.Customizer.withDefaults; + +@Configuration public class SecurityConfig { - /* - TODO: 해당 클래스를 수정하여주세요! - */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) { + UserDetails user = User.builder() + .username("user") + .password(passwordEncoder.encode("password")) + .roles("USER") + .build(); + + return new InMemoryUserDetailsManager(user); + } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - .cors(AbstractHttpConfigurer::disable) - .csrf(AbstractHttpConfigurer::disable) - .authorizeHttpRequests((authorize) -> { - authorize.anyRequest().permitAll(); - }) - .formLogin(AbstractHttpConfigurer::disable) - .sessionManagement((sessionManagement) -> { - sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS); - }) - .httpBasic(AbstractHttpConfigurer::disable); - return http.build(); + .authorizeHttpRequests(auth -> auth + .anyRequest().authenticated() + ) + .formLogin(withDefaults()) + .httpBasic(withDefaults()); + return http.build(); } } diff --git a/src/main/java/com/gsm/_8th/class4/backend/task12/global/security/TokenResponse.java b/src/main/java/com/gsm/_8th/class4/backend/task12/global/security/TokenResponse.java new file mode 100644 index 0000000..84a80a7 --- /dev/null +++ b/src/main/java/com/gsm/_8th/class4/backend/task12/global/security/TokenResponse.java @@ -0,0 +1,11 @@ +package com.gsm._8th.class4.backend.task12.global.security; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TokenResponse { + private final String accessToken; + private final String refreshToken; +}