From cc2779c1038736534b5af4bec9434020f2fb511c Mon Sep 17 00:00:00 2001 From: dami0806 Date: Tue, 4 Jun 2024 19:31:44 +0900 Subject: [PATCH] feat: logic according to conditions --- .../auth/controller/AuthRestController.java | 84 +++++++--- .../auth/dto/SignupRequestDto.java | 3 +- .../auth/entity/LoginRequest.java | 2 + .../auth/entity/LoginResponse.java | 12 ++ .../oneandzerobest/auth/entity/User.java | 64 ++++---- .../auth/service/UserDetailsServiceImpl.java | 3 + .../auth/service/UserService.java | 13 +- .../auth/service/UserServiceImpl.java | 149 ++++++++++++------ src/main/resources/application.yml | 6 +- 9 files changed, 222 insertions(+), 114 deletions(-) create mode 100644 src/main/java/com/sparta/oneandzerobest/auth/entity/LoginResponse.java diff --git a/src/main/java/com/sparta/oneandzerobest/auth/controller/AuthRestController.java b/src/main/java/com/sparta/oneandzerobest/auth/controller/AuthRestController.java index 5704b16..948271a 100644 --- a/src/main/java/com/sparta/oneandzerobest/auth/controller/AuthRestController.java +++ b/src/main/java/com/sparta/oneandzerobest/auth/controller/AuthRestController.java @@ -1,20 +1,25 @@ package com.sparta.oneandzerobest.auth.controller; -import com.sparta.oneandzerobest.auth.dto.LoginRequestDto; import com.sparta.oneandzerobest.auth.dto.RefreshTokenRequestDto; -import com.sparta.oneandzerobest.auth.dto.SignupRequestDto; import com.sparta.oneandzerobest.auth.dto.TokenResponseDto; import com.sparta.oneandzerobest.auth.entity.LoginRequest; +import com.sparta.oneandzerobest.auth.entity.LoginResponse; import com.sparta.oneandzerobest.auth.entity.SignupRequest; import com.sparta.oneandzerobest.auth.service.UserService; import com.sparta.oneandzerobest.auth.util.JwtUtil; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +/** + * 인증기능 컨트롤러 + * - 로그인 + * - 로그아웃 + * - 탈퇴 + * - 리프레시 토큰 재발급 + */ @RestController @RequestMapping("/api/auth") public class AuthRestController { @@ -26,33 +31,64 @@ public AuthRestController(UserService userService, JwtUtil jwtUtil) { this.jwtUtil = jwtUtil; } + /** + * 회원가입 + * @param signupRequest + * @return + */ @PostMapping("/signup") - public ResponseEntity signup(@RequestBody SignupRequestDto signupRequestDto) { - SignupRequest signupRequest = new SignupRequest( - signupRequestDto.getUsername(), - signupRequestDto.getPassword(), - signupRequestDto.getEmail(), - signupRequestDto.isAdmin(), - signupRequestDto.getAdminToken() - ); + public ResponseEntity signup(@RequestBody SignupRequest signupRequest) { userService.signup(signupRequest); - return ResponseEntity.ok("회원가입 성공"); + return ResponseEntity.status(HttpStatus.CREATED).body("회원가입 성공"); } + /** + * 로그인 + * @param loginRequest + * @return 헤더에 반환 + */ @PostMapping("/login") - public ResponseEntity login(@RequestBody LoginRequestDto loginRequestDto) { - LoginRequest loginRequest = new LoginRequest( - loginRequestDto.getUsername(), - loginRequestDto.getPassword() - ); + public ResponseEntity login(@RequestBody LoginRequest loginRequest) { + LoginResponse tokens = userService.login(loginRequest); // 로그인 시도 및 토큰 생성 + String accessToken = tokens.getAccessToken(); + String refreshToken = tokens.getRefreshToken(); - String token = userService.login(loginRequest); - String refreshToken = jwtUtil.createRefreshToken(loginRequestDto.getUsername()); + // 각 토큰을 별도의 헤더에 설정 + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + accessToken); + headers.set("Refresh-Token", refreshToken); - TokenResponseDto tokenResponseDto = new TokenResponseDto(token, refreshToken); - return ResponseEntity.ok(tokenResponseDto); + return new ResponseEntity<>("로그인 성공", headers, HttpStatus.OK); + } + + /** + * 로그아웃 + * @param username + * @return + */ + @PostMapping("/logout") + public ResponseEntity logout(@RequestParam String username) { + userService.logout(username); + return ResponseEntity.ok("로그아웃 성공"); + } + + /** + *withdraw: 탈퇴 + * @param username + * @param password + * @return + */ + @PostMapping("/withdraw") + public ResponseEntity withdraw(@RequestParam String username, @RequestParam String password) { + userService.withdraw(username, password); + return ResponseEntity.ok("회원탈퇴 성공"); } + /** + * 리프레시 토큰 재발급 + * @param refreshTokenRequestDto + * @return + */ @PostMapping("/refresh") public ResponseEntity refresh(@RequestBody RefreshTokenRequestDto refreshTokenRequestDto) { diff --git a/src/main/java/com/sparta/oneandzerobest/auth/dto/SignupRequestDto.java b/src/main/java/com/sparta/oneandzerobest/auth/dto/SignupRequestDto.java index dfccd65..d6d2d53 100644 --- a/src/main/java/com/sparta/oneandzerobest/auth/dto/SignupRequestDto.java +++ b/src/main/java/com/sparta/oneandzerobest/auth/dto/SignupRequestDto.java @@ -10,7 +10,6 @@ public class SignupRequestDto { private String username; private String password; + private String name; private String email; - private boolean admin = false; - private String adminToken = ""; } diff --git a/src/main/java/com/sparta/oneandzerobest/auth/entity/LoginRequest.java b/src/main/java/com/sparta/oneandzerobest/auth/entity/LoginRequest.java index 009217d..65a095d 100644 --- a/src/main/java/com/sparta/oneandzerobest/auth/entity/LoginRequest.java +++ b/src/main/java/com/sparta/oneandzerobest/auth/entity/LoginRequest.java @@ -1,12 +1,14 @@ package com.sparta.oneandzerobest.auth.entity; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Getter @NoArgsConstructor @AllArgsConstructor +@Builder public class LoginRequest { private String username; private String password; diff --git a/src/main/java/com/sparta/oneandzerobest/auth/entity/LoginResponse.java b/src/main/java/com/sparta/oneandzerobest/auth/entity/LoginResponse.java new file mode 100644 index 0000000..aaba538 --- /dev/null +++ b/src/main/java/com/sparta/oneandzerobest/auth/entity/LoginResponse.java @@ -0,0 +1,12 @@ +package com.sparta.oneandzerobest.auth.entity; + + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class LoginResponse { + private String accessToken; + private String refreshToken; +} \ No newline at end of file diff --git a/src/main/java/com/sparta/oneandzerobest/auth/entity/User.java b/src/main/java/com/sparta/oneandzerobest/auth/entity/User.java index 23654da..4759a5c 100644 --- a/src/main/java/com/sparta/oneandzerobest/auth/entity/User.java +++ b/src/main/java/com/sparta/oneandzerobest/auth/entity/User.java @@ -5,11 +5,9 @@ import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; - +import java.time.LocalDateTime; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; -import java.util.Set; @Entity @Getter @@ -26,52 +24,54 @@ public class User implements UserDetails { // Spring Security의 UserDetails @Column(nullable = false) private String password; + @Column(nullable = false) + private String name; + @Column(nullable = false, unique = true) private String email; - - @ElementCollection(fetch = FetchType.EAGER) - private Set authorities; + @Column + private String introduction; @Column(nullable = false) - @Enumerated(value = EnumType.STRING) - private UserRoleEnum role; + private String statusCode; + + @Column + private String refreshToken; - public User(String username, String password, String email, UserRoleEnum role) { + @Column + private LocalDateTime statusChangeTime; + + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column + private LocalDateTime updatedAt; + + public User(String username, String password, String name, String email, String statusCode) { this.username = username; this.password = password; + this.name = name; this.email = email; - this.role = role; - this.authorities = Collections.singleton(role.name()); - } - - @Override - public Collection getAuthorities() { - Set grantedAuthorities = new HashSet<>(); - for (String authority : this.authorities) { - grantedAuthorities.add(() -> authority); - } - return grantedAuthorities; + this.statusCode = statusCode; + this.createdAt = LocalDateTime.now(); } - - @Override - public boolean isAccountNonExpired() { - return false; + public void setStatusCode(String statusCode) { + this.statusCode = statusCode; } - @Override - public boolean isAccountNonLocked() { - return false; + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; } @Override - public boolean isCredentialsNonExpired() { - return false; + public boolean isEnabled() { + return "정상".equals(this.statusCode); // 계정이 활성화된 상태인지 확인 } @Override - public boolean isEnabled() { - return false; + public Collection getAuthorities() { + return Collections.emptyList(); // 권한 관련 설정 } -} \ No newline at end of file +} diff --git a/src/main/java/com/sparta/oneandzerobest/auth/service/UserDetailsServiceImpl.java b/src/main/java/com/sparta/oneandzerobest/auth/service/UserDetailsServiceImpl.java index 87fb50e..ce6ae0b 100644 --- a/src/main/java/com/sparta/oneandzerobest/auth/service/UserDetailsServiceImpl.java +++ b/src/main/java/com/sparta/oneandzerobest/auth/service/UserDetailsServiceImpl.java @@ -10,6 +10,9 @@ import java.util.Optional; +/** + * UserDetailsService 구현제 서비스 + */ @Service public class UserDetailsServiceImpl implements UserDetailsService { diff --git a/src/main/java/com/sparta/oneandzerobest/auth/service/UserService.java b/src/main/java/com/sparta/oneandzerobest/auth/service/UserService.java index a9f1fd8..6104cd3 100644 --- a/src/main/java/com/sparta/oneandzerobest/auth/service/UserService.java +++ b/src/main/java/com/sparta/oneandzerobest/auth/service/UserService.java @@ -1,10 +1,19 @@ package com.sparta.oneandzerobest.auth.service; +import com.sparta.oneandzerobest.auth.dto.TokenResponseDto; import com.sparta.oneandzerobest.auth.entity.LoginRequest; +import com.sparta.oneandzerobest.auth.entity.LoginResponse; import com.sparta.oneandzerobest.auth.entity.SignupRequest; public interface UserService { + // 회원가입 void signup(SignupRequest signupRequest); - String login(LoginRequest loginRequest); - + // 로그인 + LoginResponse login(LoginRequest loginRequest); + // 로그아웃 + void logout(String username); + // 탈퇴 + void withdraw(String username, String password); + // 리프레시 토큰 + TokenResponseDto refresh(String refreshToken); } \ No newline at end of file diff --git a/src/main/java/com/sparta/oneandzerobest/auth/service/UserServiceImpl.java b/src/main/java/com/sparta/oneandzerobest/auth/service/UserServiceImpl.java index a3c650c..8cd6c7d 100644 --- a/src/main/java/com/sparta/oneandzerobest/auth/service/UserServiceImpl.java +++ b/src/main/java/com/sparta/oneandzerobest/auth/service/UserServiceImpl.java @@ -1,28 +1,20 @@ package com.sparta.oneandzerobest.auth.service; - -import com.sparta.oneandzerobest.auth.entity.LoginRequest; -import com.sparta.oneandzerobest.auth.entity.SignupRequest; -import com.sparta.oneandzerobest.auth.entity.User; -import com.sparta.oneandzerobest.auth.entity.UserRoleEnum; +import com.sparta.oneandzerobest.auth.dto.TokenResponseDto; +import com.sparta.oneandzerobest.auth.entity.*; import com.sparta.oneandzerobest.auth.repository.UserRepository; import com.sparta.oneandzerobest.auth.util.JwtUtil; import com.sparta.oneandzerobest.exception.InfoNotCorrectedException; import com.sparta.oneandzerobest.exception.InvalidPasswordException; -import com.sparta.oneandzerobest.exception.UnauthorizedException; import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; -import java.util.Optional; - @Service @Slf4j public class UserServiceImpl implements UserService { - private final UserRepository userRepository; - private final PasswordEncoder passwordEncoder; private final JwtUtil jwtUtil; @@ -32,67 +24,122 @@ public UserServiceImpl(UserRepository userRepository, PasswordEncoder passwordEn this.jwtUtil = jwtUtil; } - // ADMIN_TOKEN - private final String ADMIN_TOKEN = "e36f112d-c6f2-466f-aad8-14dcdc16360b"; - - // 회원가입 + /** + * 회원가입: ID, PW, Email + * @param signupRequest + */ + @Override public void signup(SignupRequest signupRequest) { - String username = signupRequest.getUsername(); + String authId = signupRequest.getUsername(); + String password = signupRequest.getPassword(); + String email = signupRequest.getEmail(); - // username 최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z), 숫자(0~9) - if (!username.matches("^[a-z0-9]{4,10}$")) { - throw new IllegalArgumentException("아이디는 최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z)와 숫자(0~9)로 구성되어야 합니다."); + if (!authId.matches("^[a-zA-Z0-9]{10,20}$")) { + throw new IllegalArgumentException("아이디는 최소 10자 이상, 20자 이하이며 알파벳 대소문자와 숫자로 구성되어야 합니다."); + } + if (!password.matches("^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#$%^&*])[a-zA-Z\\d!@#$%^&*]{10,}$")) { + throw new IllegalArgumentException("비밀번호는 최소 10자 이상이며 알파벳 대소문자, 숫자, 특수문자를 포함해야 합니다."); } - String encodedPassword = passwordEncoder.encode( signupRequest.getPassword()); + if (userRepository.findByUsername(authId).isPresent()) { + throw new InfoNotCorrectedException("중복된 사용자 ID가 존재합니다."); + } - // password 최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9) - if (!signupRequest.getPassword().matches("^(?=.*[a-zA-Z])(?=.*\\d)[a-zA-Z\\d]{8,15}$")) { - throw new IllegalArgumentException("비밀번호은 최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z)와 숫자(0~9)로 구성되어야 합니다."); + if (userRepository.findByEmail(email).isPresent()) { + throw new InfoNotCorrectedException("중복된 이메일이 존재합니다."); } + // 비밀번호 암호화 + String encodedPassword = passwordEncoder.encode(password); + User user = new User(authId, encodedPassword, signupRequest.getUsername(), email, "정상"); + userRepository.save(user); + } + + /** + * 로그인: ACCESS TOKEN, REFRESH TOKEN 생성 + * 비밀번호: 암호화 + * 탈퇴한 사람 분별 + * @param loginRequest + * @return + */ + @Override + public LoginResponse login(LoginRequest loginRequest) { + User user = userRepository.findByUsername(loginRequest.getUsername()) + .orElseThrow(() -> new InfoNotCorrectedException("사용자 ID와 비밀번호가 일치하지 않습니다.")); - // 회원 중복 확인 - Optional checkUsername = userRepository.findByUsername(username); - if (checkUsername.isPresent()) { - throw new InfoNotCorrectedException("중복된 사용자가 존재합니다."); + if (!passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) { + throw new InvalidPasswordException("사용자 ID와 비밀번호가 일치하지 않습니다."); } - // email 중복확인 - String email = signupRequest.getEmail(); - Optional checkEmail = userRepository.findByEmail(email); - if (checkEmail.isPresent()) { - throw new InfoNotCorrectedException("중복된 Email 입니다."); + if ("탈퇴".equals(user.getStatusCode())) { + throw new InfoNotCorrectedException("탈퇴한 사용자입니다."); } - // 사용자 ROLE 확인 - UserRoleEnum role = UserRoleEnum.USER; - if (signupRequest.isAdmin()) { - if (!ADMIN_TOKEN.equals(signupRequest.getAdminToken())) { - throw new UnauthorizedException("관리자 암호가 틀려 등록이 불가능합니다."); - } - role = UserRoleEnum.ADMIN; + String accessToken = jwtUtil.createAccessToken(user.getUsername()); + String refreshToken = jwtUtil.createRefreshToken(user.getUsername()); + + user.setRefreshToken(refreshToken); + userRepository.save(user); + + return LoginResponse.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } + + /** + * 로그아웃: 리프레시 토큰 삭제 + * @param username + */ + @Override + public void logout(String username) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new InfoNotCorrectedException("사용자를 찾을 수 없습니다.")); + + user.setRefreshToken(null); + userRepository.save(user); + } + + /** + * 회원 탈퇴 -> 리프레시 토큰 삭제, getStatusCode: 탈퇴 + * @param id: 아이디 + * @param password: 비밀번호 + */ + @Override + public void withdraw(String id, String password) { + User user = userRepository.findByUsername(id) + .orElseThrow(() -> new InfoNotCorrectedException("사용자를 찾을 수 없습니다.")); + + if (!passwordEncoder.matches(password, user.getPassword())) { + throw new InvalidPasswordException("비밀번호가 일치하지 않습니다."); } - // 새로운 사용자 객체 생성 - 등록 - User user = new User(username, encodedPassword, email, role); + if ("탈퇴".equals(user.getStatusCode())) { + throw new InfoNotCorrectedException("이미 탈퇴한 사용자입니다."); + } + + user.setStatusCode("탈퇴"); + user.setRefreshToken(null); userRepository.save(user); - log.warn("회원가입 성공: " + user.getUsername()); } - // 로그인 - public String login(LoginRequest loginRequest) { - User user = userRepository.findByUsername(loginRequest.getUsername()) - .orElseThrow(() -> new InfoNotCorrectedException("이름과 비밀번호가 일치하지 않습니다.")); + @Override + public TokenResponseDto refresh(String refreshToken) { + String username = jwtUtil.getUsernameFromToken(refreshToken); + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new InfoNotCorrectedException("사용자를 찾을 수 없습니다.")); - if (!passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) { - throw new InvalidPasswordException("비밀번호와 비밀번호가 비교하지 않음."); + if (!refreshToken.equals(user.getRefreshToken())) { + throw new InvalidPasswordException("리프레시 토큰이 유효하지 않습니다."); } - String token = jwtUtil.createAccessToken(user.getUsername()); - log.info("로그인 성공: 사용자 {}, 토큰 {}", user.getUsername(), token); - return token; + String newAccessToken = jwtUtil.createAccessToken(username); + String newRefreshToken = jwtUtil.createRefreshToken(username); + + user.setRefreshToken(newRefreshToken); + userRepository.save(user); + + return new TokenResponseDto(newAccessToken, newRefreshToken); } } - diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 313472c..0269dc2 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,7 +5,7 @@ spring: url: jdbc:mysql://localhost:3306/siumechu driver-class-name: com.mysql.cj.jdbc.Driver username: root - password: Ekalekal1@ + password: password jpa: hibernate: ddl-auto: update @@ -15,10 +15,10 @@ jwt: secret: key: e36f112d-c6f2-466f-aad8-14dcdc16360b token: - expiration: 3600000 + expiration: 1800000 refresh: token: - expiration: 604800000 + expiration: 1209600000 server: port: 8080