[Spring] Spring Security + JWT 로그인 인증 구현_02

Gogh·2023년 1월 2일
0

Spring

목록 보기
22/23

🎯 목표 : JWT 개념 학습, Spring Security 어플리케이션에 JWT를 구현하는 과정 학습

📒 Spring Security + JWT

📌 JWT 생성 클래스


@Component
public class JwtTokenizer {

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

    @Getter
    @Value("${jwt.access-token-expiration-minutes}")
    private int accessTokenExpirationMinutes;

    @Getter
    @Value("${jwt.refresh-token-expiration-minutes}")
    private int refreshTokenExpirationMinutes;

    public String encodeBase64SecretKey(String secretKey) {
        return Encoders.BASE64.encode(secretKey.getBytes(StandardCharsets.UTF_8));
    }

    public String generateAccessToken(
            Map<String, Object> claims,
            String subject,
            Date expiration,
            String base64EncodedSecretKey
    ) {
        Key key = getKeyFromBase64EncodedSecretKey(base64EncodedSecretKey);

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(Calendar.getInstance().getTime())
                .setExpiration(expiration)
                .signWith(key)
                .compact();
    }

    public String generateRefreshToken(
            String subject,
            Date expiration,
            String base64EncodedSecretKey
    ) {
        Key key = getKeyFromBase64EncodedSecretKey(base64EncodedSecretKey);
        return Jwts.builder()
                .setSubject(subject)
                .setIssuedAt(Calendar.getInstance().getTime())
                .setExpiration(expiration)
                .signWith(key)
                .compact();
    }

    private Key getKeyFromBase64EncodedSecretKey(String base64EncodedSecretKey) {
        byte[] keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey);
        return Keys.hmacShaKeyFor(keyBytes);
    }

    public void verifySignature(String jws, String base64EncodedSecretKey) {
        Key key = getKeyFromBase64EncodedSecretKey(base64EncodedSecretKey);

        Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(jws);
    }

    public Jws<Claims> getClaims(String jws, String base64EncodedSecretKey) {
        Key key = getKeyFromBase64EncodedSecretKey(base64EncodedSecretKey);
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(jws);
    }

    public Date getTokenExpiration(int expirationMinutes) {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MINUTE, expirationMinutes);
        return calendar.getTime();
    }
}
  • secretKey(비밀키),accessTokenExpirationMinutes(AccessToken 만료시간),refreshTokenExpirationMinutes(RefreshToken 만료시간)는 설정 파일이나, 시스템 환경변수로 셋팅 해놓으면 된다.
  • encodeBase64SecretKey()는 Secrit key byte를 Base64 형식으로 인코딩 해준다.
  • generateAccessToken()은 JWT를 최초 발급해 주는 메소드다.
    • Claims는 인증된 사용자와 관련된 정보를 추가한다.
    • Subject는 JWT에 대한 제목을 추가한다.
    • signWith()에서는 Key 객체를 설정하는데 Signature 부분을 설정한다.
    • compace()는 JWT를 생성하고 직렬화한다.
  • generateRefreshToken() 에서는 Access Token이 만료되면 재발급 할 수 있게 해주는 Refresh Token을 생성한다.
  • getKeyFromBase64EncodedKey()에서는 JWT의 서명에 사용하는 Secret key를 생성한다.

📌 Authentication Filter 구현


@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;

    private final JwtTokenizer jwtTokenizer;

    @SneakyThrows
    @Override
    public Authentication attemptAuthentication(
            HttpServletRequest request,
            HttpServletResponse response
    ) throws AuthenticationException {
        ObjectMapper objectMapper = new ObjectMapper();
        LoginDto loginDto = objectMapper.readValue(request.getInputStream(), LoginDto.class);
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());

        return authenticationManager.authenticate(usernamePasswordAuthenticationToken);
    }

    @Override
    protected void successfulAuthentication(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain,
            Authentication authResult
    ) throws IOException, ServletException {
        Member member = (Member) authResult.getPrincipal();
        String accessToken = delegateAccessToken(member);
        String refreshToken = delegateRefreshToken(member);

        response.setHeader("Authorization", "Bearer " + accessToken);
        response.setHeader("Refresh", refreshToken);

        this.getSuccessHandler().onAuthenticationSuccess(request, response, authResult);
    }

    private String delegateAccessToken(Member member) {
        HashMap<String, Object> claims = new HashMap<>();
        claims.put("username", member.getEmail());
        claims.put("roles", member.getRoles());

        String subject = member.getEmail();
        Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes());

        String base64SecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());

        return jwtTokenizer.generateAccessToken(claims, subject, expiration, base64SecretKey);
    }

    private String delegateRefreshToken(Member member) {
        String subject = member.getEmail();
        Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getRefreshTokenExpirationMinutes());
        String base64SecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());

        return jwtTokenizer.generateRefreshToken(subject, expiration, base64SecretKey);

    }
}
  • Username/Password기반 인증을 처리하기위해 UsernamePasswordAuthenticationFilter를 확장하여 구현하였다.
  • attemptAuthentication()는 인증 시도 로직으로 오버라이딩하여 구현하였다.
  • successfulAuthentication()는 클라이언트의 인증 정보를 이용하여 인증에 성공할 경우 호출된다.
    • delegateAccessToken(Member member)에서 Access Token을 생성하여 response Header에 넣어준다.
    • delegateRefreshToken(Member member)에서 Refresh Token을 생성하여 response Header에 넣어준다.
  • 로그인 인증 요청을 하고 Token을 발급 받는 코드를 구현하였다. Config에 적용을 해보자.

📌 Config 적용

@Configuration
public class SecurityConfiguration {

    private final JwtTokenizer jwtTokenizer;

    public SecurityConfiguration(JwtTokenizer jwtTokenizer) {
        this.jwtTokenizer = jwtTokenizer;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .headers().frameOptions().sameOrigin()
            .and()
            .csrf().disable()
            .cors(withDefaults())
            .formLogin().disable()
            .httpBasic().disable()
          // filter 적용
            .apply(new CustomFilterConfigurer())
            .and()
            .authorizeHttpRequests(authorize -> authorize
                    .anyRequest().permitAll()
            );
        return http.build();
    }

    //...
    //...


    public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity> {
        @Override
        public void configure(HttpSecurity builder) throws Exception {
            AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);

            JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer);
            jwtAuthenticationFilter.setFilterProcessesUrl("/login");

            builder.addFilter(jwtAuthenticationFilter);
        }
    }
}
  • 로그인 인증까지의 코드를 적용해주기 위해 SecurityConfiguration에 필터를 적용시켜 주었다.
  • AbstractHttpConfigurer를 확장하여 CustomFilterConfigurer를 내부 클래스로 확장 하였다.
  • AbstractHttpConfigurer에서 getSharedObject(AuthenticationManager.class)를 통해 AuthenticationManager 객체를 얻을수 있으며 얻은 객체와 JwtTokenizerJwtAuthenticationFilter에 생성자로 주입하고 HttpSecurity 필터에 추가한다.
  • 추가한 필터를 적용하기 위해서는 Bean으로 등록한 FilterChain에서 .apply(new CustomFilterConfigurer())AbstractHttpConfigurer확장 클래스를 추가 해주면 된다.
  • 이후 로그인 인증 성공과 실패에 따른 추가 처리에 대한 코드를 추가하려면, AuthenticationSuccessHandler, AuthenticationFailureHandler를 구현하여 Config에 아래와 같이 추가해 주면 된다, 상세 코드는 생략하고 권한 검증에 대한 코드를 알아보자.
public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity> {
  @Override
  public void configure(HttpSecurity builder) throws Exception {
    AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);

    JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer);
    jwtAuthenticationFilter.setFilterProcessesUrl("/login");
    // Handler 추가
    // AuthenticationSuccessHandler를 구현한 MemberAuthenticationSuccessHandler 클래스
    //AuthenticationFailureHandler를 구현한 MemberAuthenticationFailureHandler 클래스
    jwtAuthenticationFilter.setAuthenticationSuccessHandler(new MemberAuthenticationSuccessHandler());
    jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler());

    builder.addFilter(jwtAuthenticationFilter);
  }
}
profile
컴퓨터가 할일은 컴퓨터가

0개의 댓글