SpringBoot Security - TokenFilter

고관운·2022년 12월 5일
1

SpringBoot Security - TokenFilter

User에 Role(역할) 추가

public class User {
    ...
    @Enumerated(EnumType.STRING)
    private UserRole role;
}

🔹 @Enumerated(EnumType.STRING) : enum 이름을 DB에 저장

public enum UserRole {
    ADMIN, USER;
}

🔹 enum으로 ADMIN, USER 2가지 Role 생성

접근 요청 막기

SecurityConfig

...

@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    private final UserService userService;

    @Value("${jwt.token.secret}")
    private String secretKey;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .httpBasic().disable()
                .csrf().disable()
                .cors().and()
                .authorizeRequests()
                .antMatchers("/api/v1/users/join", "/api/v1/users/login").permitAll() // join, login은 언제나 가능
                .antMatchers(HttpMethod.POST, "/api/v1/**").authenticated() // 접근 요청 막기
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // jwt사용하는 경우 씀
                .and()
                .addFilterBefore(new JwtTokenFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class) //UserNamePasswordAuthenticationFilter적용하기 전에 JWTTokenFilter를 적용 하라는 뜻 입니다.
                .build();
    }
}

🔹 .antMatchers("/api/v1/users/join", "/api/v1/users/login").permitAll() : 2개의 uri로 오는 요청은 모두 허용
🔹 .antMatchers(HttpMethod.POST, "/api/v1/**").authenticated() : 이후에 오는 /api/v1/** 형태의 POST 요청은 모두 막기
🔹 .addFilterBefore(new JwtTokenFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class) : UsernamePasswordAuthenticationFilter 적용하기 전에 JWTTokenFilter 적용

권한 부여하기

JwtTokenFilter

package com.hospitalreview.configuration;

import com.hospitalreview.domain.User;
import com.hospitalreview.service.UserService;
import com.hospitalreview.utils.JwtTokenUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.Filter;
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.List;

@RequiredArgsConstructor
@Slf4j
public class JwtTokenFilter extends OncePerRequestFilter {

    private final UserService userService;
    private final String secretKey;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        // 권한이 닫힌 상태에서 조건에 따라 권한을 줄지 말지 선택

        // Header의 Authorization 가져오기
        final String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
        log.info("authorization:{}", authorizationHeader);
        
        // 토큰이 없거나 Bearer로 시작하지 않는 경우
        if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        // token 분리
        String token;
        try {
            token = authorizationHeader.split(" ")[1];
        } catch (Exception e) {
            log.error("token 추출에 실패했습니다.");
            filterChain.doFilter(request, response);
            return;
        }

        // 토큰 유효 기간이 만료되었을 경우
        if(JwtTokenUtil.isExpired(token, secretKey)) {
            filterChain.doFilter(request, response);
            return;
        }

        // token의 Claim에서 userName 꺼내기
        String userName = JwtTokenUtil.getUserName(token, secretKey);
        log.info("userName:{}", userName);

        // UserDetail 가져오기
        User user = userService.getUserByUserName(userName);
        log.info("userRole:{}", user.getRole());

        // 권한 부여, Role 바인딩
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), null, List.of(new SimpleGrantedAuthority(user.getRole().name())));
        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);  // 권한 부여
        filterChain.doFilter(request, response);
    }
}

🔹 extends OncePerRequestFilter : 토큰을 가지고 요청할때마다 Check
🔹 request.getHeader(HttpHeaders.AUTHORIZATION); : Header의 AUTHORIZATION의 값을 가져옴

🔵 예외 경우

  1. if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) : 토큰이 없거나, Bearer 로 시작하지 않으면 예외 처리
    🔴 Bearer : 토큰의 하나의 형태
  2. token = authorizationHeader.split(" ")[1]; : 공백으로 분리한 후 인덱스가 1인 값만 가져오는 이유(앞에 붙은 Bearer를 제거하기 위해 ➡ 토큰만 가져오기 위해)
    🔴 토큰 추출에 예외가 발생할 수 있기 때문에 try catch문 적용
  3. if(JwtTokenUtil.isExpired(token, secretKey)) : 해당 토큰의 사용기간이 유효한지 확인(해당 메소드는 JwtTokenUtil 클래스에서 다시 설명)

🔴 filterChain.doFilter(request, response) : 다음 필터를 가르킴 (권한 부여와 상관없음 ➡ 예외 처리에 사용하면 권한을 주지않고 다음 필터로 넘긴 것)

🔵 예외가 발생하지 않은 경우

  • userName 가져오기
    JwtTokenUtil.getUserName(token, secretKey) : token의 claim에서 userName 가져오기

  • UserDetail 가져오기
    가져온 userName으로 DB에서 해당 정보 가져오기

  • Role 바인딩 및 권한 부여
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), null, List.of(new SimpleGrantedAuthority(user.getRole().name()))); : userName으로 해당 role을 주기
    SecurityContextHolder.getContext().setAuthentication(authenticationToken); : 권한 부여
    이후 다음 필터로 보내기

Token, secretKey로 정보 가져오는 메소드 생성

...
public class JwtTokenUtil {

    // secret를 사용하여 token을 parser
    private static Claims extractClaims(String token, String key) {
        return Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
    }

    public static String getUserName(String token, String key) {
        return extractClaims(token, key).get("userName", String.class);
    }

    // token 사용기간이 유효한지 Check
    public static boolean isExpired(String token, String key) {
        Date expiredDate = extractClaims(token, key).getExpiration(); // expire timestamp를 return함
        return expiredDate.before(new Date()); // 현재보다 전인지 check를 합니다.
    }

    ...
}

🔹 extractClaims : token을 secretKey로 열고 정보 꺼내기
🔹 getUserName : token에서 userName을 String 형태로 가져오기
🔹 isExpired : 리턴이 boolean형태로 만료 시간을 가져온 후, 유효하다면 true, 아니라면 false 리턴

Controller에서 userName 받기

@RestController
@RequestMapping("/api/v1/reviews")
@Slf4j
public class ReviewController {

    @PostMapping
    public String write(@RequestBody ReviewCreateRequest dto, Authentication authentication) {
        log.info("Controller user:{}", authentication.getName());
        return "리뷰 등록에 성공했습니다.";
    }
}

🔴 최종 정리

  1. /api/v1/users/login에 접근할 때, userName과 password로 토큰 받기
  2. 토큰을 Header의 Authentication에 넣고 토큰 안에 들어있는 정보를 가지고 권한 부여
  3. ReviewController에서 Authentication : 권한을 부여받은 계정에 대한 정보를 가져올 수도 있음

0개의 댓글