[Start Spring Boot] Spring Security JWT

·2024년 4월 19일
0

Start Spring Boot!

목록 보기
45/53
post-thumbnail

기존 인증과 토큰 인증

JsessionID

  • 톰캣에서 세션을 유지하기 위해 발급하는 키
  • 쿠키를 통해서 세션을 유지함
  • 쿠키에 저장하기 때문에 악용가능성이 있다
  • 무작위 데이터이기 때문에 사용자 정보를 가지지 않는다
  • 톰캣 컨테이너를 2개 이상 사용하면 세션 클러스터가 필요함

인증기반 토큰의 장점

  • 인증과 인가에 토큰을 사용
  • 실제 자격증명은 최초 로그인시에만 보냄
  • 즉, 실제 자격증명을 가지고 계속 인증할 필요가 없음
  • 토큰은 해킹시 해당 토큰만 무효화 하면 됨
  • 짧은 인증기간을 가지는 토큰을 생성 가능
  • 권한, 역할 같은 정보 포함 가능
  • 재사용이 가능함 / 여러 서비스에서 사용 가능
  • Stateless

JWT(JSON Web Token)

JWT란?

  • 데이터를 JSON 형식으로 유지
  • 가장 많이 사용함
  • 내부에 유저의 데이터를 저장하고 유지
  • Stateless가 가능하며 마이크로 서비스에서 유리
  • 최초로 생성한 것과 계속 동일해야함
  • 최초 생성 후 캐시, DB에 저장 후 동일한지만 확인
  • Signature을 이용하면 저장할 필요도 없음

JWT 구조

  • JWT 예시(JWT.IO)
  • 다음과 같이 3부분으로 구성됨
  • 각 부분은 (.)을 통해서 구분
  • Header / Payload / Signature
  • Encode / Decode를 사용함
  • 비밀번호와 같은 정보를 저장하면 안됨

Header(필수)

  • 해시 알고리즘과 토큰의 형식등을 저장
  • 토큰의 정보를 저장하는 곳

Payload(필수)

  • 유저에 대한 모든 정보를 저장 가능
  • 이름, 이메일, 역할, 만료시간 등

Signature(Optional)

  • 클라이언트를 신뢰한다면 필요없음
  • 헤더와 페이로드의 값을 가지고 secret키를 이용해서 생성한다.
  • 무작위 해시 문자열을 반환함
  • 헤더와 페이로드에 변경이 있다면 달라진다.
  • 즉, 해당 방식을 통해 토큰을 확인할 수 있다.

JWT 사용 세팅하기

dependencies

  • build.gradle
dependencies {
		implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
		runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
		runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}

Security config 수정하기

  • 해당 부분을 삭제한다.
    http.sessionManagement((sessionManagement) ->
            sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    );
    
  • JsessionID를 사용하지 않고 무상태를 유지하기 위해서 다음을 추가한다.
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS);

        return http.build();
    }
    
  • CORS() 수정하기, 다음을 추가해서 헤더에 Authorization를 포함한다.
 config.setExposedHeaders(List.of("Authorization"));

JWT 필터 구현하기

토큰에 사용할 상수 선언하기

  • SecurityConstants.java
package com.chan.ssb.constants;

public interface SecurityConstants {

    public static final String JWT_KEY = "jxgEQeXHuPq8VdbyYFNkANdudQ53YUn4";
    public static final String JWT_HEADER = "Authorization";

}
  • 비밀키로 사용할 문자열과 헤더 문자열을 정의 하였다.
  • 실제 제품에서는 다음과 같은 방식으로 비밀키를 사용하지 않는 것을 권장함!

토큰 생성 필터

  • JWTTokenGeneratorFilter.java
package com.chan.ssb.filter;

import com.chan.ssb.constants.SecurityConstants;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.crypto.SecretKey;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

public class JWTTokenGeneratorFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (null != authentication) {
            SecretKey key = Keys.hmacShaKeyFor(SecurityConstants.JWT_KEY.getBytes(StandardCharsets.UTF_8));
            String jwt = Jwts.builder().setIssuer("SSB").setSubject("JWT Token")
                    .claim("username", authentication.getName())
                    .claim("authorities", populateAuthorities(authentication.getAuthorities()))
                    .setIssuedAt(new Date())
                    .setExpiration(new Date((new Date()).getTime() + 30000000))
                    .signWith(key).compact();
            response.setHeader(SecurityConstants.JWT_HEADER, jwt);
        }

        filterChain.doFilter(request, response);
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        return !request.getServletPath().equals("/user/login");
    }

    private String populateAuthorities(Collection<? extends GrantedAuthority> collection) {
        Set<String> authoritiesSet = new HashSet<>();
        for (GrantedAuthority authority : collection) {
            authoritiesSet.add(authority.getAuthority());
        }
        return String.join(",", authoritiesSet);
    }
}
  • 먼저 인증정보를 가져온다.
  • 다음의 코드들은 JWT를 만드는 것이다.
  • claim을 통해서 원하는 내용를 추가할 수 있다.
  • 권한의 경우 별도의 함수를 통해 문자열로 저장함
  • shouldNotFilter()의 경우 true일 경우 필터를 실행하지 않음 따라서, /user/login의 요청의 경우만 토큰을 생성한다.

필터 추가하기

  • SpringSecurityConfiguration.java
.addFilterAfter(new JWTTokenGeneratorFilter(), BasicAuthenticationFilter.class)

토큰 검증 필터 만들기

  • JWTTokenValidatorFilter.java
package com.chan.ssb.filter;

import com.chan.ssb.constants.SecurityConstants;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.crypto.SecretKey;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class JWTTokenValidatorFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String jwt = request.getHeader(SecurityConstants.JWT_HEADER);
        if (null != jwt) {
            jwt = jwt.replace("Bearer ", "");
            try {
                SecretKey key = Keys.hmacShaKeyFor(
                        SecurityConstants.JWT_KEY.getBytes(StandardCharsets.UTF_8));

                Claims claims = Jwts.parserBuilder()
                        .setSigningKey(key)
                        .build()
                        .parseClaimsJws(jwt)
                        .getBody();
                String username = String.valueOf(claims.get("username"));
                String authorities = (String) claims.get("authorities");
                Authentication auth = new UsernamePasswordAuthenticationToken(username, null,
                        AuthorityUtils.commaSeparatedStringToAuthorityList(authorities));
                SecurityContextHolder.getContext().setAuthentication(auth);
            } catch (Exception e) {
                throw new BadCredentialsException("Invalid Token received!");
            }
        }
        filterChain.doFilter(request, response);
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        return request.getServletPath().equals("/user/login");
    }
}
  • 헤더의 내용을 불러와서 처리한다.
  • Bearer의 형식으로 오는 토큰도 처리가능하게 치환한다.
  • setSigningKey(): 다음에서 검증을 진행한다.
  • shouldNotFilter()을 통해서 /user/login이 아닌 모든 요청에 대해서 필터를 적용한다.

필터 추가하기

  • SpringSecurityConfiguration.java
.addFilterBefore(new JWTTokenValidatorFilter(), BasicAuthenticationFilter.class);

Swagger 설정하기

  • SwaggerConfig.java
package com.chan.ssb.config;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {
    @Bean
    public OpenAPI openAPI() {
        return new OpenAPI()
                .components(new Components().addSecuritySchemes("bearer-jwt", new SecurityScheme()
                        .name("bearer-jwt")
                        .type(SecurityScheme.Type.HTTP)
                        .scheme("bearer")
                        .bearerFormat("JWT")
                        .in(SecurityScheme.In.HEADER)
                        .description("JWT Authorization header using the Bearer scheme.")))
                .info(apiInfo())
                .addSecurityItem(new SecurityRequirement().addList("bearer-jwt"));
    }

    private Info apiInfo() {
        return new Info()
                .title("API Test")
                .description("Let's practice Swagger UI")
                .version("1.0.0");
    }
}
  • 다음과 같은 설정으로 인증을 추가할 수 있다.

사용하기

Token 발급 확인하기

  • Postman을 통해서 요청을 보낸다.
  • 헤더에서 토큰을 확인할 수 있다.

Token으로 인증 받기

  • 다음과 같이 요청에 토큰을 추가한다.
  • Swagger도 다음과 같은 버튼이 추가된다.

profile
백엔드 개발자가 꿈인 컴공과

0개의 댓글

관련 채용 정보