개발자의 서재
Chaptor.03 - JWT코드, Security 설정 추가 본문
SpringBootProject/SringBoot_JWT_RestApi
Chaptor.03 - JWT코드, Security 설정 추가
ironmask431 2022. 4. 3. 16:58
<진행할 것>
- JWT 설정 추가
- JWT 관련 코드 추가
- Security 설정 추가
1. application.yaml 에 jwt관련 설정 추가
여기서는 HS512 알고리즘을 사용해 암호화를 할 것이기 때문에 Secret Key는 64 byte 이상이어야 한다.
Secret Key는 특정 문자열을 Base64 로 인코딩한 값으로 설정한다.
jwt:
header: Authorization
#secret : 별도 문자열을 base64로 암호화한값
secret: c3ByaW5nYm9vdC1qd3QtdHV0b3JpYWwtc3ByaW5nYm9vdC1qd3QtdHV0b3JpYWwtc3ByaW5nYm9vdC1qd3QtdHV0b3JpYWwK
#토큰만료시간(ms)
token-validity-in-seconds: 3600
base64 인코딩은 리눅스에서 아래와 같이 입력하면 확인 가능
echo '문자열'|base64
secret key가 너무 짧으면 구동시 아래와 같은 에러가 발생함.
Invocation of init method failed; nested exception is io.jsonwebtoken.security.WeakKeyException:
The specified key byte array is 192 bits which is not secure enough for any JWT HMAC-SHA algorithm.
The JWT JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms
MUST have a size >= 256 bits (the key size must be greater than or equal to the hash output size).
Consider using the io.jsonwebtoken.security.Keys#secretKeyFor(SignatureAlgorithm)
method to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm.
2. jwt 패키지 생성 / 토큰의 생성, 유효성 검증을 담당할 Token Provider 생성
TokenProvider.java 생성
package com.leesh.springbootjwttutorial.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
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;
@Slf4j
@Component
public class TokenProvider implements InitializingBean {
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 tokenValidityInMilliseconds
){
this.secret = secret;
this.tokenValidityInMilliseconds = tokenValidityInMilliseconds * 1000; //3600 * 1000 ms = 1시간
}
@Override
public void afterPropertiesSet(){
//TokenProvider bean 생성 후 생성자로 주입받은 secret 값을 이용해 암호화 키 생성
byte[] keyBytes = Decoders.BASE64.decode(secret);
this.key = Keys.hmacShaKeyFor(keyBytes);
}
//Authentication 객체를 받아서 토큰생성, 반환
public String createToken(Authentication authentication){
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();
}
//토큰을 받아서 Authentication 객체를 반환
public Authentication getAuthentication(String token){
Claims claims = Jwts
.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
User principal = new User(claims.getSubject(),"",authorities);
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
//토큰의 유효성 검사
public boolean validateToken(String token){
try{
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
}catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e){
log.info("잘못된 JWT 서명입니다.");
}catch (ExpiredJwtException e){
log.info("만료된 JWT 서명입니다.");
}catch (UnsupportedJwtException e){
log.info("지원되지 않는 JWT 서명입니다.");
}catch (IllegalArgumentException e){
log.info("JWT 토큰이 잘못되었습니다.");
}
return false;
}
}
JwtFilter.java 생성
package com.leesh.springbootjwttutorial.jwt;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
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;
@Slf4j
@Component
public class JwtFilter extends GenericFilterBean {
//토큰 헤더에 입력시 설정한 key값
public static final String AUTHORIZATION_HEADER = "Authorization";
private TokenProvider tokenProvider;
public JwtFilter(TokenProvider tokenProvider){
this.tokenProvider = tokenProvider;
}
//토큰의 인증정보를 securityContext에 저장하는 역할 수행
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException{
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
//resolveToken을 통해 request에서 jwt 토큰을 획득
String jwt = resolveToken(httpServletRequest);
String requestURI = httpServletRequest.getRequestURI();
//validateToken 을 통해 토큰의 유효성 검사, 정상이면 토큰을 Security Context에 저장
if(StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)){
Authentication authentication = tokenProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("Security Context에 '{}' 인증정보를 저장했습니다. uri:{}",authentication.getName(), requestURI);
}else{
//log.debug("유효한 JWT 토큰이 없습니다.",requestURI);
}
filterChain.doFilter(servletRequest, servletResponse);
}
//request Header 에서 토큰정보를 읽어옴.
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.java 생성
package com.leesh.springbootjwttutorial.jwt;
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;
import org.springframework.stereotype.Component;
@Component
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private TokenProvider tokenProvider;
public JwtSecurityConfig(TokenProvider tokenProvider){
this.tokenProvider = tokenProvider;
}
//security로직에 JwtFilter를 등록함
@Override
public void configure(HttpSecurity http){
JwtFilter customerFilter = new JwtFilter(tokenProvider);
http.addFilterBefore(customerFilter, UsernamePasswordAuthenticationFilter.class);
}
}
JwtAuthenticationEntryPoint.java 생성
package com.leesh.springbootjwttutorial.jwt;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
//유효한 토큰 없이 접근시 401 UNAUTHORIZED 에러 리턴.
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException)
throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
JwtAccessDeniedHandler.java 생성
package com.leesh.springbootjwttutorial.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;
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
//권한 불충분시 403 FORBIDDEN 접근이 거부되었습니다. 리턴
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException)
throws IOException, ServletException{
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
}
3. 위 5개의 클래스들을 SecurityConfig 에 추가하기
SecurityConfig.java 코드추가
package com.leesh.springbootjwttutorial.config;
import com.leesh.springbootjwttutorial.jwt.JwtAccessDeniedHandler;
import com.leesh.springbootjwttutorial.jwt.JwtAuthenticationEntryPoint;
import com.leesh.springbootjwttutorial.jwt.JwtSecurityConfig;
import com.leesh.springbootjwttutorial.jwt.TokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@RequiredArgsConstructor
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final TokenProvider tokenProvider;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
//패스워드 암호화시 사용
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
//h2-console, favicon, swagger-ui 시큐리티에 필터에 걸리지않도록 설정
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(
"/h2-console/**", //h2 console
"/favicon.ico",
);
}
//http 요청에대한 권한 제한 설정
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
.and()
.headers()
.frameOptions()
.sameOrigin()
.and()
.sessionManagement()
//세션 사용하지 않으므로 STATELESS 로 지정
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/hello/**").permitAll() //hello 테스트 > 전체허용
.antMatchers("/api/auth/**").permitAll() //로그인 > 전체허용
.antMatchers("/api/join/**").permitAll() //회원가입 > 전체허용
.anyRequest().authenticated()
.and()
.apply(new JwtSecurityConfig(tokenProvider));
}
}
4. 서버실행 > 정상 구동 확인
'SpringBootProject > SringBoot_JWT_RestApi' 카테고리의 다른 글
Chaptor.06 - 상품,주문 entity, repository, dto, servcie, controller (API) 추가 (0) | 2022.04.03 |
---|---|
Chaptor.05 - 회원가입 API 생성 , 권한검증 테스트 (0) | 2022.04.03 |
Chaptor.04 - DTO, Repository, 로그인API 생성 (0) | 2022.04.03 |
Chaptor.02 - JPA Entity생성 + h2 console 에서 확인 (0) | 2022.04.01 |
Chaptor.01 - 스프링부트 기초세팅 + 스프링 시큐리티 설정 + Hello API 생성 (0) | 2022.04.01 |
Comments