Compare commits
8 Commits
b0398fccee
...
main
Author | SHA1 | Date | |
---|---|---|---|
499fbc6afb | |||
e78e98ad37 | |||
f10b028e04 | |||
cc2a34403d | |||
e8a785a20d | |||
afef6dfa80 | |||
a0ffeb236e | |||
fa1df19f64 |
BIN
jpa-curd-0.0.1.vsix
Normal file
BIN
jpa-curd-0.0.1.vsix
Normal file
Binary file not shown.
@@ -22,6 +22,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import com.bio.bio_backend.global.constants.ApiResponseCode;
|
||||
import com.bio.bio_backend.global.annotation.LogExecution;
|
||||
import com.bio.bio_backend.global.utils.SecurityUtils;
|
||||
import com.bio.bio_backend.global.utils.JwtUtils;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
|
||||
@Tag(name = "Member", description = "회원 관련 API")
|
||||
@@ -33,6 +35,7 @@ public class MemberController {
|
||||
|
||||
private final MemberService memberService;
|
||||
private final MemberMapper memberMapper;
|
||||
private final JwtUtils jwtUtils;
|
||||
|
||||
@LogExecution("회원 등록")
|
||||
@Operation(summary = "회원 등록", description = "새로운 회원을 등록합니다.")
|
||||
@@ -57,10 +60,14 @@ public class MemberController {
|
||||
@ApiResponse(responseCode = "401", description = "인증 실패", content = @Content(schema = @Schema(implementation = ApiResponseDto.class)))
|
||||
})
|
||||
@PostMapping("/logout")
|
||||
public ResponseEntity<ApiResponseDto<Void>> logout() {
|
||||
public ResponseEntity<ApiResponseDto<Void>> logout(HttpServletResponse response) {
|
||||
try {
|
||||
String userId = SecurityUtils.getCurrentUserId();
|
||||
memberService.deleteRefreshToken(userId);
|
||||
|
||||
// 모든 토큰 쿠키 삭제
|
||||
jwtUtils.deleteAllTokenCookies(response);
|
||||
|
||||
log.info("사용자 로그아웃 완료: {}", userId);
|
||||
|
||||
return ResponseEntity.ok(ApiResponseDto.success(ApiResponseCode.COMMON_SUCCESS));
|
||||
|
@@ -1,6 +1,5 @@
|
||||
package com.bio.bio_backend.domain.base.member.dto;
|
||||
|
||||
import com.bio.bio_backend.domain.base.member.enums.MemberRole;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
@@ -16,7 +15,6 @@ public class CreateMemberResponseDto {
|
||||
|
||||
private Long oid;
|
||||
private String userId;
|
||||
private MemberRole role;
|
||||
private Boolean useFlag;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
@@ -14,5 +14,6 @@ import java.time.LocalDateTime;
|
||||
public class LoginResponseDto {
|
||||
|
||||
private String userId;
|
||||
private String name;
|
||||
private LocalDateTime lastLoginAt;
|
||||
}
|
||||
|
@@ -2,13 +2,13 @@ package com.bio.bio_backend.domain.base.member.mapper;
|
||||
|
||||
import com.bio.bio_backend.domain.base.member.dto.CreateMemberRequestDto;
|
||||
import com.bio.bio_backend.domain.base.member.dto.CreateMemberResponseDto;
|
||||
import com.bio.bio_backend.domain.base.member.dto.LoginResponseDto;
|
||||
import com.bio.bio_backend.domain.base.member.dto.MemberDto;
|
||||
import com.bio.bio_backend.domain.base.member.entity.Member;
|
||||
import com.bio.bio_backend.global.annotation.IgnoreBaseEntityMapping;
|
||||
import com.bio.bio_backend.global.config.GlobalMapperConfig;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper(config = GlobalMapperConfig.class)
|
||||
@@ -52,4 +52,9 @@ public interface MemberMapper {
|
||||
*/
|
||||
@IgnoreBaseEntityMapping
|
||||
void updateMemberFromDto(MemberDto memberDto, @org.mapstruct.MappingTarget Member member);
|
||||
|
||||
/**
|
||||
* MemberDto를 LoginResponseDto로 변환
|
||||
*/
|
||||
LoginResponseDto toLoginResponseDto(MemberDto memberDto);
|
||||
}
|
||||
|
@@ -1,13 +1,13 @@
|
||||
package com.bio.bio_backend.domain.base.member.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
|
||||
import com.bio.bio_backend.domain.base.member.dto.MemberDto;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface MemberService extends UserDetailsService {
|
||||
|
||||
UserDetails loadUserByUsername(String id);
|
||||
@@ -16,9 +16,9 @@ public interface MemberService extends UserDetailsService {
|
||||
|
||||
String getRefreshToken(String id);
|
||||
|
||||
int deleteRefreshToken(String id);
|
||||
void deleteRefreshToken(String id);
|
||||
|
||||
void updateMember(MemberDto member);
|
||||
|
||||
List<MemberDto> selectMemberList(Map<String, String> params);
|
||||
|
||||
int updateMember(MemberDto member);
|
||||
}
|
||||
|
@@ -63,13 +63,12 @@ public class MemberServiceImpl implements MemberService {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int updateMember(MemberDto memberDto) {
|
||||
public void updateMember(MemberDto memberDto) {
|
||||
Member member = memberRepository.findActiveMemberByUserId(memberDto.getUserId())
|
||||
.orElseThrow(() -> new ApiException(ApiResponseCode.USER_NOT_FOUND));
|
||||
|
||||
memberMapper.updateMemberFromDto(memberDto, member);
|
||||
memberRepository.save(member);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,13 +81,12 @@ public class MemberServiceImpl implements MemberService {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int deleteRefreshToken(String id) {
|
||||
public void deleteRefreshToken(String id) {
|
||||
Member member = memberRepository.findActiveMemberByUserId(id)
|
||||
.orElseThrow(() -> new ApiException(ApiResponseCode.USER_NOT_FOUND));
|
||||
|
||||
member.setRefreshToken(null);
|
||||
memberRepository.save(member);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -16,8 +16,6 @@ public class CorsConfig {
|
||||
config.addAllowedHeader("*");
|
||||
config.addAllowedMethod("*");
|
||||
config.setAllowCredentials(true);
|
||||
|
||||
config.addExposedHeader("Authorization");
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
|
||||
return new CorsFilter(source);
|
||||
|
@@ -9,6 +9,7 @@ import com.bio.bio_backend.global.dto.ApiResponseDto;
|
||||
import com.bio.bio_backend.domain.base.member.dto.LoginRequestDto;
|
||||
import com.bio.bio_backend.domain.base.member.dto.LoginResponseDto;
|
||||
import com.bio.bio_backend.domain.base.member.dto.MemberDto;
|
||||
import com.bio.bio_backend.domain.base.member.mapper.MemberMapper;
|
||||
import com.bio.bio_backend.domain.base.member.service.MemberService;
|
||||
import com.bio.bio_backend.global.constants.ApiResponseCode;
|
||||
import com.bio.bio_backend.global.utils.JwtUtils;
|
||||
@@ -40,6 +41,7 @@ public class JwtTokenIssuanceFilter extends UsernamePasswordAuthenticationFilter
|
||||
private final ObjectMapper objectMapper;
|
||||
private final MemberService memberService;
|
||||
private final HttpUtils httpUtils;
|
||||
private final MemberMapper memberMapper;
|
||||
|
||||
// 사용자 login 인증 처리
|
||||
@Override
|
||||
@@ -75,24 +77,22 @@ public class JwtTokenIssuanceFilter extends UsernamePasswordAuthenticationFilter
|
||||
// Refresh 토큰 쿠키 저장
|
||||
jwtUtils.setRefreshTokenCookie(response, refreshToken);
|
||||
|
||||
// Access 토큰 전달
|
||||
response.setHeader("Authorization", "Bearer " + accessToken);
|
||||
// Access 토큰 쿠키 저장
|
||||
jwtUtils.setAccessTokenCookie(response, accessToken);
|
||||
|
||||
SecurityContextHolderStrategy contextHolder = SecurityContextHolder.getContextHolderStrategy();
|
||||
SecurityContext context = contextHolder.createEmptyContext();
|
||||
context.setAuthentication(authResult);
|
||||
contextHolder.setContext(context);
|
||||
|
||||
LoginResponseDto memberData = new LoginResponseDto();
|
||||
memberData.setUserId(member.getUserId());
|
||||
memberData.setLastLoginAt(member.getLastLoginAt());
|
||||
LoginResponseDto loginResponseDto = memberMapper.toLoginResponseDto(member);
|
||||
|
||||
// login 성공 메시지 전송
|
||||
response.setStatus(HttpStatus.OK.value());
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8");
|
||||
objectMapper.writeValue(
|
||||
response.getWriter(),
|
||||
ApiResponseDto.success(ApiResponseCode.LOGIN_SUCCESSFUL, memberData)
|
||||
ApiResponseDto.success(ApiResponseCode.LOGIN_SUCCESSFUL, loginResponseDto)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,10 @@
|
||||
package com.bio.bio_backend.global.filter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
import com.bio.bio_backend.domain.base.member.dto.MemberDto;
|
||||
import com.bio.bio_backend.global.utils.HttpUtils;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -79,7 +81,7 @@ public class JwtTokenValidationFilter extends OncePerRequestFilter {
|
||||
}
|
||||
|
||||
// 2. IP 주소 검증
|
||||
if (!jwtUtils.isValidClientIp(refreshToken, request.getRemoteAddr())) {
|
||||
if (!jwtUtils.isValidClientIp(refreshToken, httpUtils.getClientIp())) {
|
||||
log.warn("클라이언트 IP 주소가 일치하지 않습니다. URI: {}, IP: {}",
|
||||
request.getRequestURI(), request.getRemoteAddr());
|
||||
sendJsonResponse(response, ApiResponseDto.fail(ApiResponseCode.INVALID_CLIENT_IP));
|
||||
@@ -88,27 +90,29 @@ public class JwtTokenValidationFilter extends OncePerRequestFilter {
|
||||
|
||||
// 모든 검증을 통과한 경우 토큰 갱신 진행
|
||||
String username = jwtUtils.extractUsername(refreshToken);
|
||||
String role = jwtUtils.extractRole(refreshToken);
|
||||
|
||||
UserDetails userDetails = memberService.loadUserByUsername(username);
|
||||
|
||||
// 새로운 Access Token 생성
|
||||
String newAccessToken = jwtUtils.generateToken(username, role,
|
||||
Long.parseLong(Objects.requireNonNull(env.getProperty("token.expiration_time_access"))));
|
||||
String newAccessToken = jwtUtils.createAccessToken(username);
|
||||
|
||||
// 새로운 Access Token을 응답 헤더에 설정
|
||||
response.setHeader("Authorization", "Bearer " + newAccessToken);
|
||||
// 새로운 Access Token을 쿠키에 설정
|
||||
jwtUtils.setAccessTokenCookie(response, newAccessToken);
|
||||
|
||||
// Refresh Token 갱신
|
||||
String newRefreshToken = jwtUtils.createRefreshToken(username, role, httpUtils.getClientIp());
|
||||
String newRefreshToken = jwtUtils.createRefreshToken(username, httpUtils.getClientIp());
|
||||
jwtUtils.setRefreshTokenCookie(response, newRefreshToken);
|
||||
|
||||
MemberDto member = (MemberDto) userDetails;
|
||||
member.setRefreshToken(newRefreshToken);
|
||||
member.setLoginIp(httpUtils.getClientIp());
|
||||
memberService.updateMember(member);
|
||||
|
||||
// 인증 정보 설정
|
||||
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);
|
||||
}
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
log.info("토큰 자동 갱신 성공: {}", username);
|
||||
filterChain.doFilter(request, response);
|
||||
|
@@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
|
||||
import com.bio.bio_backend.domain.base.member.service.MemberService;
|
||||
import com.bio.bio_backend.domain.base.member.mapper.MemberMapper;
|
||||
import com.bio.bio_backend.global.exception.CustomAuthenticationFailureHandler;
|
||||
import com.bio.bio_backend.global.utils.JwtUtils;
|
||||
import com.bio.bio_backend.global.utils.HttpUtils;
|
||||
@@ -37,9 +38,10 @@ public class WebSecurity {
|
||||
private final Environment env;
|
||||
private final SecurityPathConfig securityPathConfig;
|
||||
private final HttpUtils httpUtils;
|
||||
private final MemberMapper memberMapper;
|
||||
|
||||
private JwtTokenIssuanceFilter getJwtTokenIssuanceFilter(AuthenticationManager authenticationManager) throws Exception {
|
||||
JwtTokenIssuanceFilter filter = new JwtTokenIssuanceFilter(authenticationManager, jwtUtils, objectMapper, memberService, httpUtils);
|
||||
JwtTokenIssuanceFilter filter = new JwtTokenIssuanceFilter(authenticationManager, jwtUtils, objectMapper, memberService, httpUtils, memberMapper);
|
||||
filter.setFilterProcessesUrl("/login");
|
||||
filter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler(objectMapper));
|
||||
return filter;
|
||||
|
@@ -13,7 +13,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.util.Date;
|
||||
@@ -49,7 +48,7 @@ public class JwtUtils {
|
||||
public String generateToken(String username, String clientIp, long expirationTime) {
|
||||
return Jwts.builder()
|
||||
.subject(username)
|
||||
.claim("ip", clientIp) // IP 정보 추가
|
||||
.claim("ip", clientIp)
|
||||
.issuedAt(new Date(System.currentTimeMillis()))
|
||||
.expiration(new Date(System.currentTimeMillis() + expirationTime))
|
||||
.signWith(getSigningKey())
|
||||
@@ -123,8 +122,6 @@ public class JwtUtils {
|
||||
public String extractUsername(String token) {
|
||||
return extractAllClaims(token).getSubject();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Claims extractAllClaims(String token) {
|
||||
return Jwts.parser()
|
||||
@@ -133,14 +130,19 @@ public class JwtUtils {
|
||||
.parseSignedClaims(token).getPayload();
|
||||
}
|
||||
|
||||
// Access Token을 쿠키에서 추출
|
||||
public String extractAccessJwtFromRequest(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader("Authorization");
|
||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
|
||||
return bearerToken.substring(7); // "Bearer " 제거
|
||||
if (request.getCookies() != null) {
|
||||
for (Cookie cookie : request.getCookies()) {
|
||||
if ("AccessToken".equals(cookie.getName())) {
|
||||
return cookie.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
return bearerToken;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Refresh Token을 쿠키에서 추출
|
||||
public String extractRefreshJwtFromCookie(HttpServletRequest request) {
|
||||
if (request.getCookies() != null) {
|
||||
for (Cookie cookie : request.getCookies()) {
|
||||
@@ -154,12 +156,39 @@ public class JwtUtils {
|
||||
|
||||
// Refresh Token 쿠키 설정
|
||||
public void setRefreshTokenCookie(HttpServletResponse response, String refreshToken) {
|
||||
Cookie refreshTokenCookie = new Cookie("RefreshToken", refreshToken);
|
||||
refreshTokenCookie.setHttpOnly(true);
|
||||
refreshTokenCookie.setSecure(false);
|
||||
refreshTokenCookie.setPath("/");
|
||||
refreshTokenCookie.setMaxAge(Integer.parseInt(env.getProperty("token.expiration_time_refresh")));
|
||||
|
||||
response.addCookie(refreshTokenCookie);
|
||||
setCookie(response, "RefreshToken", refreshToken,
|
||||
Integer.parseInt(env.getProperty("token.expiration_time_refresh")) / 1000);
|
||||
}
|
||||
|
||||
// Access Token 쿠키 설정
|
||||
public void setAccessTokenCookie(HttpServletResponse response, String accessToken) {
|
||||
setCookie(response, "AccessToken", accessToken,
|
||||
Integer.parseInt(env.getProperty("token.expiration_time_access")) / 1000);
|
||||
}
|
||||
|
||||
// Access Token 쿠키 삭제
|
||||
public void deleteAccessTokenCookie(HttpServletResponse response) {
|
||||
setCookie(response, "AccessToken", "", 0);
|
||||
}
|
||||
|
||||
// Refresh Token 쿠키 삭제
|
||||
public void deleteRefreshTokenCookie(HttpServletResponse response) {
|
||||
setCookie(response, "RefreshToken", "", 0);
|
||||
}
|
||||
|
||||
// 모든 토큰 쿠키 삭제
|
||||
public void deleteAllTokenCookies(HttpServletResponse response) {
|
||||
deleteAccessTokenCookie(response);
|
||||
deleteRefreshTokenCookie(response);
|
||||
}
|
||||
|
||||
// 쿠키 설정 헬퍼 메서드
|
||||
private void setCookie(HttpServletResponse response, String name, String value, int maxAge) {
|
||||
Cookie cookie = new Cookie(name, value);
|
||||
cookie.setHttpOnly(true);
|
||||
cookie.setSecure(false);
|
||||
cookie.setPath("/");
|
||||
cookie.setMaxAge(maxAge);
|
||||
response.addCookie(cookie);
|
||||
}
|
||||
}
|
||||
|
@@ -113,7 +113,7 @@ springdoc.default-consumes-media-type=application/json
|
||||
|
||||
# ========================================
|
||||
# 보안 설정 - 허용할 경로
|
||||
security.permit-all-paths=/login,/members/register,/swagger-ui/**,/swagger-ui.html,/swagger-ui/index.html,/api-docs,/api-docs/**,/v3/api-docs,/v3/api-docs/**,/ws/**,/actuator/**,/actuator/health/**,/actuator/info
|
||||
security.permit-all-paths=/login,/logout,/members/register,/swagger-ui/**,/swagger-ui.html,/swagger-ui/index.html,/api-docs,/api-docs/**,/v3/api-docs,/v3/api-docs/**,/ws/**,/actuator/**,/actuator/health/**,/actuator/info
|
||||
|
||||
# 파일 업로드 설정
|
||||
# ========================================
|
||||
|
Reference in New Issue
Block a user