반응형
이어지는 이전 글 : [Spring Security] JWT토큰 적용하기
JWT토큰관련 class
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import lombok.Getter;
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;
@RequiredArgsConstructor
@Slf4j
@Component
public class JwtTokenizer {
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String REFRESH_HEADER = "Refresh";
public static final String BEARER_PREFIX = "Bearer";
// secretKey가져오기
@Getter
// @Value import주의
@Value("${jwt.secret}")
private String secretKey;
// accessToken만료시간
@Getter
@Value("${jwt.access-token-expiration-millis}")
private long accessTokenExpirationMillis;
// refreshToken만료시간
@Getter
@Value("${jwt.refresh-token-expiration-millis}")
private long refreshTokenExpirationMillis;
private Key key;
// bean등록 후 Key SecretKey HS알고리즘 decode
// @PostConstruct는 의존성 주입이 이루어진 후 초기화를 수행하는 메서드이다.
// @PostConstruct가 붙은 메서드는 @Component, @Service, @Repository, @Controller 클래스가 Spring Bean에 등록되기 전에 발생한다.
// 이 메서드는 다른 리소스에서 호출되지 않는다해도 수행된다.
// bean이 초기화 됨과 동시에 의존성 확인 가능 = @Autowired나 @Value를 붙여 객체 사용시 사용 가능
// 실행 bean lifecycle 동안 오직 한번만 수행되는 것을 보장 = bean이 여러번 초기화 되는 것을 방지
@PostConstruct
public void init() {
String base64EncodedSecretKey = encodeBase64URLSecretKey(this.secretKey);
this.key = getKeyFromBase64URLEncodedKey(base64EncodedSecretKey);
}
// 인코딩
public String encodeBase64URLSecretKey(String secretKey) {
return Encoders.BASE64URL.encode(secretKey.getBytes(StandardCharsets.UTF_8));
}
// 디코딩
private Key getKeyFromBase64URLEncodedKey(String base64EncodedSecretKey) {
byte[] keyBytes = Decoders.BASE64URL.decode(base64EncodedSecretKey);
return Keys.hmacShaKeyFor(keyBytes);
}
public String generateAccessToken(Map<String, Object> claims, String subject,
Date expiration, String encodeBase64URLSecretKey) {
Key key = getKeyFromBase64URLEncodedKey(encodeBase64URLSecretKey);
return Jwts.builder()
// Custom Claims 추가 ( 인증된 사용자와 관련 정보 )
.setClaims(claims)
// JWT 제목 (사용자 구분용 도로 사용예정)
.setSubject(subject)
// accessToken의 발행 시간
.setIssuedAt(Calendar.getInstance().getTime())
// accessToken의 만료 시간
.setExpiration(expiration)
// 서명
.signWith(key)
// JWT생성 및 직렬화
.compact();
}
// AccessToken을 갱신하기에 Custom Claims가 필요없음
public String generateRefreshToken(String subject, Date expiration,
String encodeBase64URLSecretKey) {
Key key = getKeyFromBase64URLEncodedKey(encodeBase64URLSecretKey);
return Jwts.builder()
// JWT 제목 (사용자 구분용 도로 사용예정)
.setSubject(subject)
// refreshToken의 발행 시간
.setIssuedAt(Calendar.getInstance().getTime())
// refreshToken의 만료 시간
.setExpiration(expiration)
// 서명
.signWith(key)
// JWT생성 및 직렬화
.compact();
}
public void setAccessTokenHeader(String accessToken, HttpServletResponse response) {
String headerValue = BEARER_PREFIX + accessToken;
response.setHeader(AUTHORIZATION_HEADER, headerValue);
}
public void setRefreshTokenHeader(String refreshToken, HttpServletResponse response) {
response.setHeader(REFRESH_HEADER, refreshToken);
}
// token 복호화
public Claims parseClaim(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
// Request Header에서 Access Token 정보 추출
public String resolveAccessToken(HttpServletRequest request) {
// Header값 추출
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
// Header값 비교
if(StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
// Header 텍스트 제외하고 토큰 값 추출
return bearerToken.substring(7);
}
return null;
}
// Request Header에서 Refresh Token 정보 추출
public String resolveRefreshToken(HttpServletRequest request) {
// Header값 추출
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
// Header값 비교
if(StringUtils.hasText(bearerToken)) {
// 토큰 값 추출
return bearerToken;
}
return null;
}
// 토큰 검증
public boolean validateToken(String token) {
try {
// parsing하여 정보 추출
parseClaim(token);
} catch (MalformedJwtException e) {
log.info("Invalid JWT token");
log.trace("Invalid JWT token trace = {}", e);
} catch (ExpiredJwtException e) {
log.info("Expired JWT token");
log.trace("Expired JWT token trace = {}", e);
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token");
log.trace("Unsupported JWT token trace = {}", e);
} catch (IllegalArgumentException e) {
log.info("JWT claims string is empty.");
log.trace("JWT claims string is empty trace = {}", e);
}
return true;
}
}
JWT인증 필터
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
@Slf4j
@RequiredArgsConstructor
// JWT 토큰 인증
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final CustomUserDetailService customUserDetailService;
private final JwtTokenizer jwtTokenizer;
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException{
// access토큰 정보 추출
String token = jwtTokenizer.resolveAccessToken(request);
// 토큰 검증
if(token != null && jwtTokenizer.validateToken(token)){
// 토큰에서 유저정보 추출
String email = jwtTokenizer.parseClaim(token).getSubject();
// 사용자 인증
UserDetails userDetails = customUserDetailService.loadUserByUsername(email);
if (userDetails != null) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails.getUsername(), null, userDetails.getAuthorities());
log.info("authenticated user with email : {}", email);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}
CustomLoginSuccessHandler수정
JWT는 인증 용도로 사용하기에 최초 로그인 후 발급해도 무관하다. 그래서 로그인 성공 핸들러에서 헤더에 넣도록 해두었다.
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
@Component(value = "customAuthenticationSuccessHandler")
@Slf4j
@RequiredArgsConstructor
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {
private final Log logger = LogFactory.getLog(this.getClass());
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
// 성공시 토큰 발행
private JwtTokenizer jwtTokenizer;
public CustomLoginSuccessHandler(JwtTokenizer jwtTokenizer){
this.jwtTokenizer = jwtTokenizer;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
// JWT 토큰 생성
String username = authentication.getName();
String accessToken = jwtTokenizer.generateAccessToken(Map.of("username", username), username,
new Date(System.currentTimeMillis() + jwtTokenizer.getAccessTokenExpirationMillis()), jwtTokenizer.getSecretKey());
String refreshToken = jwtTokenizer.generateRefreshToken(username,
new Date(System.currentTimeMillis() + jwtTokenizer.getAccessTokenExpirationMillis()), jwtTokenizer.getSecretKey());
// JWT 토큰 발행
jwtTokenizer.setAccessTokenHeader(accessToken, response);
jwtTokenizer.setRefreshTokenHeader(refreshToken, response);
if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_OK);
Map<String, Object> responseData = new HashMap<>();
String targetUrl = determineTargetUrl(authentication); // 사용자 역할에 따라 URL 결정
responseData.put("redirectUrl", targetUrl); // 리디렉션할 URL을 JSON 응답에 포함
new ObjectMapper().writeValue(response.getWriter(), responseData);
} else {
handle(request, response, authentication);
}
clearAuthenticationAttributes(request);
}
private void handle(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
String targetUrl = determineTargetUrl(authentication);
logger.debug(authentication);
if (response.isCommitted()) {
logger.debug("Response has already been committed. Unable to redirect to "
+ targetUrl);
return;
}
redirectStrategy.sendRedirect(request, response, targetUrl);
}
private String determineTargetUrl(final Authentication authentication) {
// roleTargetUrlMap -> targetUrlParameterValue
Map<String, String> roleTargetUrlMap = new HashMap<>();
roleTargetUrlMap.put("ROLE_User", "/main");
roleTargetUrlMap.put("ROLE_Admin", "/admin/main");
final Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (final GrantedAuthority grantedAuthority : authorities) {
String authorityName = grantedAuthority.getAuthority();
if (roleTargetUrlMap.containsKey(authorityName)) {
return roleTargetUrlMap.get(authorityName);
}
}
throw new IllegalStateException();
}
private void clearAuthenticationAttributes(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return;
}
session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
}
}
SpringSecurityConfig수정
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.context.DelegatingSecurityContextRepository;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class SpringSecurityConfig extends AbstractHttpConfigurer {
private final AuthenticationConfiguration authenticationConfiguration;
private final JwtTokenizer jwtTokenizer;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
http.authenticationManager(authenticationManager);
http
.authorizeHttpRequests(authorizeRequest ->
authorizeRequest
.requestMatchers(new AntPathRequestMatcher("/main/**")).hasRole("User")
// .requestMatchers(new AntPathRequestMatcher("/main/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/admin/main/**")).hasRole("Admin")
.requestMatchers(new AntPathRequestMatcher("/login/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/loginFail/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/h2/**")).permitAll()
.anyRequest().permitAll()
)
// 필터 변경
.addFilterAt(
this.abstractAuthenticationProcessingFilter(authenticationManager, jwtTokenizer),
UsernamePasswordAuthenticationFilter.class)
.headers(
headersConfigurer ->
headersConfigurer
// SameOrigin Policy
.frameOptions(
FrameOptionsConfig::sameOrigin
)
// CSP
.contentSecurityPolicy(policyConfig ->
policyConfig.policyDirectives(
"script-src 'self'; img-src 'self'; font-src 'self' data:; default-src 'self'; frame-src 'self'"
).reportOnly()
)
)
return http.build();
}
// 인증 필터
public AbstractAuthenticationProcessingFilter abstractAuthenticationProcessingFilter(
// final AuthenticationManager authenticationManager) {
// 토큰 정보 추가
final AuthenticationManager authenticationManager, JwtTokenizer jwtTokenizer) {
LoginAuthenticationFilter loginAuthenticationFilter = new LoginAuthenticationFilter("/ajax/loginProcess", authenticationManager);
// loginAuthenticationFilter.setAuthenticationSuccessHandler(customSuccessHandler());
// Handler에 토큰 정보 추가
loginAuthenticationFilter.setAuthenticationSuccessHandler(customSuccessHandler(jwtTokenizer));
// Rest API 방식을 사용하기 위해 추가
loginAuthenticationFilter.setSecurityContextRepository(
new DelegatingSecurityContextRepository(
new RequestAttributeSecurityContextRepository(),
new HttpSessionSecurityContextRepository()
));
return loginAuthenticationFilter;
}
// 로그인 성공시 handler
@Bean
public AuthenticationSuccessHandler customSuccessHandler(JwtTokenizer jwtTokenizer) {
return new CustomLoginSuccessHandler(jwtTokenizer);
}
}
반응형
'Backend > Spring | SpringBoot' 카테고리의 다른 글
[JPA] Method is only allowed for a query (0) | 2024.04.30 |
---|---|
[Spring Security] JWT Cookie 저장 작업 (0) | 2024.04.04 |
[Spring Security] Onceperrequestfilter vs Usernamepasswordauthenticationfilter (0) | 2024.04.02 |
[Spring Security] JWT토큰 적용하기 (0) | 2024.04.01 |
[Spring Security] Redirect Page의 JSP불러오기 (0) | 2024.03.28 |