인증 인가 흐름 - KimGyuBek/Threadly GitHub Wiki
보안 정책 문서에 따라서 인증 인가 흐름을 구현
- JWT 기반 인증
- 2FA 인증
- 이메일 인증
- 로그인 시도 횟수 제한
- WebSocket 보안

로그인 시 발급, 매 요청에 사용
//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();
}로그인 시 발급, 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();
}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();
}-
JwtAuthenticationFilter: JWT 파싱/검증,SecurityContext설정 -
UserStatusTypeValidationFilter:UserStatusType검증 -
VerificationFilter: 추가 검증(2FA 검증)
JwtAuthenticationFilter(JWT 인증)
-
Authorization헤더에서Bearer추출 - 블랙리스트 조회 후 검증
- 토큰 서명, 만료, 클레임 검증
- 검증 성공 시
SecurityContext에Authentication설정
UserStatusTypeValidationFilter
-
SecurityContextHolder.getContext().getAuthentication().getPrinciapl()에서userStatusType추출 -
userStatusType검증
VerificationFilter(2FA)
- 헤더에서
X-Verifiy-Token추출 - 검증
//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);
}
}//JwtTokenProvider.java
/*SigningKey 생성*/
private SecretKey generateSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
return Keys.hmacShaKeyFor(keyBytes);
}- Base64 인코딩된 SecretKey 사용
-
HMAC-SHA256알고리즘 적용 - 환경 변수로 SecretKey 관리
명령어
openssl rand -base64 64예시
+PIHGSLW+LT49C7vZa9z2lCR75fCOCRT1irGgrVEXRvVv1vu2+pe5vmDRmDaLjdVyzrRcW/PvWnvqGMuLlbIhw==
- 로그아웃
- 탈퇴
- 비활성화
민감한 자기 계정 정보 변경은 비밀번호 재확인을 거친다.
- 내 계정 탈퇴
- 비밀번호 변경
- 내 계정 비활성화
-
/api/auth/verify-password에서 비밀번호 재인증 요청 - 성공 시
X-Verify-Token발급 - 2FA 대상 경로 요청 시
VerificationFilter에서 검증
회원 가입이 완료되면 계정 활성화를 위해 인증 이메일을 발송하고 이를 통해 인증하는 과정을 거친다.
- 회원 가입 성공 시 UUID로 6쟈리 코드 생성
- Redis에 코드 저장 후 TTL 짧게 설정
- 코드와 인증 URL(
/api/auth/verify-email?code=<CODE>) 메일로 전송 - 인증 요청 검증
- 사용자 상태 업데이트

반복적인 로그인 실패를 방지하기 위해 실패 횟수를 Redis에 기록, 일정 횟수 초과 시 일정 시간 동안 로그인을 제한한다.
- 로그인 실패 시
loginAttemptLimiter.upsertLoginAttempt(userId)를 호출하여 로그인 시도 횟수를 증가 - 누적 실패 횟수가 5회 추과하면 일정 시간 동안 로그인 차단
- 로그인 성공 시 Redis에 저장된 누적 실패 횟수 삭제
- Query Parameter를 이용한 JWT 토큰 전달(
/ws/notifications?token=<JWT_TOKEN>) - Handshake 전
HandShakeInterceptor.beforeHandshake()로 토큰을 검증 - 검증 성공 시
userId를attribute에 넣어서 이후 핸들러에서 사용 가능
//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; //검증 실패
}
}