Spring Boot 권한 사용 안하는 JWT+Security
- 웹 개발
- 2023. 7. 17.
Spring boot에서 보통 권한을 사용해서 JWT를 사용할거다
현재 나는 권한이 필요 없는 JWT를 만들려고 한다.
Spring boot 3.0.5 버전이다
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
application.properties
jwt.header=Authorization
jwt.access-secret={시크릿키}
jwt.refresh-secret={리프레쉬 토큰 시크릿키}
jwt.access-token-validity-in-seconds={토큰 유효시간 (초)}
jwt.refresh-token-validity-in-seconds={리프레시토큰 유효시간(초)}
JwtTokenProvider.class
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.ArrayList;
import java.util.Date;
@Component
public class jwtTokenProvider implements InitializingBean {
private final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
private final String accessSecret;
private final String refreshSecret;
private final long accessTokenValidity;
private final long refreshTokenValidity;
private Key accessSecretKey;
private Key refreshSecretKey;
public JwtTokenProvider(
@Value("${jwt.access-secret}") String accessSecret,
@Value("${jwt.refresh-secret}") String refreshSecret,
@Value("${jwt.access-token-validity-in-seconds}") long accessTokenValidityInSeconds,
@Value("${jwt.refresh-token-validity-in-seconds}") long refreshTokenValidityInSeconds) {
this.accessSecret = accessSecret;
this.refreshSecret = refreshSecret;
this.accessTokenValidity = accessTokenValidityInSeconds * 1000;
this.refreshTokenValidity = refreshTokenValidityInSeconds * 1000;
}
// application.properties 파일 안에 있는 값을 가져옴
@Override
public void afterPropertiesSet() throws Exception {
byte[] keyBytes = Decoders.BASE64.decode(accessSecret);
this.accessSecretKey = Keys.hmacShaKeyFor(keyBytes);
keyBytes = Decoders.BASE64.decode(refreshSecret);
this.refreshSecretKey = Keys.hmacShaKeyFor(keyBytes);
}
// JWT 토큰 생성 및 검증에 사용할 secret, refreshSecretkey를 생성하는 로직
public String createAccessToken(String username) {
return buildJwtToken(username, accessSecretKey, accessTokenValidity);
}
public String createRefreshToken(String username) {
return buildJwtToken(username, refreshSecretKey, refreshTokenValidity);
}
private String buildJwtToken(String username, Key secretKey, long validityPeriod) {
long now = (new Date()).getTime();
Date validity = new Date(now + validityPeriod);
return Jwts.builder()
.setSubject(username) // username 넣고
.signWith(secretKey, SignatureAlgorithm.HS512) // HS512알고리즘으로 생성
.setExpiration(validity) // 만료 시간 넣기
.compact();
}
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey) // 서명 검증에 사용할 키
.build()
.parseClaimsJws(token)
.getBody();
// jwt토큰 문자열을 파싱해서 Claims 객체 얻음
// Claims 객체는 jwt 토큰의 페이로드를 담고 있음
User principal = new User(claims.getSubject(), "N/A", new ArrayList<>());
//User 클래스는 spring Security에서 사용자 정보를 담는 클래스
//claims 객체에서 주체를 가져오고 비밀번호, 권한을 사용 안하기에 비워둠
return new UsernamePasswordAuthenticationToken(principal, token, new ArrayList<>());
// UsernamePasswordAuthenticationToken 클래스는 스프링 시큐리티에서 사용자의 인증 정보를 담음
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
return true;
} catch (ExpiredJwtException e) {
logger.info("Expired JWT token.");
throw new ExpiredJwtTokenException(CommonResponseStatus.EXPIRED_JWT);
} catch (JwtException | IllegalArgumentException e) {
logger.info("Invalid JWT token : " + e.getMessage());
throw new UnsuitableJwtException();
}
}
public String extractIDs(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(refreshSecretKey)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
} catch (ExpiredJwtException e) {
throw new InvalidJwtTokenException(CommonResponseStatus.EXPIRED_JWT);
} catch (JwtException e) {
throw new InvalidJwtTokenException(CommonResponseStatus.UNSUITABLE_JWT);
}
}
// refreshToken 검증할때 사용
}
JwtFilter.class
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;
import project.response.ErrorCode;
import project.response.ErrorResponse;
import java.io.IOException;
import java.util.Set;
@RequiredArgsConstructor
public class JwtFilter extends GenericFilterBean {
private final JwtTokenProvider jwtTokenProvider;
private final ObjectMapper objectMapper = new ObjectMapper();
private final Set<String> securedPaths = Set.of({JWT 검사할 경로 URL});
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
String path = httpServletRequest.getRequestURI(); // 어떤 경로로 접속했는지 가져옴
if (authenticationPaths.stream().anyMatch(path::startsWith)) { // 경로가 securedPaths로 시작하는지 검사
try {
String jwt = resolveToken(httpServletRequest.getHeader(AUTHORIZATION_HEADER)); // 헤더 값을 가져옴
jwtTokenProvider.validateToken(jwt); // JWT 검사
Authentication authentication = jwtTokenProvider.getAuthentication(jwt); //
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
} catch (InvalidJwtException e) { // 올바르지 않은 경우
sendResponse(httpServletResponse, JsonResponseStatus.INVALID_JWT);
} catch (ExpiredJwtTokenException e) { // 시간이 만료된 경우
sendResponse(httpServletResponse, JsonResponseStatus.ACCESS_TOKEN_EXPIRED);
} catch (MissingJwtException e) { // JWT가 없는 경우
sendResponse(httpServletResponse, JsonResponseStatus.MISSING_JWT);
}
} else {
chain.doFilter(request, response);
}
}
// 에러 전송
private void sendResponse(HttpServletResponse response, JsonResponseStatus status) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(convertObjectToJson(new JsonResponse<>(status)));
}
private String convertObjectToJson(Object object) throws IOException {
if (object == null)
return null;
return objectMapper.writeValueAsString(object);
}
private String getBearerToken(String token) {
if (StringUtils.hasText(token) && token.startsWith("Bearer "))
return token.substring(7);
throw new MissingJwtException(JsonResponseStatus.MISSING_JWT);
}
}
JwtAuthenticationEntryPoint.class
package project.neighborhoodcommunity.jwt;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import java.io.IOException;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
// 유효하지 않은 jwt를 가지고 접근할때 401에러
CustomUserDetailService.class
package project.neighborhoodcommunity.jwt;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
@Component
public class CustomUserDetailsService implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
return new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials().toString(), null);
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
JwtSecurityConfig.class
package project.neighborhoodcommunity.jwt;
import lombok.RequiredArgsConstructor;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@AllArgsConstructor
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private JwtTokenProvider jwtTokenProvider;
@Override
public void configure(HttpSecurity builder) throws Exception {
builder.addFilterBefore(new JwtFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
}
// HttpSecurity 객체를 통해 보안 설정을 구성하는 메서드
// Http 요청이 들어올 때, Username~~ 실행전에 JwtFilter를 실행하도록 설정
}
SecurityConfig.class
package project.neighborhoodcommunity.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.CorsFilter;
import project.neighborhoodcommunity.jwt.JwtAuthenticationEntryPoint;
import project.neighborhoodcommunity.jwt.JwtSecurityConfig;
import project.neighborhoodcommunity.jwt.JwtTokenProvider;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CorsFilter corsFilter;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtTokenProvider jwtTokenProvider;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
//CORS 필터 먼저 거치고 Username~~ 필터 순으로
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
// 인증 실패시 jwtAuthen~~를 통해 예외처리 설정
.and()
.headers()
.frameOptions()
.sameOrigin()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 세션 사용하지 않음
// .requiresChannel()
// .anyRequest().requiresSecure()
// .and()
.apply(new JwtSecurityConfig(jwtTokenProvider));
// JWT를 이용한 인증 설정
return http.build();
}
}