우선 글을 작성하기에 앞서 jwt 부분은 제가 아닌 다른 팀원이 진행했기 때문에 다소 부정확한 표현이 포함될 수 있음을 알립니다.
JWT가 무엇인지 모르시는 분들은 앞서 업로드한 아래의 글을 보고 오시면 감사하겠습니다.
https://velog.io/@snhng/세션-쿠키-토큰-JWT
Spring 환경에서는 Spring Security를 통해 JWT를 사용합니다.
gradle 파일에 다음 의존성을 추가했습니다.
dependencies { // Spring Security implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.security:spring-security-crypto' // JWT implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' }
application.yml 파일에 나중에 사용할 base64 문자열을 추가합니다.
jwt: secret: 문자열을_base64로_인코딩해서_쓰세요
이 부분은 개발 스타일에 따라 다릅니다.
저희 프로젝트에서는 권한 나타내는 enum 객체를 생성했습니다.
다른 분들의 코드를 보면 class로 작성한 경우, String으로 작성한 경우 등이 있습니다.
public enum Authority { ROLE_MEMBER, ROLE_CHILD }
가독성을 위해 일부 코드를 수정, 삭제했습니다. 코드 전문은 아래의 깃허브에서 확인하실 수 있습니다.
토큰에 필요한 정보를 생성하고, 토큰이 유효한지 확인하는 클래스를 작성합니다.
@Component public class JwtTokenProvider { private final Key key; // 앞서 application.yml 파일에 추가한 변수 public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) { byte[] keyBytes = Decoders.BASE64.decode(secretKey); this.key = Keys.hmacShaKeyFor(keyBytes); } // 유저 정보를 가지고 AccessToken, RefreshToken 을 생성하는 메서드 public TokenInfo generateToken(Authentication authentication) { // 권한 가져오기 String authorities = ... ; long now = (new Date()).getTime(); // Access Token 생성, 24시간 Date accessTokenExpiresIn = new Date(now + 86400000); String accessToken = Jwts.builder() ... .compact(); // Refresh Token 생성 String refreshToken = Jwts.builder() ... .compact(); return TokenInfo.builder() .grantType("Bearer") .accessToken(accessToken) .refreshToken(refreshToken) .build(); } // JWT 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내는 메서드 public UsernamePasswordAuthenticationToken getAuthentication(String accessToken) { // 토큰 복호화 Claims claims = parseClaims(accessToken); // 클레임에서 권한 정보 가져오기 Collection<? extends GrantedAuthority> authorities = Arrays.stream(claims.get("auth").toString().split(",")) .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); // UserDetails 객체를 만들어서 Authentication 리턴 UserDetails principal = new User(claims.getSubject(), "", authorities); return new UsernamePasswordAuthenticationToken(principal, "", authorities); } // 토큰 정보를 검증하는 메서드 public boolean validateToken(String token) { try { Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); return true; } catch (Exception e) { } return false; } private Claims parseClaims(String accessToken) { try { return Jwts.parserBuilder() ... .getBody(); } catch (ExpiredJwtException e) { return e.getClaims(); } } }
필터는 Controller 보다 앞에서 사용자의 요청을 필터링하는 역할을 합니다.
필터를 이용해 요청과 응답의 변형하고, 특정한 권한의 사용자를 차단할 수 있습니다.
더 자세한 내용은 관련 유튜브나 블로그를 확인해 주시기 바랍니다.
public class JwtAuthenticationFilter extends GenericFilterBean { private final JwtTokenProvider jwtTokenProvider; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 1. Request Header 에서 JWT 토큰 추출 String token = resolveToken((HttpServletRequest) request); // 2. validateToken 으로 토큰 유효성 검사 if (token != null && jwtTokenProvider.validateToken(token)) { // 토큰에서 Authentication 객체를 가지고 와서 SecurityContext 에 저장 SecurityContextHolder.getContext() .setAuthentication(jwtTokenProvider.getAuthentication(token)); } chain.doFilter(request, response); } // Request Header 에서 토큰 정보 추출 public String resolveToken(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) { return bearerToken.substring(7); } return null; } }
프로젝트의 설정을 담당하는 Configuration 파일을 작성해 권한별 인가를 정합니다.
@Configuration public class SecurityConfig { private final JwtTokenProvider jwtTokenProvider; private final String[] permitAllList = { ... } private final String[] memberPermitList = { ... }; private final String[] childPermitList = { ... }; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .httpBasic(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeRequests() .requestMatchers(permitAllList).permitAll() .requestMatchers(memberPermitList).hasRole(Authority.ROLE_MEMBER.toString()) .requestMatchers(childPermitList).hasRole(Authority.ROLE_CHILD.toString()) .anyRequest().authenticated() .and() .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class) .build(); } }
JWT를 이용해서 인증과 인가 구현하는 것은 처음이었습니다.
제가 직접 코딩을 하지는 않았지만 전반적 개발 과정에서 참여했기 때문에 JWT에 대해 배울 수 있는 기회였습니다.