jwt 소스 추가
This commit is contained in:
171
src/main/java/com/kamco/cd/kamcoback/members/AuthController.java
Normal file
171
src/main/java/com/kamco/cd/kamcoback/members/AuthController.java
Normal file
@@ -0,0 +1,171 @@
|
||||
package com.kamco.cd.kamcoback.members;
|
||||
|
||||
import com.kamco.cd.kamcoback.auth.JwtTokenProvider;
|
||||
import com.kamco.cd.kamcoback.auth.RefreshTokenService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.time.Duration;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.ErrorResponse;
|
||||
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;
|
||||
|
||||
@Tag(name = "인증(Auth)", description = "로그인, 토큰 재발급, 로그아웃 API")
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@RequiredArgsConstructor
|
||||
public class AuthController {
|
||||
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final RefreshTokenService refreshTokenService;
|
||||
|
||||
@Value("${token.refresh-cookie-name}")
|
||||
private String refreshCookieName;
|
||||
|
||||
@Value("${token.refresh-cookie-secure:true}")
|
||||
private boolean refreshCookieSecure;
|
||||
|
||||
@PostMapping("/signin")
|
||||
@Operation(summary = "로그인", description = "사번 또는 이메일과 비밀번호로 로그인하여 액세스/리프레시 토큰을 발급합니다.")
|
||||
@ApiResponses({
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "로그인 성공",
|
||||
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "401",
|
||||
description = "ID 또는 비밀번호 불일치",
|
||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
})
|
||||
public ResponseEntity<TokenResponse> signin(
|
||||
@io.swagger.v3.oas.annotations.parameters.RequestBody(
|
||||
description = "로그인 요청 정보",
|
||||
required = true)
|
||||
@RequestBody
|
||||
SignInRequest request,
|
||||
HttpServletResponse response) {
|
||||
Authentication authentication =
|
||||
authenticationManager.authenticate(
|
||||
new UsernamePasswordAuthenticationToken(request.username(), request.password()));
|
||||
|
||||
String username = authentication.getName(); // UserDetailsService 에서 사용한 username
|
||||
|
||||
String accessToken = jwtTokenProvider.createAccessToken(username);
|
||||
String refreshToken = jwtTokenProvider.createRefreshToken(username);
|
||||
|
||||
// Redis에 RefreshToken 저장 (TTL = 7일)
|
||||
refreshTokenService.save(
|
||||
username, refreshToken, jwtTokenProvider.getRefreshTokenValidityInMs());
|
||||
|
||||
// HttpOnly + Secure 쿠키에 RefreshToken 저장
|
||||
ResponseCookie cookie =
|
||||
ResponseCookie.from(refreshCookieName, refreshToken)
|
||||
.httpOnly(true)
|
||||
.secure(refreshCookieSecure) // 로컬 개발에서 http만 쓰면 false 로 바꿔야 할 수도 있음
|
||||
.path("/")
|
||||
.maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs()))
|
||||
.sameSite("Strict")
|
||||
.build();
|
||||
|
||||
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
|
||||
|
||||
return ResponseEntity.ok(new TokenResponse(accessToken));
|
||||
}
|
||||
|
||||
@PostMapping("/refresh")
|
||||
@Operation(summary = "토큰 재발급", description = "리프레시 토큰으로 새로운 액세스/리프레시 토큰을 재발급합니다.")
|
||||
@ApiResponses({
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "재발급 성공",
|
||||
content = @Content(schema = @Schema(implementation = TokenResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "401",
|
||||
description = "만료되었거나 유효하지 않은 리프레시 토큰",
|
||||
content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
|
||||
})
|
||||
public ResponseEntity<TokenResponse> refresh(String refreshToken, HttpServletResponse response) {
|
||||
if (refreshToken == null || !jwtTokenProvider.isValidToken(refreshToken)) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
|
||||
String username = jwtTokenProvider.getSubject(refreshToken);
|
||||
|
||||
// Redis에 저장된 RefreshToken과 일치하는지 확인
|
||||
if (!refreshTokenService.validate(username, refreshToken)) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
|
||||
// 새 토큰 발급
|
||||
String newAccessToken = jwtTokenProvider.createAccessToken(username);
|
||||
String newRefreshToken = jwtTokenProvider.createRefreshToken(username);
|
||||
|
||||
// Redis 갱신
|
||||
refreshTokenService.save(
|
||||
username, newRefreshToken, jwtTokenProvider.getRefreshTokenValidityInMs());
|
||||
|
||||
// 쿠키 갱신
|
||||
ResponseCookie cookie =
|
||||
ResponseCookie.from(refreshCookieName, newRefreshToken)
|
||||
.httpOnly(true)
|
||||
.secure(refreshCookieSecure)
|
||||
.path("/")
|
||||
.maxAge(Duration.ofMillis(jwtTokenProvider.getRefreshTokenValidityInMs()))
|
||||
.sameSite("Strict")
|
||||
.build();
|
||||
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
|
||||
|
||||
return ResponseEntity.ok(new TokenResponse(newAccessToken));
|
||||
}
|
||||
|
||||
@PostMapping("/logout")
|
||||
@Operation(summary = "로그아웃", description = "현재 사용자의 토큰을 무효화(블랙리스트 처리 또는 리프레시 토큰 삭제)합니다.")
|
||||
@ApiResponses({
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "로그아웃 성공",
|
||||
content = @Content(schema = @Schema(implementation = Void.class)))
|
||||
})
|
||||
public ResponseEntity<Void> logout(Authentication authentication, HttpServletResponse response) {
|
||||
if (authentication != null) {
|
||||
String username = authentication.getName();
|
||||
// Redis에서 RefreshToken 삭제
|
||||
refreshTokenService.delete(username);
|
||||
}
|
||||
|
||||
// 쿠키 삭제 (Max-Age=0)
|
||||
ResponseCookie cookie =
|
||||
ResponseCookie.from(refreshCookieName, "")
|
||||
.httpOnly(true)
|
||||
.secure(refreshCookieSecure)
|
||||
.path("/")
|
||||
.maxAge(0)
|
||||
.sameSite("Strict")
|
||||
.build();
|
||||
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@Schema(description = "로그인 요청 DTO")
|
||||
public record SignInRequest(
|
||||
@Schema(description = "사번", example = "11111") String username,
|
||||
@Schema(description = "비밀번호", example = "kamco1234!") String password) {}
|
||||
|
||||
public record TokenResponse(String accessToken) {}
|
||||
}
|
||||
@@ -12,7 +12,9 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -40,8 +42,9 @@ public class MembersApiController {
|
||||
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@PostMapping("/list")
|
||||
public ApiResponseDto<Page<Basic>> getMemberList(@RequestBody MembersDto.SearchReq searchReq) {
|
||||
@GetMapping
|
||||
public ApiResponseDto<Page<Basic>> getMemberList(
|
||||
@ParameterObject MembersDto.SearchReq searchReq) {
|
||||
return ApiResponseDto.ok(membersService.findByMembers(searchReq));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.kamco.cd.kamcoback.members.dto;
|
||||
|
||||
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class MemberDetails implements UserDetails {
|
||||
|
||||
private final MemberEntity member;
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
// TODO: tb_member_role 에서 역할 꺼내서 권한으로 변환하고 싶으면 여기 구현
|
||||
// 예시 (나중에 MemberRoleEntity 보고 수정):
|
||||
// return member.getTbMemberRoles().stream()
|
||||
// .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getRoleName()))
|
||||
// .toList();
|
||||
|
||||
return List.of(); // 일단 빈 권한 리스트
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return member.getPassword(); // 암호화된 비밀번호
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
// 로그인 ID 로 무엇을 쓸지 선택
|
||||
// 1) 이메일 로그인:
|
||||
return member.getEmail();
|
||||
|
||||
// 2) 사번으로 로그인하고 싶으면:
|
||||
// return member.getEmployeeNo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonLocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCredentialsNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
// status 가 ACTIVE 일 때만 로그인 허용
|
||||
return "ACTIVE".equalsIgnoreCase(member.getStatus());
|
||||
}
|
||||
|
||||
public MemberEntity getMember() {
|
||||
return member;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.kamco.cd.kamcoback.members.dto;
|
||||
|
||||
import com.kamco.cd.kamcoback.common.enums.RoleType;
|
||||
import com.kamco.cd.kamcoback.common.utils.interfaces.EnumValid;
|
||||
import com.kamco.cd.kamcoback.common.utils.interfaces.JsonFormatDttm;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.kamco.cd.kamcoback.members.dto;
|
||||
|
||||
import com.kamco.cd.kamcoback.config.enums.EnumType;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum RoleType implements EnumType {
|
||||
ROLE_ADMIN("시스템 관리자"),
|
||||
ROLE_LABELER("라벨러"),
|
||||
ROLE_REVIEWER("검수자");
|
||||
|
||||
private final String desc;
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.kamco.cd.kamcoback.members.service;
|
||||
|
||||
import com.kamco.cd.kamcoback.auth.CustomUserDetails;
|
||||
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
|
||||
import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository;
|
||||
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 MemberDetailsService implements UserDetailsService {
|
||||
|
||||
private final MembersRepository membersRepository;
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
MemberEntity member =
|
||||
membersRepository
|
||||
.findByEmployeeNo(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("USER NOT FOUND"));
|
||||
|
||||
return new CustomUserDetails(member);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
package com.kamco.cd.kamcoback.members.service;
|
||||
|
||||
import com.kamco.cd.kamcoback.common.exception.CustomApiException;
|
||||
import com.kamco.cd.kamcoback.config.BCryptSaltGenerator;
|
||||
import com.kamco.cd.kamcoback.members.dto.MembersDto;
|
||||
import com.kamco.cd.kamcoback.members.dto.MembersDto.Basic;
|
||||
import com.kamco.cd.kamcoback.postgres.core.MembersCoreService;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.mindrot.jbcrypt.BCrypt;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -40,8 +42,12 @@ public class MembersService {
|
||||
|
||||
if (StringUtils.isNotBlank(updateReq.getPassword())) {
|
||||
|
||||
if (!this.isValidPassword(updateReq.getPassword())) {
|
||||
throw new CustomApiException("WRONG_PASSWORD", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(updateReq.getEmployeeNo())) {
|
||||
throw new HttpMessageNotReadableException("패스워드 변경시 사번은 필수 값입니다.");
|
||||
throw new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
// salt 생성, 사번이 salt
|
||||
@@ -55,4 +61,15 @@ public class MembersService {
|
||||
|
||||
membersCoreService.updateMembers(uuid, updateReq);
|
||||
}
|
||||
|
||||
/**
|
||||
* 대문자 1개 이상 소문자 1개 이상 숫자 1개 이상 특수문자(!@#$) 1개 이상
|
||||
*
|
||||
* @param password
|
||||
* @return
|
||||
*/
|
||||
private boolean isValidPassword(String password) {
|
||||
String regex = "^(?=.*[A-Z])(?=.*[a-z])(?=.*\\d)(?=.*[!@#$]).{8,20}$";
|
||||
return Pattern.matches(regex, password);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user