youngseo's TECH blog

[Spring] Spring으로 JWT 구현하기2 - Spring Security 라이브러리 본문

BackEnd/JAVA\SPRING

[Spring] Spring으로 JWT 구현하기2 - Spring Security 라이브러리

jeonyoungseo 2023. 2. 11. 13:54

Spring Security는 Java 애플리케이션에서 인증/권한 부여 서비스를 제공하는 프레임워크이다.
이를 활용해서 소셜 로그인(카카오, 애플)을 구현해보았다.
이 글에서는 우선 Spring Security로 JWT를 구현하는 방법에 대한 글을 써보려고 한다.

일단 Spring Security는 그리 쉽지 않다.. 구현도 쉽지 않고, 찾아보니 이 쪽 영역을 제대로 공부하기 위해서 사람들이 시간을 잡고 공부하는 것 같았다.
그래서 공부방향을 코드에 대한 대략적인 JWT 작동 방식을 이해하고, 내가 나중에 확실히 또 써먹을 수 있도록 공부해보는 것으로 잡았다.

아래를 봐도 아주 복잡한 filter의 향연을 볼 수 있다 ..

Spring security에서는 애플리케이션에 대한 모든 요청을 필터로 감싼뒤 처리한다. 체인을 따라 다음 필터 -> 다음 필터 ... 이렇게 이동하는 형식이다. 구글링 키워드 : SpringFilterChain

인증 과정?

Spring Security에서 JWT 인증 과정은 다음과 같다. 

doFilterInternal

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

        // 1. Request Header 에서 토큰을 꺼냄
        String jwt = resolveToken(request);

        log.debug(String.valueOf(request));

        // 2. validateToken 으로 토큰 유효성 검사
        // 정상 토큰이면 해당 토큰으로 Authentication 을 가져와서 SecurityContext 에 저장
        if (StringUtils.hasText(jwt) && tokenService.verifyToken(jwt)) {

            System.out.println("Normal jwt");

            Authentication authentication = tokenService.getAuthentication(jwt);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }

1. 클라이언트에서 요청이 오면, 해당 요청의 header에서 "Authorization" 헤더를 가져온다.
2.
"Authorization" 헤더에서 JWT 토큰을 가져온다.
3.
JWT 토큰 유효성을 검증한다. (verify)
4.
 유효하다면? -> Authentication 객체 생성
(Authentication 객체란 Spring Security에서 한 유저의 인증 정보를 가지고 있는 객체이다. 이 정도로 이해하자..)
5. 해당 Authentication객체를
SecurityContextHolder에 담는다. (이때, 권한 관리를 위해 세션을 저장한다.)(SecurityContextHolder.getContext().setAuthentication(authentication) 메서드)
6. 마지막으로, doFilter 메서드로 체인을 따라 다음에 존재하는 필터로 이동한다.
(doFilter는 해당 필터에서 처리하고 다시 다음 필터로 넘겨주는 메소드이다.)

 

아래는 전체 코드입니다.

세팅

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{

    private final TokenService tokenService;

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/v2/api-docs/**", "/configuration/**", "/swagger-resources/**",
                "/swagger-ui/**", "/oauth/**","/webjars/**","/h2-console/**", "/members/**","/script/**","/oauth/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(tokenService);
        http.httpBasic().disable()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .antMatchers(
                        "/api/v1/auth/**",
                        "/oauth/**",
                        "/script/**",
                        "/forum/**",
                        "/interview/**",
                        "/**",
                        "/members/**",
                        "/api/v1/version",
                        "/auth/**",
                        "/token/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }

}

주로 antMatchers 에 포함되는 url은 아래와 같이 사용된다. 해당 권한이 있어야지만 해당 api에 접근이 가능하다.
내가 구현하는 곳은 딱히 Role을 정해두지 않았고, 로그인이 안되면 다른 곳에 접근이 애초에 불가능하여 일단 전부 열어뒀다.

.antMatchers("/api/v1/user/**")
    .hasAnyRole("USER", "MANAGER", "ADMIN")
.antMatchers("/api/v1/manager/**")
    .hasAnyRole("MANAGER", "ADMIN")

JwtAuthenticationFilter 코드

@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    public static final String AUTHORIZATION_HEADER = "Authorization";
    public static final String BEARER_PREFIX = "Bearer ";

    private final TokenService tokenService;

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

        // 1. Request Header 에서 토큰을 꺼냄
        String jwt = resolveToken(request);

        log.debug(String.valueOf(request));

        // 2. validateToken 으로 토큰 유효성 검사
        // 정상 토큰이면 해당 토큰으로 Authentication 을 가져와서 SecurityContext 에 저장
        if (StringUtils.hasText(jwt) && tokenService.verifyToken(jwt)) {

            System.out.println("Normal jwt");

            Authentication authentication = tokenService.getAuthentication(jwt);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
            return bearerToken.substring(7);
        }
        log.debug("토큰이 존재하지 않습니다.");
        return null;
    }

}

TokenService 코드

@Service
@RequiredArgsConstructor
public class TokenService {
    private String secretKey = "~~~~~~~";
    private static final String AUTHORITIES_KEY = "role";

    private final UserDetailsService userDetailsService;

    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    public Token generateToken(String uid, String role) {
        long tokenPeriod = 1000L * 60L * 90L;
        long refreshPeriod = 1000L * 60L * 60L * 24L * 30L * 3L;

        Claims claims = Jwts.claims().setSubject(uid);
        claims.put("role", role);

        Date now = new Date();
        return new Token(
                Jwts.builder()
                        .setClaims(claims)
                        .setIssuedAt(now)
                        .setExpiration(new Date(now.getTime() + tokenPeriod))
                        .signWith(SignatureAlgorithm.HS256, secretKey)
                        .compact(),
                Jwts.builder()
                        .setExpiration(new Date(now.getTime() + refreshPeriod))
                        .signWith(SignatureAlgorithm.HS256, secretKey)
                        .compact());
    }

    public String generateAccessToken(String uid, String role) {
        long tokenPeriod = 1000L * 60L * 10L;

        Claims claims = Jwts.claims().setSubject(uid);
        claims.put("role", role);

        Date now = new Date();

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(new Date(now.getTime() + tokenPeriod))
                .signWith(SignatureAlgorithm.HS256, secretKey)
                .compact();
    }

    public long verifyRefreshToken(String token){
        try{
            Jws<Claims> claims = Jwts.parser()
                    .setSigningKey(secretKey)
                    .parseClaimsJws(token);
            return claims.getBody()
                    .getExpiration().getTime();
        } catch (Exception e) {
            return 0;
        }
    }

    public boolean verifyToken(String token) {
        try {
            Jws<Claims> claims = Jwts.parser()
                    .setSigningKey(secretKey)
                    .parseClaimsJws(token);
            return claims.getBody()
                    .getExpiration()
                    .after(new Date());
        } catch (Exception e) {
            return false;
        }
    }

    public Authentication getAuthentication(String accessToken) {
        Claims claims = parseClaims(accessToken);

        if (claims.get(AUTHORITIES_KEY) == null) {
            throw new RuntimeException("권한 정보가 없는 토큰입니다.");
        }
        final String socialId = claims.getSubject();
        final CurrentUserDetails currentUserDetails = (CurrentUserDetails) userDetailsService.loadUserByUsername(socialId);

        return new UsernamePasswordAuthenticationToken(currentUserDetails, "", currentUserDetails.getAuthorities());
    }

    private Claims parseClaims(String accessToken) {
        try {
            return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(accessToken).getBody();
        } catch (ExpiredJwtException e) {
            return e.getClaims();
        }
    }
}

 

 

 

참조

https://iseunghan.tistory.com/category/%F0%9F%92%90%20Spring/Spring%20Security

 

'💐 Spring/Spring Security' 카테고리의 글 목록

꾸준하게 열심히..

iseunghan.tistory.com

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-jwt

 

[무료] Spring Boot JWT Tutorial - 인프런 | 강의

Spring Boot, Spring Security, JWT를 이용한 튜토리얼을 통해 인증과 인가에 대한 기초 지식을 쉽고 빠르게 학습할 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com