[프로젝트 구조 세팅]

This commit is contained in:
2025-08-12 08:46:55 +09:00
parent 9468d19736
commit 2ff5a02906
59 changed files with 23686 additions and 45 deletions

View File

@@ -1,26 +1,17 @@
package com.bio.bio_backend.domain.user.member.controller;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.modelmapper.ModelMapper;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.DeleteMapping;
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.RestController;
import jakarta.validation.Valid;
import com.bio.bio_backend.domain.user.member.dto.MemberDTO;
import com.bio.bio_backend.domain.user.member.dto.CreateMemberRequestDTO;
import com.bio.bio_backend.domain.user.member.dto.MemberDto;
import com.bio.bio_backend.domain.user.member.dto.CreateMemberRequestDto;
import com.bio.bio_backend.domain.user.member.dto.CreateMemberResponseDto;
import com.bio.bio_backend.domain.user.member.service.MemberService;
import lombok.RequiredArgsConstructor;
@@ -42,20 +33,20 @@ public class MemberController {
}
@PostMapping("/join")
public ResponseEntity<CreateMemberResponseDto> createMember(@RequestBody @Valid CreateMemberRequestDTO requestDto) {
public ResponseEntity<Long> createMember(@RequestBody @Valid CreateMemberRequestDto requestDto) {
// RequestMember를 MemberDTO로 변환
MemberDTO member = new MemberDTO();
MemberDto member = new MemberDto();
member.setId(requestDto.getUserId());
member.setPw(requestDto.getPassword());
int oid = memberService.createMember(member);
long oid = memberService.createMember(member);
// 생성된 회원 정보를 조회하여 응답
MemberDTO createdMember = memberService.selectMember(oid);
CreateMemberResponseDto responseDto = mapper.map(createdMember, CreateMemberResponseDto.class);
//MemberDto createdMember = memberService.selectMember(oid);
//CreateMemberResponseDto responseDto = mapper.map(createdMember, CreateMemberResponseDto.class);
return ResponseEntity.status(HttpStatus.CREATED).body(responseDto);
return ResponseEntity.status(HttpStatus.CREATED).body(oid);
}
// @PostMapping("/member/list")

View File

@@ -6,7 +6,7 @@ import lombok.Data;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CreateMemberRequestDTO {
public class CreateMemberRequestDto {
@NotBlank(message = "사용자 ID는 필수입니다")
private String userId;

View File

@@ -0,0 +1,15 @@
package com.bio.bio_backend.domain.user.member.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class LoginRequestDto {
@NotNull(message = "ID cannot be null")
private String id;
@NotNull(message = "Password cannot be null")
private String pw;
private int loginLogFlag;
}

View File

@@ -0,0 +1,24 @@
package com.bio.bio_backend.domain.user.member.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.sql.Date;
import java.sql.Timestamp;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class LoginResponseDto {
private String id;
private String role;
private String status;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private Date regAt;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
private Timestamp lastLoginAt;
}

View File

@@ -16,7 +16,7 @@ import lombok.Data;
/**
* 회원
*/
public class MemberDTO implements UserDetails {
public class MemberDto implements UserDetails {
/**
* 시퀀스 (PK)
*/

View File

@@ -5,23 +5,23 @@ import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
import com.bio.bio_backend.domain.user.member.dto.MemberDTO;
import com.bio.bio_backend.domain.user.member.dto.MemberDto;
@Mapper
public interface MemberMapper {
int createMember(MemberDTO memberDTO);
int createMember(MemberDto memberDTO);
MemberDTO loadUserByUsername(String id);
MemberDto loadUserByUsername(String id);
void updateRefreshToken(MemberDTO memberDTO);
void updateRefreshToken(MemberDto memberDTO);
String getRefreshToken(String id);
int deleteRefreshToken(String id);
List<MemberDTO> selectMemberList(Map<String, String> params);
List<MemberDto> selectMemberList(Map<String, String> params);
MemberDTO selectMemberBySeq(int seq);
MemberDto selectMemberBySeq(long seq);
int updateMember(MemberDTO member);
int updateMember(MemberDto member);
}

View File

@@ -6,25 +6,25 @@ import java.util.Map;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import com.bio.bio_backend.domain.user.member.dto.MemberDTO;
import com.bio.bio_backend.domain.user.member.dto.MemberDto;
public interface MemberService extends UserDetailsService {
UserDetails loadUserByUsername(String id);
int createMember(MemberDTO memberDTO);
long createMember(MemberDto memberDTO);
void updateRefreshToken(MemberDTO memberDTO);
void updateRefreshToken(MemberDto memberDTO);
String getRefreshToken(String id);
int deleteRefreshToken(String id);
List<MemberDTO> selectMemberList(Map<String, String> params);
List<MemberDto> selectMemberList(Map<String, String> params);
MemberDTO selectMember(int seq);
MemberDto selectMember(long seq);
int updateMember(MemberDTO member);
int updateMember(MemberDto member);
int deleteMember(MemberDTO member);
int deleteMember(MemberDto member);
}

View File

@@ -1,6 +1,6 @@
package com.bio.bio_backend.domain.user.member.service;
import com.bio.bio_backend.domain.user.member.dto.MemberDTO;
import com.bio.bio_backend.domain.user.member.dto.MemberDto;
import com.bio.bio_backend.domain.user.member.entity.Member;
import com.bio.bio_backend.domain.user.member.mapper.MemberMapper;
import com.bio.bio_backend.domain.user.member.repository.MemberRepository;
@@ -27,7 +27,7 @@ public class MemberServiceImpl implements MemberService {
@Override
public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException {
MemberDTO member = memberMapper.loadUserByUsername(id);
MemberDto member = memberMapper.loadUserByUsername(id);
if (member == null) {
throw new UsernameNotFoundException("User not found with id : " + id);
@@ -37,7 +37,7 @@ public class MemberServiceImpl implements MemberService {
}
@Override
public int createMember(MemberDTO memberDTO) {
public long createMember(MemberDto memberDTO) {
// JPA Entity를 사용하여 회원 생성
Member member = Member.builder()
.userId(memberDTO.getId())
@@ -50,11 +50,11 @@ public class MemberServiceImpl implements MemberService {
Member savedMember = memberRepository.save(member);
// 저장된 회원의 oid를 반환
return savedMember.getOid().intValue();
return savedMember.getOid();
}
@Override
public void updateRefreshToken(MemberDTO memberDTO) {
public void updateRefreshToken(MemberDto memberDTO) {
memberMapper.updateRefreshToken(memberDTO);
}
@@ -69,22 +69,40 @@ public class MemberServiceImpl implements MemberService {
}
@Override
public List<MemberDTO> selectMemberList(Map<String, String> params) {
public List<MemberDto> selectMemberList(Map<String, String> params) {
return memberMapper.selectMemberList(params);
}
@Override
public MemberDTO selectMember(int seq) {
return memberMapper.selectMemberBySeq(seq);
public MemberDto selectMember(long seq) {
// JPA 레파지토리를 사용하여 회원 조회
Member member = memberRepository.findById(seq)
.orElseThrow(() -> new RuntimeException("회원을 찾을 수 없습니다. seq: " + seq));
// Member 엔티티를 MemberDto로 변환하여 반환
MemberDto memberDto = new MemberDto();
memberDto.setSeq(member.getOid().intValue()); // Long을 int로 안전하게 변환
memberDto.setId(member.getUserId());
memberDto.setPw(member.getPassword());
memberDto.setRole(member.getRole());
memberDto.setStatus(member.getStatus());
memberDto.setRegAt(java.sql.Timestamp.valueOf(member.getCreatedAt())); // LocalDateTime을 Timestamp로 변환
memberDto.setUdtAt(java.sql.Timestamp.valueOf(member.getUpdatedAt()));
if (member.getLastLoginAt() != null) {
memberDto.setLastLoginAt(java.sql.Timestamp.valueOf(member.getLastLoginAt()));
}
memberDto.setRefreshToken(member.getRefreshToken());
return memberDto;
}
@Override
public int updateMember(MemberDTO member) {
public int updateMember(MemberDto member) {
return memberMapper.updateMember(member);
}
@Override
public int deleteMember(MemberDTO member) {
public int deleteMember(MemberDto member) {
member.setStatus(MemberConstants.MEMBER_INACTIVE);

View File

@@ -0,0 +1,26 @@
package com.bio.bio_backend.global.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowCredentials(true);
config.addExposedHeader("Authorization");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}

View File

@@ -0,0 +1,39 @@
package com.bio.bio_backend.global.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.bio.bio_backend.global.utils.ApiResponseCode;
import lombok.Data;
import lombok.RequiredArgsConstructor;
//공통 response Class
@Data
@RequiredArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CustomApiResponse<T> {
private int code;
private String message;
private String description;
private T data;
private static final int SUCCESS = 200;
private CustomApiResponse(int code, String message, String description, T data){
this.code = code;
this.message = message;
this.description = description;
this.data = data;
}
public static <T> CustomApiResponse<T> success(ApiResponseCode responseCode, T data) {
return new CustomApiResponse<T>(SUCCESS, responseCode.name(), responseCode.getDescription(), data);
}
public static <T> CustomApiResponse<T> fail(ApiResponseCode responseCode, T data) {
return new CustomApiResponse<T>(responseCode.getStatusCode(), responseCode.name(), responseCode.getDescription(), data);
}
}

View File

@@ -3,6 +3,7 @@ package com.bio.bio_backend.global.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.GenericGenerator;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@@ -24,7 +25,11 @@ public abstract class BaseEntity {
* 자동 증가하는 Long 타입으로 설정
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@GeneratedValue(generator = "customOidGenerator")
@GenericGenerator(
name = "customOidGenerator",
type = com.bio.bio_backend.global.utils.CustomIdGenerator.class
)
@Column(name = "oid", nullable = false)
private Long oid;

View File

@@ -0,0 +1,53 @@
package com.bio.bio_backend.global.exception;
import java.io.IOException;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import com.bio.bio_backend.global.dto.CustomApiResponse;
import com.bio.bio_backend.global.utils.ApiResponseCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor
@Slf4j
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
//Spring Security login -> filter 수행 -> ExceptionHandler 적용불가로 별도 FailureHandler 를 통하여 Error 응답 처리
private final ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.info("exception : " + exception.toString());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
CustomApiResponse<String> apiResponse;
if (exception instanceof UsernameNotFoundException) {
apiResponse = CustomApiResponse.fail(ApiResponseCode.USER_NOT_FOUND, null);
} else if (exception instanceof BadCredentialsException) {
apiResponse = CustomApiResponse.fail(ApiResponseCode.AUTHENTICATION_FAILED, null);
} else {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
apiResponse = CustomApiResponse.fail(ApiResponseCode.INTERNAL_SERVER_ERROR, null);
}
String jsonResponse = objectMapper.writeValueAsString(apiResponse);
response.getWriter().write(jsonResponse);
}
}

View File

@@ -0,0 +1,74 @@
package com.bio.bio_backend.global.exception;
import java.util.Objects;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.security.SignatureException;
import com.bio.bio_backend.global.dto.CustomApiResponse;
import com.bio.bio_backend.global.utils.ApiResponseCode;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(HttpMessageNotReadableException.class)
public CustomApiResponse<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
if (Objects.requireNonNull(e.getMessage()).contains("JSON parse error")) {
return CustomApiResponse.fail(ApiResponseCode.COMMON_FORMAT_WRONG, null);
}
return CustomApiResponse.fail(ApiResponseCode.COMMON_BAD_REQUEST, null);
}
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public CustomApiResponse<Void> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
return CustomApiResponse.fail(ApiResponseCode.COMMON_METHOD_NOT_ALLOWED, null);
}
@ExceptionHandler(NoResourceFoundException.class)
public CustomApiResponse<Void> handleNoResourceFoundException(NoResourceFoundException e){
return CustomApiResponse.fail(ApiResponseCode.COMMON_NOT_FOUND, null);
}
@ExceptionHandler(IllegalArgumentException.class)
public CustomApiResponse<Void> handleIllegalArgumentException(IllegalArgumentException e){
return CustomApiResponse.fail(ApiResponseCode.ARGUMENT_NOT_VALID, null);
}
@ExceptionHandler(IndexOutOfBoundsException.class)
public CustomApiResponse<Void> handleIndexOutOfBoundsException(IndexOutOfBoundsException e){
return CustomApiResponse.fail(ApiResponseCode.INDEX_OUT_OF_BOUND, null);
}
@ExceptionHandler(SignatureException.class)
public CustomApiResponse<Void> handleSignatureException(SignatureException e) {
return CustomApiResponse.fail(ApiResponseCode.JWT_SIGNATURE_MISMATCH, null);
}
@ExceptionHandler(MalformedJwtException.class)
public CustomApiResponse<Void> handleMalformedJwtException(MalformedJwtException e) {
return CustomApiResponse.fail(ApiResponseCode.JWT_SIGNATURE_MISMATCH, null);
}
@ExceptionHandler(JwtException.class)
public CustomApiResponse<Void> handleJwtExceptionException(JwtException e) {
return CustomApiResponse.fail(ApiResponseCode.JWT_TOKEN_NULL, null);
}
@ExceptionHandler(ExpiredJwtException.class)
public CustomApiResponse<Void> handleExpiredJwtException(ExpiredJwtException e) {
return CustomApiResponse.fail(ApiResponseCode.JWT_TOKEN_EXPIRED, null);
}
@ExceptionHandler(JsonProcessingException.class)
public CustomApiResponse<Void> handleExpiredJwtException(JsonProcessingException e) {
return CustomApiResponse.fail(ApiResponseCode.JSON_PROCESSING_EXCEPTION, null);
}
}

View File

@@ -0,0 +1,42 @@
package com.bio.bio_backend.global.exception;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import com.bio.bio_backend.global.dto.CustomApiResponse;
import com.bio.bio_backend.global.utils.ApiResponseCode;
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
private final HandlerExceptionResolver resolver;
public JwtAccessDeniedHandler(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
this.resolver = resolver;
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
Exception exception = (Exception) request.getAttribute("exception");
if (exception != null) {
resolver.resolveException(request, response, null, exception);
} else {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getWriter(),
CustomApiResponse.fail(ApiResponseCode.COMMON_FORBIDDEN, null));
}
}
}

View File

@@ -0,0 +1,35 @@
package com.bio.bio_backend.global.exception;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import java.io.IOException;
@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final HandlerExceptionResolver resolver;
public JwtAuthenticationEntryPoint(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
this.resolver = resolver;
}
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Exception exception = (Exception) request.getAttribute("exception");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
if (exception == null) {
return;
}
resolver.resolveException(request, response, null, exception);
}
}

View File

@@ -0,0 +1,57 @@
//package com.bio.bio_backend.filter;
//
//import java.io.IOException;
//import java.sql.Timestamp;
//
//import org.springframework.security.core.Authentication;
//import org.springframework.security.core.context.SecurityContextHolder;
//import org.springframework.web.filter.OncePerRequestFilter;
//
//import jakarta.servlet.FilterChain;
//import jakarta.servlet.ServletException;
//import jakarta.servlet.http.HttpServletRequest;
//import jakarta.servlet.http.HttpServletResponse;
//import com.bio.bio_backend.domain.common.dto.AccessLogDTO;
//import com.bio.bio_backend.domain.common.service.AccessLogService;
//import com.bio.bio_backend.domain.user.member.dto.MemberDTO;
//import com.bio.bio_backend.global.utils.HttpUtils;
//
//public class HttpLoggingFilter extends OncePerRequestFilter {
//
//// private AccessLogService accessLogService;
// private HttpUtils httpUtils;
//
// public HttpLoggingFilter(AccessLogService accessLogService, HttpUtils httpUtils) {
// this.accessLogService = accessLogService;
// this.httpUtils = httpUtils;
// }
//
// @Override
// protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
// throws ServletException, IOException {
// // Request 요청 시간
// Long startedAt = System.currentTimeMillis();
// filterChain.doFilter(request, response);
// Long finishedAt = System.currentTimeMillis();
//
// Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// MemberDTO member = null;
// if(auth != null && auth.getPrincipal() instanceof MemberDTO) {
// member = (MemberDTO) auth.getPrincipal();
// }
//
// AccessLogDTO log = new AccessLogDTO();
// log.setMbrSeq(member == null ? -1 : member.getSeq());
// log.setType(httpUtils.getResponseType(response.getContentType()));
// log.setMethod(request.getMethod());
// log.setIp(httpUtils.getClientIp());
// log.setUri(request.getRequestURI());
// log.setReqAt(new Timestamp(startedAt));
// log.setResAt(new Timestamp(finishedAt));
// log.setElapsedTime(finishedAt - startedAt);
// log.setResStatus(String.valueOf(response.getStatus()));
//
// accessLogService.createAccessLog(log);
// }
//
//}

View File

@@ -0,0 +1,120 @@
package com.bio.bio_backend.global.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import com.bio.bio_backend.global.dto.CustomApiResponse;
import com.bio.bio_backend.domain.user.member.dto.LoginRequestDto;
import com.bio.bio_backend.domain.user.member.dto.LoginResponseDto;
import com.bio.bio_backend.domain.user.member.dto.MemberDto;
import com.bio.bio_backend.domain.user.member.service.MemberService;
import com.bio.bio_backend.global.utils.ApiResponseCode;
import com.bio.bio_backend.global.utils.JwtUtils;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.modelmapper.ModelMapper;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.io.IOException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.Objects;
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final MemberService memberService;
private final ModelMapper modelMapper;
private final JwtUtils jwtUtils;
private final Environment env;
// 사용자 login 인증 처리
@SneakyThrows
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
LoginRequestDto req = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);
// UsernamePasswordAuthenticationToken authToken;
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(req.getId(), req.getPw());
/*
if (req.getLoginLogFlag() == 1) {
authToken = new UsernamePasswordAuthenticationToken("admin2", "test123!"); // 비밀번호는 실제 비밀번호로 설정해야 함
} else {
throw new AuthenticationServiceException("login fail");
}
*/
return authenticationManager.authenticate(authToken);
}
// 사용자 인증 성공 후 token 발급
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authResult) throws IOException, ServletException {
UserDetails userDetails = (UserDetails) authResult.getPrincipal();
MemberDto member = (MemberDto) userDetails;
String accessToken = jwtUtils.generateToken(userDetails.getUsername(), member.getRole(),
Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_access"))));
String refreshToken = jwtUtils.generateToken(userDetails.getUsername(), member.getRole(),
Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_refresh"))));
member.setRefreshToken(refreshToken);
member.setLastLoginAt(Timestamp.valueOf(LocalDateTime.now()));
memberService.updateRefreshToken(member);
// Refresh 토큰 쿠키 저장
Cookie refreshTokenCookie = new Cookie("RefreshToken", refreshToken);
refreshTokenCookie.setHttpOnly(true);
refreshTokenCookie.setSecure(false);
refreshTokenCookie.setPath("/");
refreshTokenCookie.setMaxAge(Integer.parseInt(env.getProperty("token.expiration_time_refresh")));
// JWT 토큰 전달
response.setHeader("Authorization", "Bearer " + accessToken);
// response.addCookie(refreshTokenCookie);
response.addHeader("Set-Cookie",
String.format("%s=%s; HttpOnly; Secure; Path=/; Max-Age=%d; SameSite=None",
refreshTokenCookie.getName(),
refreshTokenCookie.getValue(),
refreshTokenCookie.getMaxAge()));
SecurityContextHolderStrategy contextHolder = SecurityContextHolder.getContextHolderStrategy();
SecurityContext context = contextHolder.createEmptyContext();
context.setAuthentication(authResult);
contextHolder.setContext(context);
LoginResponseDto memberData = modelMapper.map(member, LoginResponseDto.class);
// login 성공 메시지 전송
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
new ObjectMapper().writeValue(response.getWriter(),
CustomApiResponse.success(ApiResponseCode.LOGIN_SUCCESSFUL, memberData));
}
}

View File

@@ -0,0 +1,104 @@
package com.bio.bio_backend.global.security;
import java.io.IOException;
import java.util.Arrays;
import java.util.Objects;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import com.bio.bio_backend.global.dto.CustomApiResponse;
import com.bio.bio_backend.domain.user.member.service.MemberService;
import com.bio.bio_backend.global.utils.ApiResponseCode;
import com.bio.bio_backend.global.utils.JwtUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor
@Slf4j
public class JwtTokenFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;
private final MemberService memberService;
private final Environment env;
private final UriAllowFilter uriAllowFilter;
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
return uriAllowFilter.authExceptionAllow(request);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String accessToken = jwtUtils.extractAccessJwtFromRequest(request);
String refreshToken = jwtUtils.extractRefreshJwtFromCookie(request);
if(accessToken == null){
sendJsonResponse(response, CustomApiResponse.fail(ApiResponseCode.JWT_TOKEN_NULL, null));
return;
}
try {
// 토큰 유효성 검사
try {
if (jwtUtils.validateAccessToken(accessToken)) {
String username = jwtUtils.extractUsername(accessToken);
UserDetails userDetails = memberService.loadUserByUsername(username);
if (userDetails != null) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
return;
}
}
} catch (ExpiredJwtException ignored) {
// Access Token이 만료된 경우에만 ignored >> Refresh Token 검증 수행
}
// Refresh Token 유효성 검사
if (refreshToken != null && jwtUtils.validateRefreshToken(refreshToken)) {
String username = jwtUtils.extractUsername(refreshToken);
String role = (String) jwtUtils.extractAllClaims(refreshToken).get("role");
String newAccessToken = jwtUtils.generateToken(username, role,
Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_access"))));
// 새로운 Access Token을 응답 헤더에 설정
response.setHeader("Authorization", "Bearer " + newAccessToken);
filterChain.doFilter(request, response);
} else {
sendJsonResponse(response, CustomApiResponse.fail(ApiResponseCode.All_TOKEN_INVALID, null));
}
} catch (Exception e) {
request.setAttribute("exception", e);
}
filterChain.doFilter(request, response);
}
private void sendJsonResponse(HttpServletResponse response, CustomApiResponse<?> apiResponse) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.setStatus(apiResponse.getCode());
ObjectMapper objectMapper = new ObjectMapper();
String jsonResponse = objectMapper.writeValueAsString(apiResponse);
response.getWriter().write(jsonResponse);
}
}

View File

@@ -0,0 +1,32 @@
package com.bio.bio_backend.global.security;
import java.util.Arrays;
import java.util.regex.Pattern;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
@Component
public class UriAllowFilter {
@Autowired
// ProgramService programService;
public boolean authExceptionAllow(HttpServletRequest request) {
// 임시로 모든 요청 허용
return true;
// String[] allowUrl = programService.getAuthException();
// boolean success = Arrays.stream(allowUrl).anyMatch(pattern -> matches(pattern, request.getRequestURI()));
// return success;
}
boolean matches(String pattern, String uri) {
String regex = pattern.replace("/**", "(/.*)?").replace("*", ".*");
return Pattern.matches(regex, uri);
}
}

View File

@@ -0,0 +1,104 @@
package com.bio.bio_backend.global.security;
import org.modelmapper.ModelMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.CorsFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.Filter;
//import com.bio.bio_backend.domain.common.service.AccessLogService;
import com.bio.bio_backend.domain.user.member.service.MemberService;
import com.bio.bio_backend.global.constants.MemberConstants;
import com.bio.bio_backend.global.exception.CustomAuthenticationFailureHandler;
import com.bio.bio_backend.global.exception.JwtAccessDeniedHandler;
import com.bio.bio_backend.global.exception.JwtAuthenticationEntryPoint;
//import com.bio.bio_backend.global.filter.HttpLoggingFilter;
import com.bio.bio_backend.global.utils.HttpUtils;
import com.bio.bio_backend.global.utils.JwtUtils;
import lombok.RequiredArgsConstructor;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurity {
private final MemberService memberService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final JwtUtils jwtUtils;
// private final AccessLogService accessLogService;
private final CorsFilter corsFilter;
private final ModelMapper mapper;
private final ObjectMapper objectMapper;
private final HttpUtils httpUtils;
private final Environment env;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
private final UriAllowFilter uriAllowFilter;
private JwtAuthenticationFilter getJwtAuthenticationFilter(AuthenticationManager authenticationManager) throws Exception {
JwtAuthenticationFilter filter = new JwtAuthenticationFilter(authenticationManager, memberService, mapper, jwtUtils, env);
filter.setFilterProcessesUrl("/login"); // 로그인 EndPoint
filter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler(objectMapper));
return filter;
}
private Filter getJwtTokenFilter() {
return new JwtTokenFilter(jwtUtils, memberService, env, uriAllowFilter);
}
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// AuthenticationManager 설정
AuthenticationManagerBuilder authenticationManagerBuilder =
http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.userDetailsService(memberService).passwordEncoder(bCryptPasswordEncoder);
AuthenticationManager authenticationManager = authenticationManagerBuilder.build();
http.csrf(AbstractHttpConfigurer::disable) //csrf 비활성화
.authorizeHttpRequests(request -> //request 허용 설정
request
.anyRequest().permitAll() // 모든 요청 허용
// .requestMatchers("/ws/**").permitAll()
// .requestMatchers("/admin/**", "/join").hasAnyAuthority(MemberConstants.ROLE_ADMIN)
// .requestMatchers("/member/**").hasAnyAuthority(MemberConstants.ROLE_MEMBER)
// .anyRequest().authenticated()
)
.authenticationManager(authenticationManager)
.logout(AbstractHttpConfigurer::disable);
// 예외 처리 핸들링
http.exceptionHandling((exceptionConfig) ->
exceptionConfig
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
);
// http
// .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
// .addFilterBefore(new HttpLoggingFilter(accessLogService, httpUtils), UsernamePasswordAuthenticationFilter.class)
// .addFilterBefore(getJwtAuthenticationFilter(authenticationManager), UsernamePasswordAuthenticationFilter.class)
// .addFilterBefore(getJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
// .sessionManagement(httpSecuritySessionManagementConfigurer -> //Session 사용 X
// httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}

View File

@@ -0,0 +1,71 @@
package com.bio.bio_backend.global.utils;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
/*
* API 관련 RESPONSE ENUM
*/
@Getter
@AllArgsConstructor
public enum ApiResponseCode {
/*login & logout*/
// 200 OK
LOGIN_SUCCESSFUL(HttpStatus.OK.value(), "Login successful"),
LOGOUT_SUCCESSFUL(HttpStatus.OK.value(), "Logout successful"),
USER_INFO_CHANGE(HttpStatus.OK.value(), "User info update successful"),
USER_DELETE_SUCCESSFUL(HttpStatus.OK.value(), "User delete is successful"),
// 401 Unauthorized
USER_NOT_FOUND(HttpStatus.UNAUTHORIZED.value(), "User not found. Authentication failed"),
AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED.value(), "Password is invalid"),
/*auth*/
// 401 Unauthorized
JWT_SIGNATURE_MISMATCH(HttpStatus.UNAUTHORIZED.value(), "JWT signature does not match. Authentication failed"),
JWT_TOKEN_NULL(HttpStatus.UNAUTHORIZED.value(), "JWT token is null"),
JWT_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "Token is Expired"),
All_TOKEN_INVALID(HttpStatus.UNAUTHORIZED.value(), "Access and Refresh tokens are expired or invalid"),
/*공통 Code*/
// 400 Bad Request
COMMON_BAD_REQUEST(HttpStatus.BAD_REQUEST.value(), "Required request body is missing or Error"),
COMMON_FORMAT_WRONG(HttpStatus.BAD_REQUEST.value(), "Request format is incorrect"),
// 401 Unauthorized
COMMON_UNAUTHORIZED(HttpStatus.UNAUTHORIZED.value(), "Unauthorized"),
// 403 Forbidden
COMMON_FORBIDDEN(HttpStatus.FORBIDDEN.value(), "Access is denied"),
// 404 Not Found
COMMON_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "Resource is not found"),
// 405 Method Not Allowed
COMMON_METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED.value(), "Method not Allowed"),
// 500 Internal Server Error
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR.value(), "An error occurred on the server"),
ARGUMENT_NOT_VALID(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Argument is not valid"),
INDEX_OUT_OF_BOUND(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Index out of bounds for length"),
JSON_PROCESSING_EXCEPTION(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Check if it is a valid JSON format");
private final int statusCode;
private final String description;
}

View File

@@ -0,0 +1,18 @@
package com.bio.bio_backend.global.utils;
import java.io.Serializable;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.id.IdentifierGenerator;
import com.bio.bio_backend.global.utils.OidUtil;
public class CustomIdGenerator implements IdentifierGenerator {
private static final long serialVersionUID = 1L;
@Override
public Serializable generate(SharedSessionContractImplementor session, Object object) {
return OidUtil.generateOid(); // 재사용
}
}

View File

@@ -0,0 +1,64 @@
package com.bio.bio_backend.global.utils;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
@Component
public class HttpUtils {
public String getClientIp() {
String ip = "";
HttpServletRequest request =
((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-RealIP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("REMOTE_ADDR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
public String getUri() {
HttpServletRequest request =
((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
return request.getRequestURI();
}
public String getResponseType(String contentType) {
if(contentType == null) {
return "";
} else if(contentType.contains("text/html")) {
return "PAGE";
} else if (contentType.contains("application/json")) {
return "API";
};
return "";
}
}

View File

@@ -0,0 +1,90 @@
package com.bio.bio_backend.global.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import com.bio.bio_backend.domain.user.member.service.MemberService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import java.util.Date;
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtUtils {
private final MemberService memberService;
@Value("${token.secret_key}")
private String SECRET_KEY;
private SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY);
return Keys.hmacShaKeyFor(keyBytes);
}
// Token 생성
public String generateToken(String username, String role, long expirationTime) {
return Jwts.builder()
.subject(username)
.claim("role", role)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(getSigningKey())
.compact();
}
// Token 검증
public Boolean validateAccessToken(String token) {
return isTokenExpired(token);
}
public Boolean validateRefreshToken(String token) {
String saveToken = memberService.getRefreshToken(extractUsername(token));
return (saveToken.equals(token) && isTokenExpired(token));
}
private boolean isTokenExpired(String token) {
return !extractAllClaims(token).getExpiration().before(new Date());
}
public String extractUsername(String token) {
return extractAllClaims(token).getSubject();
}
public Claims extractAllClaims(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token).getPayload();
}
public String extractAccessJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7); // "Bearer " 제거
}
return bearerToken;
}
public String extractRefreshJwtFromCookie(HttpServletRequest request) {
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
if ("RefreshToken".equals(cookie.getName())) {
return cookie.getValue();
}
}
}
return null;
}
}

View File

@@ -0,0 +1,38 @@
package com.bio.bio_backend.global.utils;
import java.util.concurrent.atomic.AtomicInteger;
public class OidUtil {
private static final int MAX_SEQUENCE = 999;
private static volatile Long lastTimestamp = null;
private static AtomicInteger sequence = new AtomicInteger(0);
private static final int SERVER_ID = resolveServerId();
public static synchronized long generateOid() {
long now = System.currentTimeMillis();
if (lastTimestamp == null || now != lastTimestamp) {
lastTimestamp = now;
sequence.set(SERVER_ID);
}
int index = sequence.getAndAdd(2);
if (index > MAX_SEQUENCE) {
now += index / 1000;
index = index % 1000;
}
return now * 1000L + index;
}
private static int resolveServerId() {
try {
// 서버 이중화
// String ip = InetAddress.getLocalHost().getHostAddress();
// if (ip.contains(".162")) return 1;
// if (ip.contains(".163")) return 0;
} catch (Exception ignored) {}
return 1;
}
}

View File

@@ -45,3 +45,9 @@ spring.output.ansi.enabled=always
# HikariCP 연결 풀 크기 설정 (선택사항)
# spring.datasource.hikari.maximum-pool-size=10
##JWT 설정
## access : 30분 / refresh : 7일
token.expiration_time_access=180000
token.expiration_time_refresh=604800000
token.secret_key= c3RhbV9qd3Rfc2VjcmV0X3Rva2Vuc3RhbV9qd3Rfc2VjcmV0X3Rva2Vu