JWT 코드 및 Security 설정

LeeKyoungChang·2022년 12월 22일
0
post-thumbnail

인프런 강의 를 참고하여 정리한 글 입니다.

 

📚 1. JWT 설정 추가하기

📖 A. application.yml 설정 

spring:

  h2:
    console:
      enabled: true

  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: cpdm
    password:

  jpa:
    defer-datasource-initialization: true
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: create-drop
    properties:
      hibernate:
        format_sql: true
        show_sql: true

logging:
  level:
    me.cpdm: DEBUG

jwt:
  header: Authorization
  secret: Y2hvcHBhLWRvbnQtYml0ZS1tZS1zcHJpbmctYm9vdC1qd3QtdGVzdC1zZWNyZXQta2V5LWNob3BwYS1kb250LWJpdGUtbWUtc3ByaW5nLWJvb3Qtand0LXRlc3Qtc2VjcmV0LWtleQo=
  token-validity-in-seconds: 86400

  • HS512 알고리즘을 사용을 위해 secret key는 64B(512bit) 이상을 사용해야 한다.
  • 터미널에서 secret key를 base64로 인코딩하여 secret 항목에 채워넣는다.
  • secret key를 만들어서 넣어준다.

 

📖 B. build.gradle 설정

implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
  • Jwt 라이브러리 3가지를 추가해준다.

 

📚 2. JWT 코드 추가

📖 A. TokenProvider.java

public TokenProvider(
        @Value("${jwt.secret}") String secret,
        @Value("${jwt.token-validity-in-seconds}") long tokenValidityInSeconds) {
    this.secret = secret;
    this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000;
}
  • jwt 패키지 하위에 TokenProvider를 추가해준다.
  • TokenProvider Bean은 application.yml에서 정의한 jwt.secret, jwt.token-validity-in-seconds값을 주입

 

@Override
public void afterPropertiesSet() {
    byte[] keyBytes = Decoders.BASE64.decode(secret);
    this.key = Keys.hmacShaKeyFor(keyBytes);
}
  • afterPropertiesSet()을 Override한 이유는 Bean이 생성되고 의존성 주입까지 끝낸 후 주입 받은 secret값을 base64 decode하여 key 변수에 할당하기 위함이다.

 

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();
}
  • createToken() 은 Authentication 객체에 포함되어 있는 권한 정보들을 담은 Token을 생성하고, 
    jwt.token-validity-in-seconds값을 이용해 토큰 만료 시간을 지정한다.

 

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);
}
  • getAuthentication() 은 Token에 담겨있는 권한 정보들을 이용하여 Authentication 객체를 리턴

 

public boolean validateToken(String token) {
    try {
        // 파싱 과정에서 catch
        Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
        return true;
    } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
        logger.info("[ERR] : 잘못된 JWT SIGN");
    } catch (ExpiredJwtException e) {
        logger.info("[ERR] : 만료된 JWT TOKEN");
    } catch (UnsupportedJwtException e) {
        logger.info("[ERR] : 미지원 JWT TOKEN");
    } catch (IllegalArgumentException e) {
        logger.info("[ERR] : 잘못된 JWT TOKEN");
    }
    return false;
}
  • validateToken() : 토큰을 검증하는 역할을 수행

 

📖 B. JwtFilter.java

public JwtFilter(TokenProvider tokenProvider) {
    this.tokenProvider = tokenProvider;
}
  • 커스텀 필터를 만들기 위해 GenericFilterBean을 상속한다.
  • TokenProvider를 주입받는다.

 

private String resolveToken(HttpServletRequest request) {
    String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
    if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
        return bearerToken.substring(7);
    }
    return null;
}
  • resolveToken()은 HttpServletRequest객체의 Header에서 token을 추출한다.

 

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
        throws IOException, ServletException {
    HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
    String jwt = resolveToken(httpServletRequest);// request에서 토큰을 받아온다.
    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);
}
  • doFilter() 에서 커스텀 필터를 오버라이딩하여 작성한다.
    • jwt token의 인증 정보를 Security Context(실행 중 스레드)에 저장한다.

 

📖 C. JwtSecurityConfig.java

@Override
public void configure(HttpSecurity http) {
    JwtFilter customFilter = new JwtFilter(tokenProvider);
    // security 로직에 필터를 등록한다.
    http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
  • SecurityConfigurerAdapter를 상속받는다
  • configure 메소드를 오버라이드하여 JwtFilter를 Security 로직에 적용한다.

 

📖 D. JwtAuthenticationEntryPoint.java

@Override
public void commence(HttpServletRequest request,
                     HttpServletResponse response,
                     AuthenticationException authException) throws IOException {
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
  • AuthenticationEntryPoint를 구현한다.
  • 유효 자격 증명없이 접근하려 할 때 401 Unauthorized 에러를 리턴한다.

 

📖 E. JwtAccessDeniedHandler.java

@Override
public void handle(
    HttpServletRequest request, 
    HttpServletResponse response, 
    AccessDeniedException accessDeniedException
) throws IOException {
    response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
  • AccessDeniedHandler를 구현한다.
  • 필요한 권한이 존재하지 않은 경우 403 Forbidden 에러를 리턴한다.

 

 

📚 3. Security 설정 추가

@EnableGlobalMethodSecurity(prePostEnabled = true)
  • 메소드 단위로 @PreAuthorize 검증 annotation을 사용하기 위해 다음을 추가한다.
public SecurityConfig(
        TokenProvider tokenProvider,
        JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
        JwtAccessDeniedHandler jwtAccessDeniedHandler
) {
    this.tokenProvider = tokenProvider;
    this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
    this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
}
  • TokenProvider, JwtAuthenticationEntryPoint, JwtAccessDeniedHandler를 주입받는 코드를 추가한다.

 

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}
  • 패스워드 인코드는 BCryptPasswordEncoder() 를 사용한다.

 

.csrf().disable()
  • configure() 안의 내용을 살펴보면,
  • Token 방식을 사용하므로 csrf 설정을 비활성화 한다.

 

.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
  • 예외 처리를 위해 만들었던 jwtAuthenticationEntryPoint, jwtAccessDeniedHandler를 지정해준다.

 

.and()
.headers()
.frameOptions()
.sameOrigin()
  • h2-console 설정

 

.and()
.sessionManagement() // 세션 미사용
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
  • Session은 사용하지 않으므로 Session 설정을 Stateless로 지정

 

.and()
.authorizeRequests() // 토큰이 없는 상태에서 요청이 들어오는 경우 permitAll 해준다.
.antMatchers("/api/cpdm").permitAll()
.antMatchers("/api/authenticate").permitAll() // 로그인
.antMatchers("/api/join").permitAll() // 회원가입
.anyRequest().authenticated()
  • Token 없이 사용할 수 있도록 허가 처리 (token 없이 사용할 수 있도록 하기 위해)

 

.and()
.apply(new JwtSecurityConfig(tokenProvider));
  • JwtSecurityConfig 클래스 적용
profile
"야, (오류 만났어?) 너두 (해결) 할 수 있어"

0개의 댓글