Jwt 인증 인가.

Sol's·2023년 1월 7일
0

스프링부트

목록 보기
3/5

앞서 Security Chain에 대한 이론과 실습과정을 진행했다.
Spring Security적용을 하려면 Security Filter의 구조를 알아야 한다

이제 Jwt를 이용해아 인증 인가를 구현해보자.

우선 Dependency를 추가해주자

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    testImplementation 'org.springframework.security:spring-security-test'
    implementation 'io.jsonwebtoken:jjwt:0.9.1'
}

토큰을 발급받는 메서드가있는 클래스이다.
로그인시 CreateToken를 통해 토큰을 발급받는다.

public class JwtUtil {
    public static String getUserName(String token, String secretKey) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
                .getBody().get("userName", String.class);
    }
    public static boolean isExpired(String token, String secretKey) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
                .getBody().getExpiration().before(new Date());
    }
    public static String CreateToken(String userName, String key, Long expireTimeMs) {
        Claims claims = Jwts.claims(); //일종의 map
        claims.put("userName", userName);

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + expireTimeMs))
                .signWith(SignatureAlgorithm.HS256,key)
                .compact()
                ;
    }
}

JwtFilter를 만들어 준다.
OncePerRequestFilter = 매요청시 한번 작동하라는 뜻이다.
이 클래스에서 권한을 확인하고 접근을 허용할지 말지 결정할 것이다.

@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {

    private final UserService userService;
    private final String secretKey;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //SecurityConfig에서 Post요청을 전부 막아놓았는데 열여줄지 말지 결정하는 곳이다.

        //헤더에서 authentication을 꺼내서 문을 열어줄지 말지 결정 할 것이다.
        final String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
        log.info("authorization : {}", authorization);

        //token 안보냈다면 null이라면 권한부여하지 않고 리턴한다.
        if (authorization == null || !authorization.startsWith("Bearer ")) {
            log.error("authorization을 잘못보냈습니다.");
            filterChain.doFilter(request,response);
            return;
        }

        //토큰 꺼내기
        String token = authorization.split(" ")[1];

        //Token Expired 되었는지 여부
        if(JwtUtil.isExpired(token,secretKey)){
            log.error("token이 만료되었습니다.");
            filterChain.doFilter(request,response);
            return;
        }

        // UserName을 Token에서 꺼내기
        String userName = JwtUtil.getUserName(token,secretKey);
        log.info(userName);

        //Role 꺼내기
        User user = userService.getUserByUserName(userName);
        UserRole role = user.getRole();

        //문 열어주기
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken
                (userName, null, List.of(new SimpleGrantedAuthority(role.toString())));

        //Detail을 넣는다.
        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request,response);
    }
}
  1. 헤더에서 Token(authorization)이 형식에 맞게 잘 들어왔는지 확인한다.
  2. 잘 들어왔다면 Token이 만료되었는지 확인
  3. Token(authorization)에 저장된 userName을 꺼내 권한(Role)을 확인한다.
  4. 에러가 발생하지 않았다면 Token(authorization)을 발행한다.
  5. SecurityContextHolder에 토큰을 저장한다.

SecurityFilterChain에서 jwtFilter를 UsernamePasswordAuthenticationFilter보다 먼저 작동하게 설정

@RequiredArgsConstructor
@EnableWebSecurity(debug = true) // request가 올 떄마다 어떤 filter를 사용하고 있는지 출력을 해준다.
public class SecurityConfig {

    private final UserService userService;

    @Value("${jwt.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/posts/**").authenticated()
                .antMatchers(HttpMethod.POST, "/api/v1/users/{id}/role/change").access("hasRole('ROLE_ADMIN')")
                .antMatchers(HttpMethod.PUT, "/api/**").authenticated()
                .antMatchers(HttpMethod.DELETE, "/api/**").authenticated()
                .antMatchers(HttpMethod.GET, "/api/v1/posts/my").authenticated()
                .and()
                .exceptionHandling()// 예외처리기능 작동
                .authenticationEntryPoint(new CustomAuthenticationEntryPointHandler()) // 인증처리 실패시 처리
                .accessDeniedHandler(new CustomAccessDeniedHandler())// 인가처리 실패시 처리
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // STATELESS = jwt사용하는 경우 씀 : 매번 토큰을 사용하는 개념?
                .and()
                .addFilterBefore(new JwtFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class) //UserNamePasswordAuthenticationFilter적용하기 전에 JWTTokenFilter를 적용 하라는 뜻 입니다.
                .addFilterBefore(new ExceptionHandlerFilter(), JwtFilter.class)
                .build();
    }
}

위 코드에서 .addFilterBefore(new JwtFilter(userService, secretKey), UsernamePasswordAuthenticationFilter.class)가 UserNamePasswordAuthenticationFilter적용하기 전에 직접만든 JwtFilter를 적용하는 설정이다.

이제 securityFilterChain에서 인증인가 설정에 맞게 설정이 되었다.

profile
배우고, 생각하고, 행동해라

0개의 댓글