[스프링/Spring] Google OAuth2 로그인 구현하기 (2) - JWT와 Security 설정

dongbrown·2024년 11월 18일

Spring

목록 보기
15/23

1. JWT 이해와 구현

1.1 JWT란?

JWT(JSON Web Token)는 당사자 간에 정보를 JSON 객체로 안전하게 전송하기 위한 개방형 표준(RFC 7519)입니다.

JWT의 구조:

  • Header: 토큰 타입과 알고리즘 정보
  • Payload: 실제 전달할 정보 (Claims)
  • Signature: 토큰의 유효성 검증을 위한 서명

1.2 JwtTokenProvider 구현

@Slf4j
@Component
public class JwtTokenProvider {
    
    private String secretKey;
    private final long tokenValidityInMilliseconds;
    private Key key;

    public JwtTokenProvider(
            @Value("${jwt.token-validity-in-milliseconds}") long tokenValidityInMilliseconds) {
        this.tokenValidityInMilliseconds = tokenValidityInMilliseconds;
    }

    @PostConstruct
    protected void init() {
        try {
            // HS512 알고리즘용 키 자동 생성
            this.key = Keys.secretKeyFor(SignatureAlgorithm.HS512);
            this.secretKey = Base64.getEncoder().encodeToString(key.getEncoded());
            log.info("JWT secret key successfully generated for HS512");
        } catch (Exception e) {
            log.error("JWT secret key initialization error: ", e);
            throw new RuntimeException("Failed to initialize JWT secret key", e);
        }
    }

    // Access Token 생성
    public String createAccessToken(String id, Long memberNo, String name, MemberRole role) {
        return createToken(id, memberNo, name, role, tokenValidityInMilliseconds);
    }

    // Refresh Token 생성
    public String createRefreshToken(String id, Long memberNo, String name, MemberRole role) {
        return createToken(id, memberNo, name, role, tokenValidityInMilliseconds * 2);
    }

    // 토큰 생성 공통 메서드
    private String createToken(String id, Long memberNo, String name, 
                             MemberRole role, long validityInMilliseconds) {
        Claims claims = Jwts.claims().setSubject(id);
        claims.put("memberNo", memberNo);
        claims.put("name", name);
        claims.put("role", role.name());

        Date now = new Date();
        Date validity = new Date(now.getTime() + validityInMilliseconds);

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(validity)
                .signWith(key, SignatureAlgorithm.HS512)
                .compact();
    }

    // 토큰 유효성 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            log.warn("잘못된 JWT 서명입니다.");
        } catch (ExpiredJwtException e) {
            log.warn("만료된 JWT 토큰입니다.");
        } catch (UnsupportedJwtException e) {
            log.warn("지원되지 않는 JWT 토큰입니다.");
        } catch (IllegalArgumentException e) {
            log.warn("JWT 토큰이 잘못되었습니다.");
        }
        return false;
    }

    // 토큰에서 정보 추출 메서드들
    public String getIdFromToken(String token) {
        return parseClaims(token).getSubject();
    }

    public Long getMemberNoFromToken(String token) {
        return parseClaims(token).get("memberNo", Long.class);
    }

    public MemberRole getRoleFromToken(String token) {
        String roleStr = parseClaims(token).get("role", String.class);
        return MemberRole.valueOf(roleStr);
    }

    private Claims parseClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}

주요 구현 포인트:
1. 보안 키 자동 생성 (PostConstruct)
2. Access Token과 Refresh Token 분리
3. Claims에 사용자 정보 포함
4. 다양한 예외 상황 처리

2. Spring Security 설정

2.1 SecurityConfig 구현

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@Slf4j
public class SecurityConfig {

    private final JwtTokenProvider jwtTokenProvider;
    private final CustomUserDetailsService userDetailsService;
    private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
    private final OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;

    @Bean
    public DefaultOAuth2UserService defaultOAuth2UserService() {
        return new DefaultOAuth2UserService();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.disable())
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(
                    "/",
                    "/auth/**",
                    "/oauth2/**",
                    "/login/**",
                    "/resources/**",
                    "/css/**",
                    "/js/**",
                    "/images/**"
                ).permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .authorizationEndpoint(authorization -> authorization
                    .baseUri("/oauth2/authorization"))
                .redirectionEndpoint(redirection -> redirection
                    .baseUri("/login/oauth2/code/*"))
                .userInfoEndpoint(userInfo -> userInfo
                    .userService(defaultOAuth2UserService()))
                .successHandler(oAuth2AuthenticationSuccessHandler)
                .failureHandler(oAuth2AuthenticationFailureHandler)
            );

        return http.build();
    }
}

2.2 Security 설정 상세 설명

  1. 기본 설정:

    • CORS 비활성화: 개발 환경 편의성
    • CSRF 비활성화: JWT 사용으로 불필요
    • 세션 관리: STATELESS (JWT 사용)
  2. URL 권한 설정:

    .authorizeHttpRequests(auth -> auth
        .requestMatchers("/auth/**", "/oauth2/**", "/login/**").permitAll()
        .anyRequest().authenticated()
    )
    • 공개 URL: 인증 없이 접근 가능
    • 보호된 URL: 인증 필요
  3. OAuth2 설정:

    • 인증 시작점: /oauth2/authorization
    • 리다이렉트 URI: /login/oauth2/code/*
    • 성공/실패 핸들러 등록

0개의 댓글