Skip to content

과제 1-2 pgsshiho 과제제출 #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<String> signup(@RequestBody UserSignupRequest request) throws URISyntaxException {
authService.signup(request);
URI location = new URI("http://localhost:8081/api/v1/auth/signin");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 이 코드는 왜 있는건가요?
API 주도형 개발에서는 이런 URI를 반환할 이유가 거의 없지 않나요?서버에 직접 정적파일을 보관하는 것이 아니라면 불필요해 보입니다

혹시 필요하다면 이유를 알려주세요
진짜로 모름

return ResponseEntity.created(location).build();
}

// 회원 삭제
@DeleteMapping("/delete")
public ResponseEntity<String> deleteUser(@RequestBody UserLoginRequest request) {
deleteUserService.deleteUser(request.getUsername());
return ResponseEntity.ok("계정 삭제 완료");
}

// 로그인
@PostMapping("/signin") // ✅ API 경로 수정
public ResponseEntity<TokenResponse> login(@RequestBody UserLoginRequest request) {
return ResponseEntity.ok(signinService.login(request.getUsername(), request.getPassword()));
}

// 토큰 갱신
@PostMapping("/refresh") // ✅ API 경로 유지
public ResponseEntity<TokenResponse> refreshToken(@RequestHeader("Refresh-Token") String refreshToken) {
return ResponseEntity.ok(refreshTokenService.refreshToken(refreshToken));
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Comment on lines +1 to +28
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entity 이름이 new_sign인 것은 현재 비즈니스 요구사항에는 적합하지 않은 것 같습니다

만약 회원가입이 승인제로 이뤄지는 서비스여서 새 계정 정보를 승인 전까지 임시로 저장하는 테이블이라면 나름 적합할 수도 있겠지만...
현재 비즈니스 요구에서는 단순한 사용자 정보 저장이 목적이니만큼 UserMember 라는 이름이 적합한 것일것 같습니다

Original file line number Diff line number Diff line change
@@ -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<NewSign, Long> {

Optional<NewSign> findByUsername(String username);

boolean existsByUsername(String username);

void deleteByUsername(String username);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.gsm._8th.class4.backend.task12.domain.auth.Service;

public interface DeleteUserService {
void deleteUser(String username);
}
Original file line number Diff line number Diff line change
@@ -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<String> handleIllegalArgumentException(IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
}
Comment on lines +1 to +16
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GlobalExceptionHandler는 다른 패키지로 이동시켜주세요!

프로젝트 전역적으로 적용되는 클래스이니 만큼 domain보단 global 패키지 내부가 적절할 듯 합니다

Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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)
);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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("존재하지 않는 사용자입니다."); }
);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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)
);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading