implementation: 강의 버전을 따라서 implement 해줬다.
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
[1] JWTUtil
@Log4j2
public class JWTUtil {
private static String key = "123456789910Thisisthekeythatiusetoencryptthepassword";
public static String generateToken(Map<String, Object> valueMap, int min) {
SecretKey key = null;
try {
key = Keys.hmacShaKeyFor(JWTUtil.key.getBytes("UTF-8"));
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
String jwtStr = Jwts.builder()
.setHeader(Map.of("typ", "JWT"))
.setClaims(valueMap)
.setIssuedAt(Date.from(ZonedDateTime.now().toInstant()))
.setExpiration(Date.from(ZonedDateTime.now().plusMinutes(min).toInstant()))
.signWith(key)
.compact();
return jwtStr;
}
public static Map<String, Object> validateToken(String token) {
Map<String, Object> claim = null;
try {
SecretKey key = Keys.hmacShaKeyFor(JWTUtil.key.getBytes("UTF-8"));
claim = Jwts.parserBuilder()
.setSigningKey(key) // 서명 키 설정
.build()
.parseClaimsJws(token) // 토큰 검증 및 파싱
.getBody();
} catch (MalformedJwtException malformedJwtException) {
throw new CustomJWTException("MalFormed"); // 잘못된 형식의 토큰
} catch (ExpiredJwtException expiredJwtException) {
throw new CustomJWTException("Expired"); // 만료된 토큰
} catch (InvalidClaimException invalidClaimException) {
throw new CustomJWTException("Invalid"); // 유효하지 않은 클레임
} catch (JwtException jwtException) {
throw new CustomJWTException("JWTError"); // 기타 JWT 관련 오류
} catch (Exception e) {
throw new CustomJWTException("Error"); // 기타 예외 발생
}
return claim;
}
}
[2] ApiLoginSuccessHandler 추가 -- 토큰 생성 코드 추가
@Log4j2
public class ApiLoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//access & refresh claims에 넣기
MemberDTO memberDTO = (MemberDTO) authentication.getPrincipal();
Map<String, Object> claims = memberDTO.getClaims();
// +) token 생성하는 부분 추가하기 !
String accessToken = JWTUtil.generateToken(claims, 10);
String refreshToken = JWTUtil.generateToken(claims, 60*24);
claims.put("accessToken", "");
claims.put("refreshToken", "");
Gson gson = new Gson();
String jsonStr = gson.toJson(claims);
response.setContentType("applicaion/json; charset=UTF-8"); //한글 처리
PrintWriter printWriter = response.getWriter();
printWriter.println(jsonStr);
printWriter.close();
}
}
[3] CustomSecurityConfig 추가
public class CustomSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
log.info("--------security config-----");
http.cors(httpSecurityCorsConfigurer -> {
httpSecurityCorsConfigurer.configurationSource(corsConfigurationSource());
});
// session 제거
http.sessionManagement(httpSecuritySessionManagementConfigurer -> {
httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.NEVER);
});
http.csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.disable());
//로그인 성공, 실패 관리
http.formLogin(formLogin -> formLogin
.loginPage("/api/member/login")
.successHandler(new ApiLoginSuccessHandler())
.failureHandler(new ApiLoginFailHandler())
);
//+) JWTCheckFilter 추가하기
http.addFilterBefore(new JWTCheckFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
사용자가 로그인하여 Access Token을 발급받은 후 API 요청을 보낼 때, 해당 요청이 이미 인증된 사용자인지 JWT를 먼저 확인하고,
인증되지 않은 경우에만 UsernamePasswordAuthenticationFilter가 실행되도록 하는 구조이다.
[4] JWTCheckFilter
@Log4j2
public class JWTCheckFilter extends OncePerRequestFilter {
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String path = request.getRequestURI();
return false; //check함
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeaderStr = request.getHeader("Authorization");
//Bearer: 공백 포함 7개, 뒤에가 jwt
try {
String accessToken = authHeaderStr.substring(7);
Map<String, Object> claims = JWTUtil.validateToken(accessToken); //검사
filterChain.doFilter(request, response);
}catch (Exception e){
Gson gson = new Gson();
String msg = gson.toJson(Map.of("error", "ERROR_ACCESS_TOKEN"));
response.setContentType("application/json");
PrintWriter printWriter = response.getWriter();
printWriter.println(msg);
printWriter.close();
}
//도착지
filterChain.doFilter(request, response);
}
}
[5] CustomSecurityConfig 설정 추가
@EnableMethodSecurity
public class CustomSecurityConfig {
...
}
[6] JWTCheckFilter에 하단 내용 추가
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeaderStr = request.getHeader("Authorization");
//Bearer: 공백 포함 7개, 뒤에가 jwt
try {
String accessToken = authHeaderStr.substring(7);
Map<String, Object> claims = JWTUtil.validateToken(accessToken); //검사
String email = (String) claims.get("email");
String pw = (String) claims.get("pw");
String nickname = (String) claims.get("nickname");
Boolean social = (Boolean) claims.get("social");
List<String> roleNames = (List<String>) claims.get("roleNames");
MemberDTO memberDTO = new MemberDTO(email, pw, nickname, social.booleanValue(), roleNames);
UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken(memberDTO, pw, memberDTO.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}catch (Exception e){
Gson gson = new Gson();
String msg = gson.toJson(Map.of("error", "ERROR_ACCESS_TOKEN"));
response.setContentType("application/json");
PrintWriter printWriter = response.getWriter();
printWriter.println(msg);
printWriter.close();
}
//도착지
filterChain.doFilter(request, response);
}
roleNames: 사용자의 권한 목록(ROLE_USER, ROLE_ADMIN 등)이 들어있음.
MemberDTO 객체 생성 → 사용자 정보를 담고 있음.
UsernamePasswordAuthenticationToken 객체 생성 → Spring Security에서 인증 객체로 사용.
SecurityContextHolder.getContext().setAuthentication(authenticationToken); → Spring Security가 이 사용자를 인증된 상태로 설정함!, @PreAuthorize가 사용자 권한을 체크할 수 있음.
[7] ProductController
@PreAuthorize("hasAnyRole('ROLE_USER','ROLE_ADMIN')")
@GetMapping("/list")
public PageResponseDTO<ProductDTO> list(PageRequestDTO pageRequestDTO){
return productService.getList(pageRequestDTO);
}
이 메서드는 ROLE_USER 또는 ROLE_ADMIN 권한이 있는 사용자만 호출이 가능함
@PreAuthorize를 사용하면, 메서드 실행 전에 Spring Security가 권한을 검사해서 허용된 사용자만 접근할 수 있음.
[8] login 페이지는 filter하면 안 됨
/api/member/로 시작하면 filter 안 함
@Log4j2
public class JWTCheckFilter extends OncePerRequestFilter {
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String path = request.getRequestURI();
if(path.startsWith("/api/member/")){
return true;
}
return false; //check함
}
[9] Gson 이란?
: Java 객체(Map, DTO 등)를 JSON으로 쉽게 변환하는 기술임
Map<String, Object> data = new HashMap<>();
data.put("name", "Alice");
data.put("age", 25);
data.put("isStudent", false);
String json = gson.toJson(data);
System.out.println(json); 하면 {"name":"Alice","age":25,"isStudent":false} 이렇게 나온다.
[10] CustomAccessDeniedHandler
특정 권한을 가진 유저만 접근이 가능함
@Log4j2
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
Gson gson = new Gson();
String jsonStr = gson.toJson(Map.of("error", "ERROR_ACCESSDENIED"));
response.setContentType("application/json");
response.setStatus(HttpStatus.FORBIDDEN.value());
PrintWriter printWriter = response.getWriter();
printWriter.println(jsonStr);
printWriter.close();
}
}
[11] CustomSecurityConfig
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
...
//로그인 성공, 실패 관리
http.formLogin(formLogin -> formLogin
.loginPage("/api/member/login")
.successHandler(new ApiLoginSuccessHandler())
.failureHandler(new ApiLoginFailHandler())
);
// 이 부분 추가하기
http.addFilterBefore(new JWTCheckFilter(), UsernamePasswordAuthenticationFilter.class);
http.exceptionHandling(config -> {
config.accessDeniedHandler(new CustomAccessDeniedHandler());
})
return http.build();
}
[12] ApirefreshController - refresh token을 이용한 토큰 갱신
@RestController
@RequiredArgsConstructor
@Log4j2
public class ApiRefreshController {
@RequestMapping("/api/member/refresh")
public Map<String, Object> refresh(
@RequestHeader("Authorization") String authHeader,
String refreshToken) {
if (refreshToken == null) {
throw new CustomJWTException("NULL_REFRESH");
}
if (authHeader == null || authHeader.length() < 7) {
throw new CustomJWTException("INVALID_STRING");
}
String accessToken = authHeader.substring(7);
// Access Token이 아직 만료되지 않았다면 그대로 반환
if (!checkExpiredToken(accessToken)) {
return Map.of("accessToken", accessToken, "refreshToken", refreshToken);
}
// Refresh Token을 검증하고 새로운 Access Token 생성
Map<String, Object> claims = JWTUtil.validateToken(refreshToken);
String newAccessToken = JWTUtil.generateToken(claims, 10);
String newRefreshToken = checkTime((Integer) claims.get("exp"))
? JWTUtil.generateToken(claims, 60 * 24)
: refreshToken;
return Map.of("accessToken", newAccessToken, "refreshToken", newRefreshToken);
}
// Refresh Token의 남은 유효 시간 확인
private boolean checkTime(Integer exp) {
java.util.Date expDate = new java.util.Date((long) exp * 1000);
long gap = expDate.getTime() - System.currentTimeMillis();
long leftMin = gap / (1000 * 60);
return leftMin < 60; // 남은 시간이 60분 미만이면 새로운 Refresh Token 발급
}
// Access Token 만료 여부 확인
private boolean checkExpiredToken(String token) {
try {
JWTUtil.validateToken(token);
} catch (CustomJWTException ex) {
if ("Expired".equals(ex.getMessage())) {
return true;
}
}
return false;
}
}
'Spring' 카테고리의 다른 글
[2] 자바 중급 문법 2 (0) | 2025.03.21 |
---|---|
[1] 자바 중급 문법 1 (0) | 2025.03.19 |
[9] 상품 생성 (0) | 2025.03.18 |
[7] 소셜 로그인 구현 (0) | 2025.03.12 |
[6] 이미지 파일 업로드 (0) | 2025.03.07 |