인증 인가 흐름 - KimGyuBek/Threadly GitHub Wiki

인증 및 인가 흐름


개요

보안 정책 문서에 따라서 인증 인가 흐름을 구현

주요 보안 특징

  • JWT 기반 인증
  • 2FA 인증
  • 이메일 인증
  • 로그인 시도 횟수 제한
  • WebSocket 보안

목차

  1. 개요
  2. 전체 흐름
  3. JWT 기반 인증
  4. 2FA 인증
  5. 이메일 인증
  6. 로그인 시도 횟수 제한
  7. WebSocket 보안

전체 흐름

authentication_flow


JWT 기반 인증

1. 토큰 종류

AccessToken

로그인 시 발급, 매 요청에 사용

//JwtTokenProvider.java
/*AccessToken 생성*/
  private String generateAccessToken(String userId, String userType, String userStatusType) {
    Date now = new Date();
    Instant instant = now.toInstant();

    return
        Jwts.builder()
            .claim("userId", userId) // 사용자 식별 id
            .claim("userType", userType) // 사용자 타입
            .claim("userStatusType", userStatusType) // 사용자 상태(ACTIVE, INACTIVE 등)
            .setId(UUID.randomUUID().toString().substring(0, 8)) // jti 대용
            .setIssuedAt(now) // 발급 시간
            .setExpiration( // 만료 시간
                Date.from(Instant.from(instant.plus(ttlProperties.getAccessToken())))
            ) 
            .signWith(generateSigningKey(), SignatureAlgorithm.HS256) // 서명
            .compact();
  }

RefreshToken

로그인 시 발급, AccessToken 만료 후 재발급 시 사용

//  JwtTokenProvider.java
/*RefreshToken 생성*/
  private String generateRefreshToken(String userId) {
  Date now = new Date();
  Instant instant = now.toInstant();

  return
      Jwts.builder()
          .claim("userId", userId) // 사용자 식별 id
          .setId(UUID.randomUUID().toString().substring(0, 8)) // jti 대용
          .setIssuedAt(now) // 발급 시간
          .setExpiration( // 만료 시간
              Date.from(Instant.from(instant.plus(ttlProperties.getRefreshToken())))
          )
          .signWith(generateSigningKey(), SignatureAlgorithm.HS256) // 서명
          .compact();
}

Purpose Token

2FA용 토큰

//JwtTokenProvider.java
/*Purpost token 생성*/
private String generateTokenWithPurpose(String userId, String purpose, Duration duration) {
  Date now = new Date();
  Instant instant = now.toInstant();

  return
      Jwts.builder()
          .claim("userId", userId) //사용자 식별 id
          .claim("purpose", purpose) // 목적
          .setId(UUID.randomUUID().toString().substring(0, 8)) //jti 대용
          .setIssuedAt(now)//발급 시간
          .setExpiration( // 만료 시간 
              Date.from(Instant.from(instant.plus(duration)))
          )
          .signWith(generateSigningKey(), SignatureAlgorithm.HS256) // 서명
          .compact();
}

2. 토큰 인증/인가 필터 체인

1. 검증 필터 순서

  1. JwtAuthenticationFilter: JWT 파싱/검증, SecurityContext 설정
  2. UserStatusTypeValidationFilter: UserStatusType 검증
  3. VerificationFilter: 추가 검증(2FA 검증)

JwtAuthenticationFilter(JWT 인증)

  1. Authorization 헤더에서 Bearer 추출
  2. 블랙리스트 조회 후 검증
  3. 토큰 서명, 만료, 클레임 검증
  4. 검증 성공 시 SecurityContextAuthentication 설정

UserStatusTypeValidationFilter

  1. SecurityContextHolder.getContext().getAuthentication().getPrinciapl()에서 userStatusType 추출
  2. userStatusType 검증

VerificationFilter(2FA)

  1. 헤더에서 X-Verifiy-Token 추출
  2. 검증

2. 검증

//JwtTokenProvider.java
/*토큰 검증*/
  public boolean validateToken(String token) {
  try {
    Jwts.parserBuilder()
        .setSigningKey(generateSigningKey())
        .build()
        .parseClaimsJws(token);
    return true;
    
  } catch (ExpiredJwtException e) { // 만료된 토큰
    logFailure("토큰 만료됨");
    throw new TokenException(ErrorCode.TOKEN_EXPIRED);
  } catch (Exception e) { // 나머지 예외
    logFailure("토큰 검증 안 됨");
    throw new TokenException(ErrorCode.TOKEN_INVALID);
  }
}

3. SigningKey

코드

//JwtTokenProvider.java
/*SigningKey 생성*/
  private SecretKey generateSigningKey() {
  byte[] keyBytes = Decoders.BASE64.decode(secretKey);
  return Keys.hmacShaKeyFor(keyBytes);
}
  • Base64 인코딩된 SecretKey 사용
  • HMAC-SHA256 알고리즘 적용
  • 환경 변수로 SecretKey 관리

SecretKey 생성

명령어

openssl rand -base64 64

예시

+PIHGSLW+LT49C7vZa9z2lCR75fCOCRT1irGgrVEXRvVv1vu2+pe5vmDRmDaLjdVyzrRcW/PvWnvqGMuLlbIhw==

4. 블랙리스트

다음의 경우 블랙리스트에 등록

  • 로그아웃
  • 탈퇴
  • 비활성화

2FA 인증

민감한 자기 계정 정보 변경은 비밀번호 재확인을 거친다.

대상(/api/me/account/)

  • 내 계정 탈퇴
  • 비밀번호 변경
  • 내 계정 비활성화

흐름

  1. /api/auth/verify-password에서 비밀번호 재인증 요청
  2. 성공 시 X-Verify-Token 발급
  3. 2FA 대상 경로 요청 시 VerificationFilter에서 검증

이메일 인증

회원 가입이 완료되면 계정 활성화를 위해 인증 이메일을 발송하고 이를 통해 인증하는 과정을 거친다.

흐름

  1. 회원 가입 성공 시 UUID로 6쟈리 코드 생성
  2. Redis에 코드 저장 후 TTL 짧게 설정
  3. 코드와 인증 URL(/api/auth/verify-email?code=<CODE>) 메일로 전송
  4. 인증 요청 검증
  5. 사용자 상태 업데이트

인증 메일

email-verify


로그인 시도 횟수 제한

반복적인 로그인 실패를 방지하기 위해 실패 횟수를 Redis에 기록, 일정 횟수 초과 시 일정 시간 동안 로그인을 제한한다.

흐름

  1. 로그인 실패 시 loginAttemptLimiter.upsertLoginAttempt(userId)를 호출하여 로그인 시도 횟수를 증가
  2. 누적 실패 횟수가 5회 추과하면 일정 시간 동안 로그인 차단
  3. 로그인 성공 시 Redis에 저장된 누적 실패 횟수 삭제

WebSocket 보안

인증 방식

  • Query Parameter를 이용한 JWT 토큰 전달(/ws/notifications?token=<JWT_TOKEN>)
  • Handshake 전 HandShakeInterceptor.beforeHandshake()로 토큰을 검증
  • 검증 성공 시 userIdattribute에 넣어서 이후 핸들러에서 사용 가능

JwtHandshakeInterceptor

//JwtHandshakeInterceptor.java
@Component
public class JwtHandshakeInterceptor implements HandshakeInterceptor {

  @Override
  public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
      WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {

    /*토큰 검증*/
    String token = getTokenFromRequest(request); // 쿼리에서 token 추출
    if (jwtTokenProvider.validateToken(token)) { //토큰 검증

      String userId = jwtTokenProvider.getUserId(token); 
      attributes.put("userId", userId); // attribute에 userId 추가(NotificationWebSockettHandler에서 사용)
      return true; //검증 성공
    }
    return false; //검증 실패
  }
}

관련 문서

⚠️ **GitHub.com Fallback** ⚠️