jwt 소스 추가

This commit is contained in:
2025-12-03 18:47:45 +09:00
parent c3c484442e
commit 7884416e75
33 changed files with 738 additions and 681 deletions

View File

@@ -1,151 +0,0 @@
package com.kamco.cd.kamcoback.auth;
import com.kamco.cd.kamcoback.auth.dto.AuthDto;
import com.kamco.cd.kamcoback.auth.dto.AuthDto.Basic;
import com.kamco.cd.kamcoback.auth.service.AuthService;
import com.kamco.cd.kamcoback.config.api.ApiResponseDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
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.validation.Valid;
import lombok.RequiredArgsConstructor;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "관리자 관리", description = "관리자 관리 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
public class AuthApiController {
private final AuthService authService;
@Operation(summary = "관리자 등록", description = "관리자를 등록 합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "관리자 등록 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PostMapping("/save")
public ApiResponseDto<Long> save(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "관리자 정보",
required = true,
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = AuthDto.SaveReq.class)))
@RequestBody
@Valid
AuthDto.SaveReq saveReq) {
return ApiResponseDto.createOK(authService.save(saveReq).getId());
}
@Operation(summary = "관리자 정보 수정", description = "관리자 정보를 수정 합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "관리자 정보 수정 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PutMapping("/update/{id}")
public ApiResponseDto<Long> update(@PathVariable Long id, @RequestBody AuthDto.SaveReq saveReq) {
return ApiResponseDto.createOK(authService.update(id, saveReq).getId());
}
@Operation(summary = "관리자 정보 탈퇴처리", description = "관리자 정보를 탈퇴처리 합니다.")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "201",
description = "관리자 탈퇴처리 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Long.class))),
@ApiResponse(responseCode = "400", description = "잘못된 요청 데이터", content = @Content),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@PutMapping("/withdrawal/{id}")
public ApiResponseDto<Long> withdrawal(@PathVariable Long id) {
return ApiResponseDto.deleteOk(authService.withdrawal(id).getId());
}
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "조회 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = AuthDto.Basic.class))),
@ApiResponse(responseCode = "404", description = "코드를 찾을 수 없음", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@Operation(summary = "관리자 상세조회", description = "관리자 정보를 조회 합니다.")
@GetMapping("/detail")
public ApiResponseDto<AuthDto.Basic> getDetail(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "관리자 목록 id",
required = true)
@RequestParam
Long id) {
return ApiResponseDto.ok(authService.getFindUserById(id));
}
@Operation(summary = "관리자 목록", description = "관리자 목록 조회")
@ApiResponses(
value = {
@ApiResponse(
responseCode = "200",
description = "검색 성공",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = Page.class))),
@ApiResponse(responseCode = "400", description = "잘못된 검색 조건", content = @Content),
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
})
@GetMapping("/list")
public ApiResponseDto<Page<Basic>> getUserList(
@Parameter(description = "관리자 이름") @RequestParam(required = false) String userNm,
@Parameter(description = "페이지 번호 (0부터 시작)", example = "0") @RequestParam(defaultValue = "0")
int page,
@Parameter(description = "페이지 크기", example = "20") @RequestParam(defaultValue = "20")
int size,
@Parameter(description = "정렬 조건 (형식: 필드명,방향)", example = "name,asc")
@RequestParam(required = false)
String sort) {
AuthDto.SearchReq searchReq = new AuthDto.SearchReq(userNm, page, size, sort);
Page<AuthDto.Basic> userList = authService.getUserList(searchReq);
return ApiResponseDto.ok(userList);
}
}

View File

@@ -0,0 +1,46 @@
package com.kamco.cd.kamcoback.auth;
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
import com.kamco.cd.kamcoback.postgres.repository.members.MembersRepository;
import lombok.RequiredArgsConstructor;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final MembersRepository membersRepository;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String rawPassword = authentication.getCredentials().toString();
// 1. 유저 조회
MemberEntity member =
membersRepository
.findByEmployeeNo(username)
.orElseThrow(() -> new BadCredentialsException("ID 또는 비밀번호가 일치하지 않습니다."));
// 2. jBCrypt + 커스텀 salt 로 저장된 패스워드 비교
if (!BCrypt.checkpw(rawPassword, member.getPassword())) {
throw new BadCredentialsException("ID 또는 비밀번호가 일치하지 않습니다.");
}
// 3. 인증 성공 → UserDetails 생성
CustomUserDetails userDetails = new CustomUserDetails(member);
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}

View File

@@ -0,0 +1,56 @@
package com.kamco.cd.kamcoback.auth;
import com.kamco.cd.kamcoback.postgres.entity.MemberEntity;
import java.util.Collection;
import java.util.Collections;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class CustomUserDetails implements UserDetails {
private final MemberEntity member;
public CustomUserDetails(MemberEntity member) {
this.member = member;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 권한을 Member에서 가져오는 경우 바꾸면 됩니다 — 일단 기본값
return Collections.emptyList();
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return String.valueOf(member.getUuid());
}
@Override
public boolean isAccountNonExpired() {
return true; // 추후 상태 필드에 따라 수정 가능
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return member.getStatus().equalsIgnoreCase("ACTIVE");
}
public MemberEntity getMember() {
return member;
}
}

View File

@@ -0,0 +1,21 @@
package com.kamco.cd.kamcoback.auth;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.security.crypto.password.PasswordEncoder;
public class JBCryptPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
throw new UnsupportedOperationException("custom salt 사용");
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword == null || encodedPassword.isBlank()) {
return false;
}
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
}

View File

@@ -0,0 +1,49 @@
package com.kamco.cd.kamcoback.auth;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = resolveToken(request);
if (token != null && jwtTokenProvider.isValidToken(token)) {
String username = jwtTokenProvider.getSubject(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
if (bearer != null && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}

View File

@@ -0,0 +1,72 @@
package com.kamco.cd.kamcoback.auth;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import javax.crypto.SecretKey;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access-token-validity-in-ms}")
private long accessTokenValidityInMs;
@Value("${jwt.refresh-token-validity-in-ms}")
private long refreshTokenValidityInMs;
private SecretKey key;
@PostConstruct
public void init() {
// HS256용 SecretKey
this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
public String createAccessToken(String subject) {
return createToken(subject, accessTokenValidityInMs);
}
public String createRefreshToken(String subject) {
return createToken(subject, refreshTokenValidityInMs);
}
private String createToken(String subject, long validityInMs) {
Date now = new Date();
Date expiry = new Date(now.getTime() + validityInMs);
return Jwts.builder().subject(subject).issuedAt(now).expiration(expiry).signWith(key).compact();
}
public String getSubject(String token) {
var claims = parseClaims(token).getPayload();
return claims.getSubject();
}
public boolean isValidToken(String token) {
try {
Jws<Claims> claims = parseClaims(token);
return !claims.getPayload().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
private Jws<Claims> parseClaims(String token) {
return Jwts.parser()
.verifyWith(key) // SecretKey 타입
.build()
.parseSignedClaims(token);
}
public long getRefreshTokenValidityInMs() {
return refreshTokenValidityInMs;
}
}

View File

@@ -0,0 +1,29 @@
package com.kamco.cd.kamcoback.auth;
import java.time.Duration;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class RefreshTokenService {
private final StringRedisTemplate redisTemplate;
private static final String PREFIX = "RT:";
public void save(String username, String refreshToken, long ttlMillis) {
ValueOperations<String, String> ops = redisTemplate.opsForValue();
ops.set(PREFIX + username, refreshToken, Duration.ofMillis(ttlMillis));
}
public boolean validate(String username, String refreshToken) {
String stored = redisTemplate.opsForValue().get(PREFIX + username);
return stored != null && stored.equals(refreshToken);
}
public void delete(String username) {
redisTemplate.delete(PREFIX + username);
}
}

View File

@@ -1,178 +0,0 @@
package com.kamco.cd.kamcoback.auth.dto;
import com.kamco.cd.kamcoback.common.utils.interfaces.JsonFormatDttm;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import java.time.ZonedDateTime;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@RequiredArgsConstructor
public class AuthDto {
@Getter
@Setter
public static class Basic {
private Long id;
private String userAuth;
private String userNm;
private String userId;
private String empId;
private String userEmail;
@JsonFormatDttm private ZonedDateTime createdDttm;
public Basic(
Long id,
String userAuth,
String userNm,
String userId,
String empId,
String userEmail,
ZonedDateTime createdDttm) {
this.id = id;
this.userAuth = userAuth;
this.userNm = userNm;
this.userId = userId;
this.empId = empId;
this.userEmail = userEmail;
this.createdDttm = createdDttm;
}
}
@Schema(name = "save request", description = "사용자 등록 정보")
@Getter
@Setter
public static class SaveReq {
@Schema(description = "구분", example = "관리자/라벨러/검수자 중 하나")
@NotBlank
private String userAuth;
@NotBlank
@Schema(description = "이름", example = "홍길동")
private String userNm;
@Schema(description = "ID", example = "gildong")
@NotBlank
private String userId;
@Schema(description = "PW", example = "password")
@NotBlank
private String userPw;
@Schema(description = "사번", example = "사번")
@NotBlank
private String empId;
@Schema(description = "이메일", example = "gildong@naver.com")
@NotBlank
private String userEmail;
public SaveReq(
String userAuth,
String userNm,
String userId,
String userPw,
String empId,
String userEmail) {
this.userAuth = userAuth;
this.userNm = userNm;
this.userId = userId;
this.userPw = userPw;
this.empId = empId;
this.userEmail = userEmail;
}
}
@Schema(name = "update request", description = "사용자 수정 정보")
@Getter
@Setter
public static class UpdateReq {
@Schema(description = "id", example = "1")
@NotBlank
private Long id;
@Schema(description = "구분", example = "관리자/라벨러/검수자 중 하나")
@NotBlank
private String userAuth;
@NotBlank
@Schema(description = "이름", example = "홍길동")
private String userNm;
@Schema(description = "ID", example = "gildong")
@NotBlank
private String userId;
@Schema(description = "PW", example = "password")
@NotBlank
private String userPw;
@Schema(description = "사번", example = "사번")
@NotBlank
private String empId;
@Schema(description = "이메일", example = "gildong@naver.com")
@NotBlank
private String userEmail;
public UpdateReq(
Long id,
String userAuth,
String userNm,
String userId,
String userPw,
String empId,
String userEmail) {
this.id = id;
this.userAuth = userAuth;
this.userNm = userNm;
this.userId = userId;
this.userPw = userPw;
this.empId = empId;
this.userEmail = userEmail;
}
}
@Getter
public static class User {
String userId;
String userPw;
}
@Schema(name = "UserSearchReq", description = "관리자 목록 요청 정보")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public static class SearchReq {
// 검색 조건
private String userNm;
// 페이징 파라미터
private int page = 0;
private int size = 20;
private String sort;
public Pageable toPageable() {
if (sort != null && !sort.isEmpty()) {
String[] sortParams = sort.split(",");
String property = sortParams[0];
Sort.Direction direction =
sortParams.length > 1 ? Sort.Direction.fromString(sortParams[1]) : Sort.Direction.ASC;
return PageRequest.of(page, size, Sort.by(direction, property));
}
return PageRequest.of(page, size);
}
}
}

View File

@@ -1,71 +0,0 @@
package com.kamco.cd.kamcoback.auth.service;
import com.kamco.cd.kamcoback.auth.dto.AuthDto;
import com.kamco.cd.kamcoback.auth.dto.AuthDto.Basic;
import com.kamco.cd.kamcoback.postgres.core.AuthCoreService;
import com.kamco.cd.kamcoback.postgres.entity.UserEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class AuthService {
private final AuthCoreService authCoreService;
/**
* 관리자 등록
*
* @param saveReq
* @return
*/
@Transactional
public UserEntity save(AuthDto.SaveReq saveReq) {
return authCoreService.save(saveReq);
}
/**
* 관리자 정보 수정
*
* @param id
* @param saveReq
* @return
*/
public UserEntity update(Long id, AuthDto.SaveReq saveReq) {
if (saveReq.getUserPw() != null) {}
return authCoreService.update(id, saveReq);
}
/**
* 관리자 삭제
*
* @param id
* @return
*/
public UserEntity withdrawal(Long id) {
return authCoreService.withdrawal(id);
}
/**
* 시퀀스 id로 관리자 조회
*
* @param id
* @return
*/
public AuthDto.Basic getFindUserById(Long id) {
return authCoreService.findUserById(id);
}
/**
* 관리자 목록 조회
*
* @param searchReq
* @return
*/
public Page<Basic> getUserList(AuthDto.SearchReq searchReq) {
return authCoreService.getUserList(searchReq);
}
}