[캡스톤] 기능 개발 - JWT 인증, 인가

이신행·2024년 6월 18일

capstone

목록 보기
6/15
post-thumbnail

우선 글을 작성하기에 앞서 jwt 부분은 제가 아닌 다른 팀원이 진행했기 때문에 다소 부정확한 표현이 포함될 수 있음을 알립니다.

JWT가 무엇인지 모르시는 분들은 앞서 업로드한 아래의 글을 보고 오시면 감사하겠습니다.
https://velog.io/@snhng/세션-쿠키-토큰-JWT


JWT를 이용하게 된 배경

  1. 모바일 앱의 경우 세션을 사용하기 어렵습니다.
    • 물론 사용할 수는 있지만, 직접 세션을 관리해야 합니다.
    • 사용하려면 세션 ID를 관리하는 DB를 사용해야 합니다.
  2. OAuth와 함께 요즘 가장 많이 사용하는 인증 방식입니다.

Spring 환경에서의 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 생성

이 부분은 개발 스타일에 따라 다릅니다.
저희 프로젝트에서는 권한 나타내는 enum 객체를 생성했습니다.
다른 분들의 코드를 보면 class로 작성한 경우, String으로 작성한 경우 등이 있습니다.

public enum Authority {
    ROLE_MEMBER, ROLE_CHILD
}

개발

가독성을 위해 일부 코드를 수정, 삭제했습니다. 코드 전문은 아래의 깃허브에서 확인하실 수 있습니다.

TokenProvider (토큰 생성 클래스) 작성

토큰에 필요한 정보를 생성하고, 토큰이 유효한지 확인하는 클래스를 작성합니다.

@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 파일을 작성해 권한별 인가를 정합니다.

@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에 대해 배울 수 있는 기회였습니다.

코드 깃허브

https://github.com/LeeShinHaeng/safeGuard

profile
언제나 Response 하는 Ability가 있는 서버를 만드는, Responsibility 있는 개발자가 되자

0개의 댓글