✅ Security Config + Jwt Filter추가
@Bean
public SecurityFilterChain defaultFilterChain(HttpSecurity http) throws Exception {
http
// 새로운 방식으로 CORS 설정 적용
.cors(cors -> cors.configurationSource(corsConfig.corsConfigurationSource()))
//csrf disable
.csrf(AbstractHttpConfigurer::disable)
//form 로그인 방식 disalbe
.formLogin(AbstractHttpConfigurer::disable)
//http basic 인증 방식 disable
.httpBasic(AbstractHttpConfigurer::disable)
//JwtFilter 추가
.addFilterBefore(new JwtFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class)
//JwtAuthentication Exception Hadling
.exceptionHandling(authenticationManager -> authenticationManager
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler()))
.authorizeHttpRequests(authorize -> authorize
// 공개 접근 허용 URL 설정
.requestMatchers("/" , "/api/member/login").permitAll()
.requestMatchers("/css/**", "/js/**", "/img/**", "/favicon.ico").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
// .requestMatchers("/seller/**").hasRole("SELLER")
.requestMatchers("/mypage/**").hasRole("USER")
.anyRequest().authenticated()
)
//JWT 사용하기 때문에 세션 설정 : STATELESS
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.permitAll()
)
//GET 요청을 통해 로그아웃을 처리하도록 허용
.logout(logout -> logout.logoutRequestMatcher(
new OrRequestMatcher(
new AntPathRequestMatcher("/logout", "GET"),
new AntPathRequestMatcher("/logout", "POST")
)
))
// .userDetailsService(customUserDetailsService)
// OAuth2 로그인 설정
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService)
)
.successHandler(customOAuth2SuccessHandler)
.failureHandler((request, response, exception) -> {
// OAuth2 로그인 실패 시 처리
response.sendRedirect("/login?error=oauth2");
})
);
return http.build();
}
🖥️ JwtUtil 코드
@Slf4j
@Component
public class JwtUtil {
private final SecretKey secretKey ;
private final Long expirationTime;
private final Long refreshExpirationTime;
private final CustomUserDetailsService customUserDetailsService;
private final MemberRepository memberRepository;
private final RefreshTokenRepository refreshTokenRepository;
public JwtUtil(
@Value("${spring.jwt.secret}") String secret,
@Value("${spring.jwt.expiration-time}") Long expirationTime,
@Value("${spring.jwt.refresh-expiration-time}") Long refreshExpirationTime,
CustomUserDetailsService customUserDetailsService, MemberRepository memberRepository, RefreshTokenRepository refreshTokenRepository) {
this.customUserDetailsService = customUserDetailsService;
this.memberRepository = memberRepository;
this.refreshTokenRepository = refreshTokenRepository;
secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8),
Jwts.SIG.HS256.key().build().getAlgorithm());
this.expirationTime = expirationTime;
this.refreshExpirationTime = refreshExpirationTime;
}
// 토큰에서 회원 이름 추출
public String getUsername(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class);
}
// 토큰에서 이메일 추출 PK
public String getEmail(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("email", String.class);
}
//JWT 토큰에서 인증정보 조회
public Authentication getAuthentication(String token) {
UserDetails userDetails = customUserDetailsService.loadUserByUsername(this.getEmail(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
public Member getMember(String token) {
return memberRepository.findByEmail(this.getEmail(token))
.orElseThrow(() -> new UsernameNotFoundException("Not Found USER"));
}
public Boolean isExpired(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
}
public JwtDto createTokens(Member member) throws ParseException {
String accessToken = createAccessToken(member);
String refreshToken = createRefreshToken(member);
log.info("success to create tokens");
return JwtDto.builder()
.grantType("Bearer")
.access_token(accessToken)
.refresh_token(refreshToken)
.build();
}
public String createAccessToken(Member member) throws ParseException {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expirationTime);
// JWT access token 생성
return Jwts.builder()
.claim("email", member.getEmail())
.claim("name", member.getMemberName())
.claim("roles", member.getRoles())
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(expiryDate)
.signWith(secretKey)
.compact();// builder에서 최종적으로 JWT string 생성
}
public String createRefreshToken(Member member) throws ParseException {
// 토큰의 subject가 user의 Email로 저장되므로 loadUserByUsername()의 인자로 넘기기
// Member member = (Member) customUserDetailsService.loadUserByUsername(accessToken);
// 해당 유저에 대해 refresh token 조회
Optional<RefreshToken> refreshTokenByUser = refreshTokenRepository.findByMemberEmail(member.getEmail());
// refresh token이 이미 존재하는 경우
if(refreshTokenByUser.isPresent())
return refreshTokenByUser.get().getRefreshToken(); // 해당 refresh token 반환
// refresh token이 존재하지 않으므로 refresh token 생성
Date refreshValidity = new Date(new Date().getTime() + refreshExpirationTime); // refreshToken 만료 기간 설정
String refreshToken = Jwts.builder()
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(refreshValidity)
.signWith(secretKey)
.compact(); // 최종 JWT string 생성
// RefreshTokenRepository에 user와 매핑해 refresh token 저장
refreshTokenRepository.save(new RefreshToken(member, refreshToken, refreshValidity));
return refreshToken;
}
public boolean validateToken(String token) {
try {
Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.info("Invalid JWT signature.");
} catch (ExpiredJwtException e) {
log.info("Expired JWT token.");
} catch (UnsupportedJwtException e) {
log.info("JWT token not supported.");
} catch (IllegalArgumentException e) {
log.info("JWT token is invalid");
}
return false;
}
public String validateRefreshToken(String accessToken, String token) throws ParseException {
RefreshToken refreshToken = refreshTokenRepository.findByRefreshToken(token)
.orElseThrow(() -> new BusinessException(ErrorCode.REFRESH_TOKEN_INVALD));
// 만료된 accessToken의 주인과 입력한 refresh token의 유저가 같은지 판단
if(!this.getEmail(accessToken).equals(refreshToken.getMember().getEmail()))
throw new BusinessException(ErrorCode.NOT_MATCH_USER_ACCESSTOKEN_REFRESHTOKEN);
// refresh token이 유효하지 않은 경우 (만료기간이 지난 경우) => 로그인 풀도록 요청
if(refreshToken.getValidityDate().before(new Date())) {
// RefreshToken DB에서 해당 엔티티 삭제 후 만료 response 반환
refreshTokenRepository.delete(refreshToken);
return HttpStatus.UNAUTHORIZED.toString();
}
// refresh token이 유효한 경우 => access token 발급
else{
Member member = (Member) customUserDetailsService.loadUserByUsername(refreshToken.getMember().getEmail());
return createAccessToken(member);
}
}
}
🖥️ validation token & refresh token validation 코드
public boolean validateToken(String token) {
try {
Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.info("Invalid JWT signature.");
} catch (ExpiredJwtException e) {
log.info("Expired JWT token.");
} catch (UnsupportedJwtException e) {
log.info("JWT token not supported.");
} catch (IllegalArgumentException e) {
log.info("JWT token is invalid");
}
return false;
}
public String validateRefreshToken(String accessToken, String token) throws ParseException {
RefreshToken refreshToken = refreshTokenRepository.findByRefreshToken(token)
.orElseThrow(() -> new BusinessException(ErrorCode.REFRESH_TOKEN_INVALD));
// 만료된 accessToken의 주인과 입력한 refresh token의 유저가 같은지 판단
if(!this.getEmail(accessToken).equals(refreshToken.getMember().getEmail()))
throw new BusinessException(ErrorCode.NOT_MATCH_USER_ACCESSTOKEN_REFRESHTOKEN);
// refresh token이 유효하지 않은 경우 (만료기간이 지난 경우) => 로그인 풀도록 요청
if(refreshToken.getValidityDate().before(new Date())) {
// RefreshToken DB에서 해당 엔티티 삭제 후 만료 response 반환
refreshTokenRepository.delete(refreshToken);
return HttpStatus.UNAUTHORIZED.toString();
}
// refresh token이 유효한 경우 => access token 발급
else{
Member member = (Member) customUserDetailsService.loadUserByUsername(refreshToken.getMember().getEmail());
return createAccessToken(member);
}
}
✏️ 공부한점
① 스프링부트에서 @Value는 @RequiredArgsConstructor에 의해 주입받지 못한다.
필드 변수를 선언할때 @RequiredArgsConstructor를 이용하기 위해 항상 하던 것처럼
@Value 변수를 선언할 때에도 private final로 선언했지만 다음과 같은 에러가 발생
Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'java.lang.String' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
Description:
Parameter 0 of constructor in ai.planit.superb.domain.mail.service.Test required a bean of type 'java.lang.String' that could not be found.
Action:
Consider defining a bean of type 'java.lang.String' in your configuration.
Lombok의 @RequiredArgsConstructor로 필드의 private final의 컴파일 에러는 해결된 것처럼 보이지만
@Value가 주입되는 타이밍과 @RequiredArgsConstructor의 주입 타이밍이 달라 에러가 발생하는 것이 원인
그래서 생성자 주입방식으로 하나씩 값을 넣어주었다.
public JwtUtil(
@Value("${spring.jwt.secret}") String secret,
@Value("${spring.jwt.expiration-time}") Long expirationTime,
@Value("${spring.jwt.refresh-expiration-time}") Long refreshExpirationTime,
CustomUserDetailsService customUserDetailsService, MemberRepository memberRepository, RefreshTokenRepository refreshTokenRepository) {
this.customUserDetailsService = customUserDetailsService;
this.memberRepository = memberRepository;
this.refreshTokenRepository = refreshTokenRepository;
secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8),
Jwts.SIG.HS256.key().build().getAlgorithm());
this.expirationTime = expirationTime;
this.refreshExpirationTime = refreshExpirationTime;
}
② Jwts.parser().verifyWith(key).build().parseSignedClaims(jwt)
Jwts.parser()로 토큰을 열기위한 도구 세팅!
(파서)verifyWith(key)로 서명을 검증할 키를 설정하고 (키)build()로 최종적으로 토큰을 열기위한 도구 세팅 완료 ( JwtParser 객체 생성)parseSignedClaims(jwt)로 jwt문자열을 열어서 서명이 잘 되었는지 확인!
③ Jwt생성
subject(토큰을 발급받는 주체가 누구인지),
issuer(토큰을 발행하는 주체가 누구인지) ,
issuedAt(토큰이 언제 발급되었는지),
expiration(토큰의 만료시간은 언제인지),
claims(어떤 정보가 있는지),
signWith(어떤 key로 토큰에 서명을 할건지),
compact() : 설정된 모든 정보를 기반으로 JWT토큰 생성하고 문자열로 반환!
'Back-end > Spring Security' 카테고리의 다른 글
| [Spring Security] Security + JWT 설정 (JwtFilter편) (0) | 2025.05.27 |
|---|---|
| [Spring Security] 기본 동작과정, 필터 구조 (0) | 2025.05.22 |