-
JWT 실습 프로젝트 - 2 (Security 기본 설정, JWT 및 Security 설정)SPRING/스프링 시큐리티 -JWT 2021. 9. 17. 00:46
SilverNine님의 JWT 강의를 듣고 정리하였습니다.
Security 기본 설정
@EnableWebSecurity // 기본적인 Web 보안 활성화 @EnableGlobalMethodSecurity(prePostEnabled = true) // @EnableGlobalMethodSecurity(prePostEnabled = true) 어노테이션은 메소드 단위로 // @PreAuthorize 검증 어노테이션을 사용하기 위해 추가합니다. public class SecurityConfig extends WebSecurityConfigurerAdapter { private final TokenProvider tokenProvider; private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; private final JwtAccessDeniedHandler jwtAccessDeniedHandler; public SecurityConfig( TokenProvider tokenProvider, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint, JwtAccessDeniedHandler jwtAccessDeniedHandler ) { this.tokenProvider = tokenProvider; this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint; this.jwtAccessDeniedHandler = jwtAccessDeniedHandler; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override public void configure(WebSecurity web) { // Spring Security 로직을 수행하지 않고 아래 요청에 접근 web.ignoring() .antMatchers( "/h2-console/**" ,"/favicon.ico" ,"/error" ); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity // token을 사용하는 방식이기 때문에 csrf를 disable합니다. .csrf().disable() .exceptionHandling() .authenticationEntryPoint(jwtAuthenticationEntryPoint) .accessDeniedHandler(jwtAccessDeniedHandler) // enable h2-console .and() .headers() .frameOptions() .sameOrigin() // 세션을 사용하지 않기 때문에 STATELESS로 설정 .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // HttpServletRequest를 사용하는 요청들에 대한 접근제한을 설정 .antMatchers("/api/hello").permitAll() // 접근 허용 .antMatchers("/api/authenticate").permitAll() .antMatchers("/api/signup").permitAll() .anyRequest().authenticated() .and() .apply(new JwtSecurityConfig(tokenProvider)); } }
TokenProvider
@Component public class TokenProvider implements InitializingBean { private final Logger 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() { byte[] keyBytes = Decoders.BASE64.decode(secret); this.key = Keys.hmacShaKeyFor(keyBytes); } // Authentication 객체에 포함되어 있는 권한 정보들을 담은 토큰을 생성하고 jwt.token-validity-in-seconds 값을 이용해 토큰의 만료 시간을 지정 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); return Jwts.builder() .setSubject(authentication.getName()) .claim(AUTHORITIES_KEY, authorities) .signWith(key, SignatureAlgorithm.HS512) .setExpiration(validity) .compact(); } // getAuthentication 메소드는 토큰에 담겨있는 권한 정보들을 이용해 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); } //validateToken 메소드는 토큰을 검증하는 역할을 수행합니다. public boolean validateToken(String token) { try { Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); return true; } 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; } }
TokenProvider는 jwt.secret, jwt-token-validity-in-seconds 값을 주입받는다.
생성자에서 setting 한 이후 afterPropertiesSet()을 오버라이드하는데 의존성 주입 이후 주입받은 secret 값을 base64로 decode하여 key변수에 할당하기 위함이다.
createToken 메소드 : 인증객체에 포함되어 있는 정보들을 담은 토큰을 생성하고, jwt-token-validity-in-seconds 값을 이용해 토큰의 만료시간을 지정함.
getAuthentication 메소드 : 토큰에 담겨있는 권한 정보를 이용하여 인증객체를 리턴한다.
validaeToken 메소드 : 토큰을 검증하는 역할을 수행한다.
JWT FIlter
public class JwtFilter extends GenericFilterBean { private static final Logger logger = LoggerFactory.getLogger(JwtFilter.class); public static final String AUTHORIZATION_HEADER = "Authorization"; private TokenProvider tokenProvider; public JwtFilter(TokenProvider tokenProvider) { this.tokenProvider = tokenProvider; } // doFilter는 jwt 토큰의 인증 정보를 현재 실행중인 스레드(security Context에 저장합니다.) @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(); if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { Authentication authentication = tokenProvider.getAuthentication(jwt); SecurityContextHolder.getContext().setAuthentication(authentication); logger.debug("Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}", authentication.getName(), requestURI); } else { logger.debug("유효한 JWT 토큰이 없습니다, uri: {}", requestURI); } filterChain.doFilter(servletRequest, servletResponse); } // resolveToken 메소드는 HttpServletRequest 객체의 Header에서 token을 꺼내는 역할을 수행합니다. private String resolveToken(HttpServletRequest request) { String bearerToken = request.getHeader(AUTHORIZATION_HEADER); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } }
JwtFilter Bean은 TokenProvider를 주입받음.
resolveToken 메소드 : HttpServletRequest 객체의 Header에서 token을 꺼내는 역할을 수행함.
doFilter 메소드 : jwt 토큰의 인증 정보를 현재 실행중인 스레드(Security Context)에 저장함.
JWtSecurityConfig
// JwtSecurityConfig.java 는 SecurityConfigurerAdapter를 extends하며 // configure메소드를 오버라이드하여 위에서 만든 JwtFilter를 Security 로직에 적용하는 역할을 수행합니다 public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { private TokenProvider tokenProvider; public JwtSecurityConfig(TokenProvider tokenProvider) { this.tokenProvider = tokenProvider; } @Override public void configure(HttpSecurity http) { JwtFilter customFilter = new JwtFilter(tokenProvider); http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); } }
JwtFilter를 Security 로직에 적용하는 역할을 수행함.
JwtAuthenticationEntryPoint
// 유효한 자격증명을 제공하지 않고 접근하려 할때 401 UNAUTHORIZED 에러를 리턴하기 위해 // AuthenticationEntryPoint를 구현한 JwtAuthenticationEntryPoint 클래스를 작성합니다. @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { // 유효한 자격증명을 제공하지 않고 접근하려 할때 401 response.sendError(HttpServletResponse.SC_UNAUTHORIZED); } }
- 유효하지 않은 자격증명으로 접근하려고 할때 UNAUTHORIZED 에러 리턴
JwtAccessDeniedHandler
// 필요한 권한이 존재하지 않은 경우 403 FORBIDDEN 에러를 리턴하기 위해 // AccessDeniedHandler를 구현한 JwtAccessDeniedHandler 클래스를 작성합니다. @Component public class JwtAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { //필요한 권한이 없이 접근하려 할때 403 response.sendError(HttpServletResponse.SC_FORBIDDEN); } }
- 필요한 권한이 존재하지 않은 경우 403 Forbidden 에러를 리턴하기 위해 AccessDeniedHandler를 작성
'SPRING > 스프링 시큐리티 -JWT' 카테고리의 다른 글
JWT 실습 프로젝트 - 4 (회원가입, 권한검증) (0) 2021.09.18 JWT 실습 프로젝트 - 3 (로그인) (0) 2021.09.17 JWT 실습 프로젝트 - 1 (프로젝트 생성 및 설정) (0) 2021.09.11 JWT란? (0) 2021.09.11