
sequenceDiagram
participant Client as React Client
participant API as Spring Boot API
participant DB as Database
%% 로그인 프로세스
rect rgb(191, 223, 255)
Note over Client,DB: 로그인 프로세스
Client->>+API: 1. POST /api/auth/login (email, password)
API->>DB: 2. 사용자 검증
DB-->>API: 3. 사용자 정보 반환
API->>API: 4. JWT 토큰 생성 (Access + Refresh)
API-->>-Client: 5. JWT 토큰 반환
Client->>Client: 6. 토큰 저장 (localStorage/Cookie)
end
%% API 요청 프로세스
rect rgb(200, 255, 200)
Note over Client,DB: 인증된 API 요청
Client->>+API: 7. API 요청 + Authorization 헤더
API->>API: 8. JWT 토큰 검증
API->>DB: 9. 데이터 요청
DB-->>API: 10. 데이터 반환
API-->>-Client: 11. 응답 데이터 반환
end
%% 토큰 갱신 프로세스
rect rgb(255, 228, 191)
Note over Client,DB: 토큰 갱신 프로세스
Client->>+API: 12. API 요청 (만료된 Access Token)
API-->>Client: 13. 401 Unauthorized
Client->>API: 14. POST /api/auth/refresh (Refresh Token)
API->>API: 15. Refresh Token 검증
API-->>-Client: 16. 새로운 Access Token 발급
Client->>Client: 17. 새 Access Token 저장
end
JWT는 당사자 간에 정보를 JSON 객체로 안전하게 전송하기 위한 컴팩트하고 독립적인 방식을 정의하는 개방형 표준(RFC 7519)입니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
로그인 요청 (프론트엔드 → 백엔드)
// React (프론트엔드)
const login = async (email, password) => {
try {
const response = await axios.post('/api/auth/login', {
email,
password
});
const { accessToken, refreshToken } = response.data;
// 토큰 저장
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
} catch (error) {
console.error('Login failed:', error);
}
};
토큰 생성 (백엔드)
// Spring Boot (백엔드)
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
// 사용자 인증
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getEmail(),
loginRequest.getPassword()
)
);
// JWT 토큰 생성
String accessToken = jwtUtils.generateAccessToken(authentication);
String refreshToken = jwtUtils.generateRefreshToken(authentication);
return ResponseEntity.ok(new JwtResponse(accessToken, refreshToken));
}
API 요청 시 토큰 포함 (프론트엔드)
// Axios Interceptor 설정
axios.interceptors.request.use(
(config) => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
);
토큰 검증 (백엔드)
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateToken(jwt)) {
String username = jwtUtils.getUserNameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails,
null,
userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
}
filterChain.doFilter(request, response);
}
}
토큰 만료 확인 (프론트엔드)
axios.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post('/api/auth/refresh', { refreshToken });
const { accessToken } = response.data;
localStorage.setItem('accessToken', accessToken);
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return axios(originalRequest);
} catch (error) {
// 리프레시 토큰도 만료된 경우
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);
리프레시 토큰 처리 (백엔드)
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest request) {
String requestRefreshToken = request.getRefreshToken();
return refreshTokenService.findByToken(requestRefreshToken)
.map(refreshToken -> {
if (jwtUtils.validateRefreshToken(refreshToken.getToken())) {
String username = refreshToken.getUser().getUsername();
String newAccessToken = jwtUtils.generateAccessToken(username);
return ResponseEntity.ok(new TokenRefreshResponse(newAccessToken));
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
})
.orElse(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build());
}
토큰 관련
API 엔드포인트
/api/auth/login/api/auth/refresh/api/auth/logout응답 형식
// 로그인 성공 응답
{
"accessToken": "eyJhbGc...",
"refreshToken": "eyJhbGc...",
"tokenType": "Bearer",
"expiresIn": 3600 // 선택사항
}
// 에러 응답
{
"error": "invalid_credentials",
"message": "Invalid email or password",
"status": 401
}
헤더 형식
Authorization: Bearer {token}
Content-Type: application/json
정상 로그인/로그아웃
토큰 만료
에러 처리
이 문서는 JWT 기반 인증 구현을 위한 기본 가이드라인입니다. 실제 구현 시에는 프로젝트의 요구사항과 보안 정책에 따라 적절히 수정하여 사용해주세요.