Configuration 구현
AuthenticationConfig
스프링 시큐리티의 기본적인 웹 보안 활성화하고, 메서드를 재정의하여 인증과 권한을 설정할 수 있는 설정파일
package com.jh.jwt.configuration;
import com.jh.jwt.exception.JwtExceptionFilter;
import com.jh.jwt.utils.TokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class AuthenticationConfig {
private final TokenProvider tokenProvider;
// 비밀번호 인코딩에 BcryptPasswordEncoder 사용
@Bean
public BCryptPasswordEncoder encodePassword() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
// JWT를 사용하기 때문에 httpBasic, csrf를 disable
.httpBasic().disable()
.csrf().disable()
.cors().and()
.authorizeRequests()
// API 경로 설정
.antMatchers("/api/v1/users/login").permitAll() // 권한없이 사용할 수 있는 api
.antMatchers("/api/v1/users/sign").permitAll() // 권한없이 사용할 수 있는 api
.antMatchers(HttpMethod.POST, "/api/v1/reviews").authenticated() // JWT인증이 필요한 api
// 세션도 사용하지 않기때문에 STATELESS로 설정
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 필터를 거치기 이전에 JwtFilter를 먼저 거치도록 설정
.addFilterBefore(new JwtFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class)
// Jwt 필터를 거치기 전에 Exception 관리를 위해 JwtException Filter를 등록
.addFilterBefore(new JwtExceptionFilter(), JwtFilter.class)
.build();
}
}
- @EnableWebSecurity는 스프링 스큐리티의 기본적인 웹 보안을 활성화 합니다.
- 추가로 antMathcer.hasRole("ADMIN")을 이용하면 ADMIN권한을 가진사람만 api를 사용할 수 있도록 설정할 수 있다.
TokenProvider
토큰을 생성하고 검증하는 역할과 토큰의 정보를 꺼내는 역할을 수행
package com.jh.jwt.utils;
import com.jh.jwt.domain.CustomUserDetails;
import com.jh.jwt.exception.NotLoggedInException;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.security.Key;
import java.util.*;
import java.util.stream.Collectors;
public class TokenProvider {
protected final String secret;
protected final String refreshSecret;
protected final long accessTokenExpired;
protected final long refreshTokenExpired;
private final RedisUtil redisUtil;
protected Key accessKey;
protected Key refreshKey;
public TokenProvider(String secret, Long expiredMs, String refreshSecret, Long refreshTokenExpired, RedisUtil redisUtil){
this.secret = secret;
this.accessTokenExpired = expiredMs;
this.refreshSecret = refreshSecret;
this.refreshTokenExpired = refreshTokenExpired;
byte[] accessKeyBytes = Base64.getDecoder().decode(secret);
this.accessKey = Keys.hmacShaKeyFor(accessKeyBytes);
byte[] refreshKeyBytes = Base64.getDecoder().decode(refreshSecret);
this.refreshKey = Keys.hmacShaKeyFor(refreshKeyBytes);
this.redisUtil = redisUtil;
}
// Access 토큰에서 사용자명 반환
public String getUsername(String token) {
return Jwts.parserBuilder().setSigningKey(accessKey).build().parseClaimsJws(token).getBody().get("username", String.class);
}
// Refresh토큰에서 사용자명 반환
public String getRefreshUsername(String token) {
return Jwts.parserBuilder().setSigningKey(refreshKey).build().parseClaimsJws(token).getBody().get("username", String.class);
}
// 토큰에서 권한정보 반환
public Collection<? extends GrantedAuthority> getAuthority(String token) {
Claims claims = Jwts.parserBuilder().setSigningKey(accessKey).build().parseClaimsJws(token).getBody();
return Arrays.stream(claims.get("auth").toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
// Access 토큰 생성
public String createAccessJwt(CustomUserDetails principal) {
Claims claims = Jwts.claims();
claims.put("username", principal.getUsername());
claims.put("auth", principal.getAuthorities());
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + accessTokenExpired))
.signWith(accessKey, SignatureAlgorithm.HS512)
.compact();
}
// Refresh 토큰 생성
public String createRefreshJwt(CustomUserDetails principal) {
Claims claims = Jwts.claims();
claims.put("username", principal.getUsername());
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + refreshTokenExpired))
.signWith(refreshKey, SignatureAlgorithm.HS512)
.compact();
}
// 헤더 Authorization에서 토큰정보 추출
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
// Access토큰 토큰 유효성 검사
public boolean validateToken(String token) {
try {
Claims claims = Jwts.parserBuilder().setSigningKey(accessKey).build().parseClaimsJws(token).getBody();
if (redisUtil.hasKeyBlackList(token)) {
throw new SignatureException("로그인을 해주세요.");
}
return true;
} catch(MalformedJwtException | SignatureException e) {
throw new MalformedJwtException("잘못된 JWT 서명입니다.");
} catch(ExpiredJwtException e) {
throw new ExpiredJwtException(e.getHeader(), e.getClaims(), "만료된 JWT 토큰입니다.");
} catch(UnsupportedJwtException e) {
throw new UnsupportedJwtException("지원하지 않는 JWT 토큰입니다.");
} catch(IllegalArgumentException e) {
throw new IllegalArgumentException("JWT 토큰이 잘못되었습니다.");
}
}
// Refresh 토큰 유효성 검사
public boolean validateRefreshToken(String token) {
try {
Claims claims = Jwts.parserBuilder().setSigningKey(refreshKey).build().parseClaimsJws(token).getBody();
return true;
} catch(MalformedJwtException | SignatureException | ExpiredJwtException | UnsupportedJwtException | IllegalArgumentException e) {
System.out.println("e = " + e);
throw new NotLoggedInException("로그인을 해주세요.");
}
}
}
JwtFilter
실직적인 인증절차를 진행하는 설정파일
package com.jh.jwt.configuration;
import com.jh.jwt.utils.TokenProvider;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
// 토큰 인증 계층
@RequiredArgsConstructor
public class JwtFilter extends OncePerRequestFilter {
protected final Logger logger = LoggerFactory.getLogger(JwtFilter.class);
private final TokenProvider tokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// authorization이 null이면 SecurityFilterChain을 거치도록한다.
final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
if(authorization == null){
filterChain.doFilter(request, response);
return;
}
String token = tokenProvider.resolveToken(request);
if (tokenProvider.validateToken(token)){
// Username, Authorities Token에서 꺼내기
String username = tokenProvider.getUsername(token);
Collection<? extends GrantedAuthority> authority = tokenProvider.getAuthority(token);
logger.info("username: {}", username);
logger.info("authority: {}", authority);
// 권한 부여
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username, null, authority);
// 해당 authenticationToken을 스프링 시큐리티 컨텍스트에 저장한다.
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
}
- OncePerRequestFilter는 Request한번에 한번만 실행되는 Filter입니다.
- doFilterInternal에서 토큰의 유효성을 검증하고, 토큰에 들어있는 username과 authority를 가져와 authentication을 생성하고 스프링 시큐리티 컨텍스트에 저장하는 역할을 합니다.
JWTConfig
TokenProvider에 의존성을 주입하고 빈을 생성하는 역할을 하는 설정파일
package com.jh.jwt.configuration;
import com.jh.jwt.utils.RedisUtil;
import com.jh.jwt.utils.TokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// TokenProvider에 의존성을 주입하고 빈을 생성하는 설정 파일
@Configuration
@RequiredArgsConstructor
public class JwtConfig {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access_token_expired}")
private Long accessTokenExpired;
@Value("${jwt.refresh_secret}")
private String refreshSecret;
@Value("${jwt.refresh_token_expired}")
private Long refreshTokenExpired;
private final RedisUtil redisUtil;
@Bean(name = "tokenProvider")
public TokenProvider tokenProvider() {
return new TokenProvider(secret, accessTokenExpired, refreshSecret, refreshTokenExpired, redisUtil);
}
}
참고
멋사 SpringSecurity 인증인가 - 04 Token유효한지 만료되었는지 검사하기
https://velog.io/@suhongkim98/Spring-Security-JWT로 인증인가구현하기
'Spring > Server' 카테고리의 다른 글
Spring JPA + JWT로 로그인 구현하기(5) - 로그아웃 구현(Redis) (1) | 2023.10.05 |
---|---|
Spring JPA + JWT로 로그인 구현하기(4) - 인증 API 구현 (1) | 2023.10.04 |
Spring JPA + JWT로 로그인 구현하기(2) - 엔티티 구현 (0) | 2023.08.05 |
Spring JPA + JWT로 로그인 구현하기(1) - 기본 설정 (0) | 2023.08.04 |
[JWT] JWT(Json Web Token)란 무엇인가? (0) | 2023.07.21 |