spring boot

JWT를 이용한 인증-II(feat. react, spring boot)

hoazzinews 2024. 12. 25. 17:33

이번 시간에는 지난 시간에 이어서 spring boot 프로젝트를 만들겠습니다.

 

1. spring boot 프로젝트 생성

 

 

2. JWT 의존 모듈 설정
JWT 의존 모듈을 추가합니다.

dependencies {

	// JWT
	implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
	runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
	runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
	
	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.4'
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.4'
	testImplementation 'org.springframework.security:spring-security-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}


3. 애플리케이션 환경 변수 설정(application.properties)

mysql, mybatis, jwt 관련 환경 변수를 선언합니다.

spring.application.name=jwtex

# Tomcat
server.port=8090

# Dev Tools
spring.devtools.restart.enabled=true

# Thymeleaf
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.check-template-location=true

# MySQL
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/DB_JWT
spring.datasource.username=root
spring.datasource.password=1234

#DB(MySQL) Mapper XML Locations
mybatis.config-location=classpath:mybatis/config/mybatis-config.xml
mybatis.mapper-locations=classpath:mybatis/mappers/*.xml

# JWT
spring.jwt.secret=slkvfairsflsvaisfakvmccadpeivnzmxcveguisfasvnadsnfasjdnfas

 

4. Spring Security Config 파일 작성

package com.office.jwtex.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean PasswordEncoder passwordEncoder() {
		log.info("passwordEncoder()");
		
		return new BCryptPasswordEncoder();
		
	}
	
	@Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		
		http
		.cors(cors -> cors.disable())
		.csrf(csrf -> csrf.disable());
		
		http
		.formLogin(login -> login.disable());
		
		return http.build();
		
	}
	
}

 

5. CORS Config 파일 작성

package com.office.jwtex.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

	@Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") 							// 모든 경로에 대해
                .allowedOrigins("http://localhost:3000", "http://localhost:3001") 	// 허용할 Origin
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") 		// 허용할 HTTP 메서드
                .allowedHeaders("*") 							// 허용할 헤더
                .allowCredentials(true); 						// 자격 증명 허용 (쿠키 포함)
        
        log.info("CORS configuration has been applied: Allowed Origins - http://localhost:3000, http://localhost:3001");
        
    }
	
}

 

5. jwt 패키지

package com.office.jwtex.jwt;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Service;

import com.office.jwtex.jwt.token.RefreshTokenDto;
import com.office.jwtex.jwt.token.RefreshTokenService;
import com.office.jwtex.jwt.token.TokenConstant;
import com.office.jwtex.jwt.token.TokenGenerateAndSaveResult;
import com.office.jwtex.member.MemberDto;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class JwtService {
	
	private final JwtUtil jwtUtil;
	private final RefreshTokenService refreshTokenService;
	
	public JwtService(JwtUtil jwtUtil, RefreshTokenService refreshTokenService) {
		this.jwtUtil = jwtUtil;
		this.refreshTokenService = refreshTokenService;
		
	}
	
	public TokenGenerateAndSaveResult generateTokenAndSave(MemberDto memberDto) {
		log.info("generateTokenAndSave()");
		
		TokenGenerateAndSaveResult tokenGenerateAndSaveResult = new TokenGenerateAndSaveResult();
		
		try {
			String accessToken = jwtUtil.generateAccessToken("accessToken", memberDto.getId(), "USER");
	        String refreshToken = jwtUtil.generateRefreshToken("refreshToken", memberDto.getId(), "USER");
	        
	        // RefreshToken을 쿠키에 저장
	        ResponseCookie responseCookie = ResponseCookie.from("refreshToken", refreshToken)
	                .httpOnly(true)
	                .secure(false)  				// HTTPS 연결에서만 전송되도록 설정
	                .path("/")  					// 모든 경로에서 쿠키를 사용
	                .sameSite("Strict")  			// CSRF 보호를 위해 SameSite 속성 설정
	                .maxAge(Duration.ofDays(7))  	// 쿠키의 만료 시간
	                .build();
	        
	        // 리프레시 토큰을 DB에 저장
	        refreshTokenService.saveRefreshToken(memberDto.getNo(), refreshToken);
	        
//	        tokenGenerateAndSaveResult.setSuccess(true);
//	        tokenGenerateAndSaveResult.setMessage("Tokens successfully generated and saved.");
//	        tokenGenerateAndSaveResult.setAccessToken(accessToken);
//	        tokenGenerateAndSaveResult.setRefreshToken(refreshToken);
//	        tokenGenerateAndSaveResult.setResponseCookie(responseCookie);
	        
	        tokenGenerateAndSaveResult = TokenGenerateAndSaveResult.builder()
	        		.success(true)
	        		.message(TokenConstant.TOKENS_GENERATE_SAVE_SUCCESS)
	        		.accessToken(accessToken)
	        		.refreshToken(refreshToken)
	        		.responseCookie(responseCookie)
	        		.build();
	        
		} catch (Exception e) {
			e.printStackTrace();
			
//			tokenGenerateAndSaveResult.setSuccess(false);
//			tokenGenerateAndSaveResult.setMessage("Failed to generate or save tokens: " + e.getMessage());
			
			tokenGenerateAndSaveResult = TokenGenerateAndSaveResult.builder()
	        		.success(false)
	        		.message(TokenConstant.TOKENS_GENERATE_SAVE_SUCCESS)
	        		.build();
	        
		}
		
		return tokenGenerateAndSaveResult;
		
	}

	public boolean verify(HttpServletRequest request, String authorizationHeader) {
		log.info("verify()");
		
	    String accesstoken = authorizationHeader.substring(7); // "Bearer " 이후의 토큰 추출
	    String oldRefreshToken = getRefreshToken(request);
		
	    // 토큰 검증
	    if (!jwtUtil.validateToken(accesstoken)) {		// AccessToken에 문제가 있는 경우
	        log.warn("유효하지 않은 토큰입니다.");
	        
	        RefreshTokenDto refreshTokenDto = refreshTokenService.getRefreshToken(oldRefreshToken);
	        
	        if (refreshTokenDto != null) {				// DB에 RefreshToken이 있는 경우
            	String regDate = refreshTokenDto.getReg_date();
                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); 
                LocalDateTime regDateTime = LocalDateTime.parse(regDate, formatter);	// regDate 문자열을 LocalDateTime으로 변환
                LocalDateTime now = LocalDateTime.now();								// 현재 시간
                
                Duration duration = Duration.between(regDateTime, now);					// 두 시간 간의 차이를 Duration으로 계산
                
                // 30분 이상 경과한 경우
                if (duration.toMinutes() >= 1) {
                    log.info("30분이 지났습니다.");
                    return false;
                    
                } else {
                    log.info("30분이 지나지 않았습니다.");
                    return true;
                    
                }
                
	        }
	        
	        return false;
	        
	    }
	    
		return true;
	}
	
	public String getRefreshToken(HttpServletRequest request) {
		log.info("getRefreshToken()");
		
		// 요청의 쿠키 배열 가져오기
	    Cookie[] cookies = request.getCookies();
	    if (cookies != null) {
	        for (Cookie cookie : cookies) {
	            if ("refreshToken".equals(cookie.getName())) {
	                // refreshToken 값 반환
	                return cookie.getValue();
	            }
	        }
	    }
	    // 쿠키가 없거나 refreshToken이 없을 경우
	    return null;
	    
	}
	
	public void deleteRefreshToken(HttpServletRequest request, HttpServletResponse response) {
		log.info("deleteRefreshToken()");
		
		refreshTokenService.deleteRefreshToken(getRefreshToken(request));
		
		// ResponseCookie를 사용하여 쿠키 생성
        ResponseCookie refreshTokenCookie = ResponseCookie.from("refreshToken", null)
        		.httpOnly(true)             // HttpOnly 속성
        		.secure(false)
                .path("/")                  // 쿠키 경로 설정
                .sameSite("Strict")  		// CSRF 보호를 위해 SameSite 속성 설정
                .maxAge(0)                  // 즉시 만료
                .build();
		
        // 응답 헤더에 쿠키 추가
        response.addHeader(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString());
	        
	}
	
}

 

package com.office.jwtex.jwt;

import java.nio.charset.StandardCharsets;
import java.util.Date;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class JwtUtil {

    // 비밀 키 (Base64로 인코딩된 문자열로 처리)
	final private SecretKey SECRET_KEY;
    private final long ACCESS_TOKEN_EXPIRATION = 1000 * 10 * 1; 			// 1분
    private final long REFRESH_TOKEN_EXPIRATION = 1000 * 60 * 60 * 24 * 7; 	// 7일

    public JwtUtil(@Value("${spring.jwt.secret}") String secret) {
		this.SECRET_KEY = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
		
	}
    
    // 액세스 토큰 생성
    public String generateAccessToken(String category, String username, String role) {
        log.info("generateAccessToken()");
        
        return Jwts.builder()
				.claim("category", category)
				.claim("username", username)
				.claim("role", role)
				.issuedAt(new Date(System.currentTimeMillis()))
				.expiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRATION))
				.signWith(SECRET_KEY)
				.compact();
    }

    
    // 리프레시 토큰 생성
    public String generateRefreshToken(String category, String username, String role) {
        log.info("generateRefreshToken()");
        
        return Jwts.builder()
				.claim("category", category)
				.claim("username", username)
				.claim("role", role)
				.issuedAt(new Date(System.currentTimeMillis()))
				.expiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRATION))
				.signWith(SECRET_KEY)
				.compact();
    }
    
    // 토큰에서 사용자 CATEGORY 추출
    public String getCategory(String token) {
		log.info("getCategory()");
		
		if (validateToken(token))
			return Jwts.parser().verifyWith(SECRET_KEY).build().parseSignedClaims(token).getPayload().get("category", String.class);
		
		return null;

	}
    
    // 토큰에서 사용자 ID 추출
    public String getUsername(String token) {
		log.info("getUsername()");
		
		if (validateToken(token))
			return Jwts.parser().verifyWith(SECRET_KEY).build().parseSignedClaims(token).getPayload().get("username", String.class);
		
		return null;
		
	}
    
    // 토큰에서 사용자 ROLE 추출
    public String getRole(String token) {
		log.info("getRole()");
		
		if (validateToken(token))
			return Jwts.parser().verifyWith(SECRET_KEY).build().parseSignedClaims(token).getPayload().get("role", String.class);

		return null;
		
	}
    
    // 토큰에서 토큰 발행일 추출
    public Date getIssuedAt(String token) {
    	log.info("getIssuedAt()");
    	
    	if (validateToken(token))
    		return Jwts.parser().verifyWith(SECRET_KEY).build().parseSignedClaims(token).getPayload().getIssuedAt();
    	
    	return null;
    	
    }
    
    // 토큰에서 토큰 유효시간 추출
    public Date getExpiration(String token) {
    	log.info("getExpiration()");
    	
    	if (validateToken(token))
    		return Jwts.parser().verifyWith(SECRET_KEY).build().parseSignedClaims(token).getPayload().getExpiration();
    	
    	return null;
    	
    }
    
    // 유휴시간 검증
    public boolean isExpired(String token) {
		log.info("isExpired()");
		
		return Jwts.parser().verifyWith(SECRET_KEY).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());

	}
    
    // 토큰 유효성 검증
    public boolean validateToken(String token) {
        log.info("validateToken()");

        try {
            // Jwts.parser()를 사용하여 JWT 파싱 및 서명 검증
            Jwts.parser()
            	.verifyWith(SECRET_KEY)
            	.build()
            	.parseSignedClaims(token);
            
            return true; // 서명이 유효하면 true 반환
            
        } catch (ExpiredJwtException e) {
            log.info("토큰이 만료되었습니다: {}", e.getMessage());
            
        } catch (MalformedJwtException e) {
            log.info("토큰 형식이 잘못되었습니다: {}", e.getMessage());
            
        } catch (IllegalArgumentException e) {
            log.info("토큰이 null이거나 비어 있습니다: {}", e.getMessage());
            
        } catch (Exception e) {
            log.info("유효하지 않은 토큰: {}", e.getMessage());
            
        }

        return false; // 예외가 발생하면 false 반환
        
    }
    
}

 

6. token 패키지

 

RefreshTokenDto

package com.office.jwtex.jwt.token;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RefreshTokenDto {

	private int no;
	private int owner_no;
	private String refresh_token;
	private String reg_date;
	private String mod_date;
    
}


RefreshTokenMapper

package com.office.jwtex.jwt.token;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface RefreshTokenMapper {

	int insertNewRefreshToken(RefreshTokenDto refreshTokenDto);

	RefreshTokenDto selectRefreshToken(String refreshToken);

	int deleteRefreshToken(String oldRefreshToken);

}


RefreshTokenService

package com.office.jwtex.jwt.token;

import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class RefreshTokenService {

	final private RefreshTokenMapper refreshTokenMapper;
	
	public RefreshTokenService(RefreshTokenMapper refreshTokenMapper) {
		this.refreshTokenMapper = refreshTokenMapper;
	}
	
	public int saveRefreshToken(int no, String refreshToken) {
		log.info("insertNewRefreshToken()");
		
		return refreshTokenMapper.insertNewRefreshToken(RefreshTokenDto.builder()
				.owner_no(no)
				.refresh_token(refreshToken)
				.build());
		
	}

	public RefreshTokenDto getRefreshToken(String refreshToken) {
		log.info("getRefreshToken()");
		
		return refreshTokenMapper.selectRefreshToken(refreshToken);
		
	}

	public int deleteRefreshToken(String oldRefreshToken) {
		log.info("deleteRefreshToken()");
		
		return refreshTokenMapper.deleteRefreshToken(oldRefreshToken);
		
	}

}


TokenConstant

package com.office.jwtex.jwt.token;

public class TokenConstant {

	public static final String TOKENS_GENERATE_SAVE_SUCCESS = "TOKENS GENERATED AND SAVED SUCCESS!!.";
	public static final String TOKENS_GENERATE_SAVE_FAIl 	= "TOKENS GENERATED AND SAVED FAIL!!";
	
}

 

TokenGenerateAndSaveResult

package com.office.jwtex.jwt.token;

import org.springframework.http.ResponseCookie;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TokenGenerateAndSaveResult {

	private boolean success;
    private String message; 		// 에러 메시지 또는 성공 메시지
    private String accessToken;
    private String refreshToken;
    private ResponseCookie responseCookie;
    
}

 

TokenResponse

package com.office.jwtex.jwt.token;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TokenResponse {

    private String accessToken;
    private String refreshToken;
    
}

 

8. mybatis config & mapper 파일 작성

 

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

</configuration>

 

member-mapper.xml

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTO Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.office.jwtex.member.MemberMapper">

	<insert id="insertNewMember" 
			parameterType="com.office.jwtex.member.MemberDto">
		
		INSERT INTO MEMBER(ID, PW, MAIL) 
		VALUES(#{id}, #{pw}, #{mail})
		
	</insert>
	
	<select id="selectMemberById" 
			parameterType="String" 
			resultType="com.office.jwtex.member.MemberDto">
	
		SELECT 
			* 
		FROM 
			MEMBER 
		WHERE 
			ID = #{id}	
	
	</select>
	
	<update id="updateMember" 
			parameterType="com.office.jwtex.member.MemberDto">
		
		UPDATE 
			MEMBER 
		SET 
			PW = #{pw}, 
			MAIL = #{mail}, 
			MOD_DATE = NOW() 
		WHERE 
			ID = #{id}
		
	</update>

</mapper>



refreshtoken-mapper.xml

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTO Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.office.jwtex.jwt.token.RefreshTokenMapper">

	<insert 
		id="insertNewRefreshToken" 
		parameterType="com.office.jwtex.jwt.token.RefreshTokenDto">
		
		INSERT INTO REFRESH_TOKEN(OWNER_NO, REFRESH_TOKEN) 
		VALUES(#{owner_no}, #{refresh_token})
		
	</insert>
	
	<select id="selectRefreshToken" 
			parameterType="com.office.jwtex.jwt.token.RefreshTokenDto">
			
		SELECT 
			* 
		FROM 
			REFRESH_TOKEN 
		WHERE 
			REFRESH_TOKEN = #{refreshToken}
			
	</select>
	
	<delete id="deleteRefreshToken" 
			parameterType="String">
		
		DELETE FROM 
			REFRESH_TOKEN
		WHERE 
			REFRESH_TOKEN = #{oldRefreshToken}
		
	</delete>

</mapper>

 

 

9. member 패키지


MemberConstant.java

package com.office.jwtex.member;

public class MemberConstant {

	public static final String SIGNUP_SUCCESS 	= "SIGNUP SUCCESS!!";
    public static final String SIGNUP_FAIL 		= "SIGNUP FAIL!!";
    
    public static final String SIGNIN_SUCCESS 	= "SIGNIN SUCCESS!!";
    public static final String SIGNIN_FAIL 		= "SIGNIN FAIL!!";
    
    public static final String CREDENTIALS_INVALID 	= "THE CREDENTIALS ARE INVALID.";	// 인증 정보가 유효하지 않습니다.
    public static final String INVALID_TOKEN 		= "INVALID TOKEN.";					// 유효하지 않은 토큰입니다.
    
    public static final String GET_USER_INFO_SUCCESS	= "GET USER INFO SUCCESS!!";	// 회원 정보 조회 성공
    public static final String GET_USER_INFO_FAIL		= "GET USER INFO FAIL!!";	// 회원 정보 조회 성공
    
    public static final String MODIFY_SUCCESS 	= "MODIFY SUCCESS!!";
    public static final String MODIFY_FAIL 		= "MODIFY FAIL!!";
    
    public static final String SIGNOUT_SUCCESS	= "SIGNOUT SUCCESS!!";
    public static final String SIGNOUT_FAIL		= "SIGNOUT FAIL!!";

}


MemberController.java

package com.office.jwtex.member;

import java.util.HashMap;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.office.jwtex.jwt.JwtService;
import com.office.jwtex.jwt.token.TokenGenerateAndSaveResult;
import com.office.jwtex.jwt.token.TokenResponse;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
@RequestMapping("/member")
public class MemberController {

	private final MemberService memberService;
	private final JwtService jwtService;
	
	public MemberController(MemberService memberService, JwtService jwtService) {
		this.memberService = memberService;
		this.jwtService = jwtService;
		
	}
	
	@PostMapping("/signup")
    public ResponseEntity<?> signup(@RequestBody MemberDto memberDto) {
		log.info("signup()");

        int signupResult = memberService.signup(memberDto);
        if (signupResult > 0)
        	return ResponseEntity.ok(
//        			new SignUpResponse(true, MemberConstant.SIGNUP_SUCCESS, memberDto.getId())
        			SignUpResponse.builder()
        			.isSuccess(true)
        			.message(MemberConstant.SIGNUP_SUCCESS)
        			.userId(memberDto.getId())
        			.build());
        	
        return ResponseEntity.ok(
//        		new SignUpResponse(false, MemberConstant.SIGNUP_FAIL, null)
        		SignUpResponse.builder()
        		.message(MemberConstant.SIGNUP_FAIL)
        		.build()
        		);

        
    }
	
	@PostMapping("/signin")
    public ResponseEntity<?> signin(@RequestBody MemberDto memberDto) {
		log.info("signin()");
		
		MemberDto signinedMemberDto = memberService.signin(memberDto);

        if (signinedMemberDto != null) {
        	
        	TokenGenerateAndSaveResult tokenGenerateAndSaveResult = jwtService.generateTokenAndSave(signinedMemberDto);
            
            // 쿠키는 헤더에,  AccessToken은 바디에 담아서 클라이언트에게 응답
            return ResponseEntity.ok()
                    .header(HttpHeaders.SET_COOKIE, tokenGenerateAndSaveResult.getResponseCookie().toString())  		// 쿠키를 헤더에 설정
                    .body(new HashMap<>() {{
//                    	put("signInResponse", new SignInResponse(true, MemberConstant.SIGNIN_SUCCESS, signinedMemberDto.getId()));
//                    	put("tokenResponse", new TokenResponse(MemberConstant.SIGNIN_SUCCESS, tokenGenerateAndSaveResult.getAccessToken(), tokenGenerateAndSaveResult.getRefreshToken()));
                    	put("signInResponse", SignInResponse.builder()
                    			.isSuccess(true)
                    			.message(MemberConstant.SIGNIN_SUCCESS)
                    			.userId(signinedMemberDto.getId()).build());
                    	put("tokenResponse", TokenResponse.builder()
                    			.accessToken(tokenGenerateAndSaveResult.getAccessToken())
                    			.refreshToken(tokenGenerateAndSaveResult.getRefreshToken()).build());
                    	
                    }});
                    		
        }
        
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
        		.body(new HashMap<>() {{
//                	put("signInResponse", new SignInResponse(false, MemberConstant.SIGNIN_FAIL, null));
        			put("signInResponse", SignInResponse.builder()
        					.message(MemberConstant.SIGNIN_FAIL)
        					.build());
                }});

    }

	
	@GetMapping("/getuserinfo")
	public ResponseEntity<?> getUserInfo (
	        @RequestParam(value = "id") String id,
	        HttpServletRequest request, 
	        HttpServletResponse response, 
	        @RequestHeader(value = "Authorization", required = false) String authorizationHeader) {
	    log.info("getUserInfo()");

	    // Authorization 헤더 검증
	    if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
	        log.warn("Authorization header is missing or incorrect.");
	        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
//	        		.body(new UserInfoResponse(MemberConstant.CREDENTIALS_INVALID));
	        		.body(UserInfoResponse.builder()
	        				.message(MemberConstant.CREDENTIALS_INVALID)
	        				.build());
	        
	    }
	    
	    // JWT 검증
	    if (!jwtService.verify(request, authorizationHeader)) {
	    	log.warn("The token is invalid.");
	    	return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
//	    			.body(new UserInfoResponse(MemberConstant.INVALID_TOKEN));
	    			.body(UserInfoResponse.builder()
	    					.message(MemberConstant.INVALID_TOKEN)
	    					.build());
	    	
	    }
	    
	    // 사용자 정보 조회
	    MemberDto memberDto = memberService.getUserInfo(id);
	    if (memberDto != null) {
	    	jwtService.deleteRefreshToken(request, response);
	    	
	    	TokenGenerateAndSaveResult tokenGenerateAndSaveResult = jwtService.generateTokenAndSave(memberDto);
	        return ResponseEntity.ok()								
	        		.header(HttpHeaders.SET_COOKIE, tokenGenerateAndSaveResult.getResponseCookie().toString())
	        		.body(new HashMap<String, Object>() {{
	        			put("member", memberDto);
	        			put("userInfoResponse", UserInfoResponse.builder()
	        					.accessToken(tokenGenerateAndSaveResult.getAccessToken())
	        					.message(MemberConstant.GET_USER_INFO_SUCCESS)
	        					.build());
	        		}});  	
	        
	    }

	    return ResponseEntity.status(HttpStatus.NOT_FOUND).body(UserInfoResponse.builder()
	    		.message(MemberConstant.GET_USER_INFO_FAIL)
	    		.build());
	    
	}
	
	@PostMapping("/modify")
	public ResponseEntity<?> modify(
			@RequestBody MemberDto memberDto,
			HttpServletRequest request, 
			HttpServletResponse response, 
			@RequestHeader(value = "Authorization", required = false) String authorizationHeader) {
		log.info("modify()");

	    // Authorization 헤더 검증
	    if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
	        log.warn("Authorization header is missing or incorrect.");
	        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
//	        		.body(new ModifyResponse(MemberConstant.CREDENTIALS_INVALID));
	        		.body(ModifyResponse.builder()
	        				.message(MemberConstant.CREDENTIALS_INVALID)
	        				.build());
	        
	    }
	    
	    // JWT 검증
	    if (!jwtService.verify(request, authorizationHeader)) {
	    	log.warn("The token is invalid.");
	    	return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
//	    			.body(new ModifyResponse(MemberConstant.INVALID_TOKEN));
	    			.body(ModifyResponse.builder()
	    					.message(MemberConstant.INVALID_TOKEN)
	    					.build());
	    	
	    }
	    
	    int modifyResult = memberService.modify(memberDto);
	    if (modifyResult > 0) {
	    	jwtService.deleteRefreshToken(request, response);
	    	
	    	TokenGenerateAndSaveResult tokenGenerateAndSaveResult = jwtService.generateTokenAndSave(memberDto);
	    	
	    	return ResponseEntity.ok()								
            		.header(HttpHeaders.SET_COOKIE, tokenGenerateAndSaveResult.getResponseCookie().toString())
            		.body(new HashMap<String, Object>() {{
	        			put("member", memberDto);
	        			put("modifyResponse", ModifyResponse.builder()
	        					.accessToken(tokenGenerateAndSaveResult.getAccessToken())
	        					.message(MemberConstant.MODIFY_SUCCESS)
	        					.build());
	        		}});
	    	
	    }
	    
	    return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ModifyResponse.builder()
	    		.message(MemberConstant.MODIFY_FAIL)
	    		.build());
        
    }
	
	@PostMapping("/signout")
	public ResponseEntity<?> signout(HttpServletRequest request, HttpServletResponse response) {
		log.info("signout()");
		
		jwtService.deleteRefreshToken(request, response);
		
        return ResponseEntity.ok(SignoutResponse.builder()
        		.message(MemberConstant.SIGNOUT_SUCCESS)
        		.build());
		
	}
	
}


MemberDto.java

package com.office.jwtex.member;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MemberDto {

	private int no;
	private String id; 
	private String pw;
	private String mail; 
	private String reg_date;
	private String mod_date;
    
}


MemberMapper.java

package com.office.jwtex.member;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface MemberMapper {

	int insertNewMember(MemberDto memberDto);

	MemberDto selectMemberById(String id);

	MemberDto getUserInfo(String id);

	int updateMember(MemberDto memberDto);
		
}


MemberService.java

package com.office.jwtex.member;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class MemberService {
	
	final private MemberMapper memberMapper;
	final private PasswordEncoder passwordEncoder;
	
	public MemberService(MemberMapper memberMapper, PasswordEncoder passwordEncoder) {
		this.memberMapper = memberMapper;
		this.passwordEncoder = passwordEncoder;
		
	}
	
	public int signup(MemberDto memberDto) {
		log.info("signup()");
		
		memberDto.setPw(passwordEncoder.encode(memberDto.getPw()));
		return memberMapper.insertNewMember(memberDto);
		
	}

	public MemberDto signin(MemberDto memberDto) {
		log.info("signin()");
		
		MemberDto selectMemberDto = memberMapper.selectMemberById(memberDto.getId());
		if (selectMemberDto == null || !passwordEncoder.matches(memberDto.getPw(), selectMemberDto.getPw()))
			return null;
			
		return selectMemberDto;
		
	}

	public MemberDto getUserInfo(String id) {
		log.info("findMemberById()");
		
		return memberMapper.selectMemberById(id);
		
	}

	public int modify(MemberDto memberDto) {
		log.info("modify()");
		
		memberDto.setPw(passwordEncoder.encode(memberDto.getPw()));
		return memberMapper.updateMember(memberDto);
		
	}

}

 

10. member response 패키지

ModifyResponse

package com.office.jwtex.member.response;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ModifyResponse {

	private String message;
	private String accessToken;
	
}


SignInResponse

package com.office.jwtex.member.response;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SignInResponse {
	
	private boolean isSuccess;
	private String message;
    private String userId;
    
}

 


SignoutResponse

package com.office.jwtex.member.response;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SignoutResponse {

	private String message;
}


SignUpResponse

package com.office.jwtex.member.response;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SignUpResponse {

	private boolean isSuccess;
	private String message;
    private String userId;
    
}


UserInfoResponse

package com.office.jwtex.member.response;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserInfoResponse {

	private String message;
	private String accessToken;
	
}

 

spring 소스 첨부합니다.

jwtex.zip
0.12MB