코드 그라데이션
JWT 예제 구현 (4) JWT 기본 코드 구현 + Security 설정 추가 본문
build.gradle에 jwt 관련 코드를 추가
plugins {
id 'java'
id 'org.springframework.boot' version '2.4.1' // Spring Boot 버전을 2.4.1로 변경
id 'io.spring.dependency-management' version '1.0.10.RELEASE' // Spring Dependency Management 버전 변경
}
group = 'inflearn'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2:1.4.200'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
// 추가!!
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
}
tasks.named('test') {
useJUnitPlatform()
}
application.yml 에 jwt 관련 내용 추가
spring:
h2:
console:
enabled: true
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create-drop
properties:
hibernate:
format_sql: true
show_sql: true
# 추가!!!
jwt:
header: Authorization
#HS512 알고리즘을 사용할 것이기 때문에 512bit, 즉 64byte 이상의 secret key를 사용해야 한다.
#echo 'silvernine-tech-spring-boot-jwt-tutorial-secret-silvernine-tech-spring-boot-jwt-tutorial-secret'|base64
secret: c2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQtc2lsdmVybmluZS10ZWNoLXNwcmluZy1ib290LWp3dC10dXRvcmlhbC1zZWNyZXQK
token-validity-in-seconds: 86400
logging:
level:
inflearn.freejwt: DEBUG
jwt 디렉토리 생성
TokenProvider.java
package inflearn.freejwt.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
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.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;
@Component
public class TokenProvider implements InitializingBean {
private final Logger logger = LoggerFactory.getLogger(TokenProvider.class);
private static final String AUTHORITIES_KEY = "auth";
private final String secret;
private final long tokenValidityInMilliseconds;
private Key key;
public TokenProvider(
@Value("${jwt.secret}") String secret,
@Value("${jwt.token-validity-in-seconds}") long tokenValidityInSeconds) {
this.secret = secret;
this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000;
}
@Override
public void afterPropertiesSet() {
// JWT 서명을 위한 Key 생성
byte[] keyBytes = Decoders.BASE64.decode(secret);
this.key = Keys.hmacShaKeyFor(keyBytes);
}
/**
* InitializingBean을 implements 해서 afterPropertiesSet을 오버라이드 한 이유는
* 빈이 생성 되고 주입을 받은 후에 secret 값을 Base64 Decode 해서 key 변수에 할당하기 위함.
*/
/**
*
* @param authentication
* @return
*
* Authentication 객체의 권한정보를 이용해서 토큰을 생성하는 createToken 메서드 추가
*/
public String createToken(Authentication authentication) {
// 사용자 권한을 JWT에 추가
String authorities = authentication.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
long now = (new Date()).getTime();
Date validity = new Date(now + this.tokenValidityInMilliseconds); // 토큰 만료 시간
// JWT 토큰 생성
return Jwts.builder()
.setSubject(authentication.getName())
.claim(AUTHORITIES_KEY, authorities)
.signWith(key, SignatureAlgorithm.HS512)
.setExpiration(validity)
.compact();
}
/**
*
* @param token
* @return
*
* Token에 담겨있는 정보를 이용해 Authentication 객체를 리턴하는 메서드 생성
*/
public Authentication getAuthentication(String token) {
// JWT 토큰 파싱
// 토큰을 파라미터로 받아서 클레임을 만들고
Claims claims = Jwts
.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
// 사용자 권한 파싱 및 Principal 생성
Collection<? extends GrantedAuthority> authorities = // 클레임에서 권한정보들을 빼내서
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// 권한정보들을 이용해서 User 객체를 만들어 주고,
User principal = new User(claims.getSubject(), "", authorities);
// 유저객체와 토큰, 권한정보를 이용해서 최종적으로
// Authentication 객체 반환
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
/**
*
* @param token
* @return
* 토큰을 파라미터로 받아서 토큰의 유효성 검사를 수행하는 validateToken 메서드 추가
*/
public boolean validateToken(String token) {
try {
// 토큰을 파싱해보고
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
// 나오는 Exception들을 캐치하고
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
logger.info("잘못된 JWT 서명입니다.");
} catch (ExpiredJwtException e) {
logger.info("만료된 JWT 토큰입니다.");
} catch (UnsupportedJwtException e) {
logger.info("지원되지 않는 JWT 토큰입니다.");
} catch (IllegalArgumentException e) {
logger.info("JWT 토큰이 잘못되었습니다.");
}
return false;
}
}
JwtFilter.java
package inflearn.freejwt.jwt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class JwtFilter extends GenericFilterBean {
private static final Logger logger = LoggerFactory.getLogger(JwtFilter.class);
// Authorization 헤더의 이름
public static final String AUTHORIZATION_HEADER = "Authorization";
private TokenProvider tokenProvider;
public JwtFilter(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
/**
*
* @param servletRequest The request to process
* @param servletResponse The response associated with the request
* @param filterChain Provides access to the next filter in the chain for this
* filter to pass the request and response to for further
* processing
*
* @throws IOException
* @throws ServletException
*
* 실제 필터링 로직은 이 메서드 내부에 작성
* 이것은 토큰의 인증정보를 SecurityContext에 저장하는 역할을 수행
*/
@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String jwt = resolveToken(httpServletRequest);
String requestURI = httpServletRequest.getRequestURI();
// 추출한 JWT 토큰이 유효한지 검증
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
// JWT 토큰을 사용하여 사용자 인증 정보 생성
Authentication authentication = tokenProvider.getAuthentication(jwt);
// 정상 토큰이면 Spring Security의 SecurityContextHolder에 인증 정보 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
// 로깅
logger.debug("Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}", authentication.getName(), requestURI);
} else {
// 유효한 JWT 토큰이 없을 경우 로깅
logger.debug("유효한 JWT 토큰이 없습니다, uri: {}", requestURI);
}
// 다음 필터 또는 요청 처리로 전달
filterChain.doFilter(servletRequest, servletResponse);
}
/**
*
* @param request
* @return
* Request Header에서 토큰 정보를 꺼내오기 위한 resolveToken 메서드
*/
// Authorization 헤더에서 JWT 토큰 추출
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
JwtSecurityConfig
/**
* TokenProvider, JwtFilter를 SecurityConfig에 적용할 때 사용할 클래스
*/
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private TokenProvider tokenProvider;
@Override
public void configure(HttpSecurity http) throws Exception {
JwtFilter customFilter = new JwtFilter(tokenProvider);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
JwtAuthenticationEntryPoint
package inflearn.freejwt.jwt;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
// 유효한 자격증명을 제공하지 않고 접근하려고 할 때 401 Unauthorized 에러를 리턴할 클래스
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED); // 401에러
}
}
JwtAccessDeniedHandler
package inflearn.freejwt.jwt;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
// 필요한 권한이 존재하지 않는 경우에 403 에러 리턴하기 위함
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
}
CorsConfig
package inflearn.freejwt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
// CORS 필터 빈을 정의하는 메서드
@Bean
public CorsFilter corsFilter() {
// URL 기반의 CORS 구성을 관리하는 객체 생성
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// CORS 구성 설정 객체 생성
CorsConfiguration config = new CorsConfiguration();
// 요청에서 자격 증명(자격 증명 쿠키, 인증 등)을 허용
config.setAllowCredentials(true);
// 모든 출처(Origin)를 허용
config.addAllowedOrigin("*");
// 모든 HTTP 헤더를 허용
config.addAllowedHeader("*");
// 모든 HTTP 메서드(GET, POST, PUT, DELETE 등)를 허용
config.addAllowedMethod("*");
// "/api/**" 패턴에 해당하는 엔드포인트에 대해 CORS 구성을 등록
source.registerCorsConfiguration("/api/**", config);
// CORS 필터를 생성하고 반환
return new CorsFilter(source);
}
}
728x90
'Spring > Security' 카테고리의 다른 글
JWT 예제 구현 (6) Dto, Repository, UserDetailsService 구현 인터페이스 (0) | 2023.12.12 |
---|---|
JWT 예제 구현 (5) 5개의 클래스를 SecurityConfig에 적용 (0) | 2023.12.11 |
JWT 예제 구현 (3) SecurityConfig 추가, 기본 엔티티 구현 (0) | 2023.12.09 |
JWT 예제 구현 (2) 권한 에러 해결, 데이터베이스 연결 (1) | 2023.12.09 |
JWT 예제 구현 (1) 프로젝트 세팅 (0) | 2023.12.08 |
Comments