feat: 로그인 프로세스 제거 + 사용자 역할 토글 버튼

- JWT 인증 및 LoginPage 제거, SecurityConfig permitAll 전환
- @PreAuthorize 어노테이션 전체 제거 (@EnableMethodSecurity 비활성화)
- ADMIN/MANAGER/USER 역할 토글 버튼 (헤더) + localStorage 연동
- X-User-Id 헤더 기반 사용자 식별 (ApiKeyController, ApiKeyRequestController)
- RoleGuard 컴포넌트로 관리자 전용 페이지 접근 제어
- WebViewController 루트 리다이렉트 수정 (이중 context-path 방지)

closes #35
This commit is contained in:
HYOJIN 2026-04-13 09:27:17 +09:00
부모 bfaf5e9b97
커밋 97e5a24343
18개의 변경된 파일150개의 추가작업 그리고 412개의 파일을 삭제

파일 보기

@ -1,10 +1,7 @@
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import ThemeProvider from './store/ThemeContext'; import ThemeProvider from './store/ThemeContext';
import AuthProvider from './store/AuthContext'; import AuthProvider from './store/AuthContext';
import AuthLayout from './layouts/AuthLayout';
import MainLayout from './layouts/MainLayout'; import MainLayout from './layouts/MainLayout';
import ProtectedRoute from './components/ProtectedRoute';
import LoginPage from './pages/LoginPage';
import DashboardPage from './pages/DashboardPage'; import DashboardPage from './pages/DashboardPage';
import RequestLogsPage from './pages/monitoring/RequestLogsPage'; import RequestLogsPage from './pages/monitoring/RequestLogsPage';
import RequestLogDetailPage from './pages/monitoring/RequestLogDetailPage'; import RequestLogDetailPage from './pages/monitoring/RequestLogDetailPage';
@ -22,6 +19,7 @@ import ApiStatsPage from './pages/statistics/ApiStatsPage';
import TenantStatsPage from './pages/statistics/TenantStatsPage'; import TenantStatsPage from './pages/statistics/TenantStatsPage';
import UsageTrendPage from './pages/statistics/UsageTrendPage'; import UsageTrendPage from './pages/statistics/UsageTrendPage';
import NotFoundPage from './pages/NotFoundPage'; import NotFoundPage from './pages/NotFoundPage';
import RoleGuard from './components/RoleGuard';
const BASE_PATH = '/snp-connection'; const BASE_PATH = '/snp-connection';
@ -31,11 +29,6 @@ const App = () => {
<ThemeProvider> <ThemeProvider>
<AuthProvider> <AuthProvider>
<Routes> <Routes>
<Route element={<AuthLayout />}>
<Route path="/login" element={<LoginPage />} />
</Route>
<Route element={<ProtectedRoute />}>
<Route element={<MainLayout />}> <Route element={<MainLayout />}>
<Route path="/" element={<Navigate to="/dashboard" replace />} /> <Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/dashboard" element={<DashboardPage />} /> <Route path="/dashboard" element={<DashboardPage />} />
@ -50,13 +43,12 @@ const App = () => {
<Route path="/statistics/usage-trend" element={<UsageTrendPage />} /> <Route path="/statistics/usage-trend" element={<UsageTrendPage />} />
<Route path="/apikeys/my-keys" element={<MyKeysPage />} /> <Route path="/apikeys/my-keys" element={<MyKeysPage />} />
<Route path="/apikeys/request" element={<KeyRequestPage />} /> <Route path="/apikeys/request" element={<KeyRequestPage />} />
<Route path="/apikeys/admin" element={<KeyAdminPage />} /> <Route path="/apikeys/admin" element={<RoleGuard allowedRoles={['ADMIN', 'MANAGER']}><KeyAdminPage /></RoleGuard>} />
<Route path="/admin/services" element={<ServicesPage />} /> <Route path="/admin/services" element={<RoleGuard allowedRoles={['ADMIN']}><ServicesPage /></RoleGuard>} />
<Route path="/admin/users" element={<UsersPage />} /> <Route path="/admin/users" element={<RoleGuard allowedRoles={['ADMIN']}><UsersPage /></RoleGuard>} />
<Route path="/admin/tenants" element={<TenantsPage />} /> <Route path="/admin/tenants" element={<RoleGuard allowedRoles={['ADMIN']}><TenantsPage /></RoleGuard>} />
<Route path="*" element={<NotFoundPage />} /> <Route path="*" element={<NotFoundPage />} />
</Route> </Route>
</Route>
</Routes> </Routes>
</AuthProvider> </AuthProvider>
</ThemeProvider> </ThemeProvider>

파일 보기

@ -1,22 +1,5 @@
import { Navigate, Outlet } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';
const ProtectedRoute = () => { const ProtectedRoute = () => <Outlet />;
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-100 dark:bg-gray-900">
<div className="h-8 w-8 animate-spin rounded-full border-4 border-blue-500 border-t-transparent dark:border-blue-400 dark:border-t-transparent" />
</div>
);
}
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <Outlet />;
};
export default ProtectedRoute; export default ProtectedRoute;

파일 보기

@ -0,0 +1,41 @@
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';
interface RoleGuardProps {
allowedRoles: string[];
children: React.ReactNode;
}
const RoleGuard = ({ allowedRoles, children }: RoleGuardProps) => {
const { user } = useAuth();
const navigate = useNavigate();
if (!user || !allowedRoles.includes(user.role)) {
return (
<div className="max-w-7xl mx-auto flex flex-col items-center justify-center py-32">
<div className="bg-white dark:bg-gray-800 rounded-2xl border border-gray-200 dark:border-gray-700 shadow-lg p-10 text-center max-w-md">
<div className="w-16 h-16 mx-auto mb-5 rounded-full bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700/30 flex items-center justify-center">
<svg className="w-8 h-8 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
</div>
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100 mb-2"> </h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
<span className="font-semibold text-gray-700 dark:text-gray-300">{allowedRoles.join(', ')}</span> .
<br /> : <span className="font-semibold text-gray-700 dark:text-gray-300">{user?.role || '-'}</span>
</p>
<button
onClick={() => navigate('/dashboard')}
className="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2.5 rounded-lg text-sm font-medium transition-colors"
>
</button>
</div>
</div>
);
}
return <>{children}</>;
};
export default RoleGuard;

파일 보기

@ -1,11 +0,0 @@
import { Outlet } from 'react-router-dom';
const AuthLayout = () => {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900">
<Outlet />
</div>
);
};
export default AuthLayout;

파일 보기

@ -46,8 +46,10 @@ const navGroups: NavGroup[] = [
}, },
]; ];
const ROLES = ['ADMIN', 'MANAGER', 'USER'] as const;
const MainLayout = () => { const MainLayout = () => {
const { user, logout } = useAuth(); const { user, setRole } = useAuth();
const { theme, toggleTheme } = useTheme(); const { theme, toggleTheme } = useTheme();
const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({ const [openGroups, setOpenGroups] = useState<Record<string, boolean>>({
Monitoring: true, Monitoring: true,
@ -60,10 +62,6 @@ const MainLayout = () => {
setOpenGroups((prev) => ({ ...prev, [label]: !prev[label] })); setOpenGroups((prev) => ({ ...prev, [label]: !prev[label] }));
}; };
const handleLogout = async () => {
await logout();
};
const isAdminOrManager = user?.role === 'ADMIN' || user?.role === 'MANAGER'; const isAdminOrManager = user?.role === 'ADMIN' || user?.role === 'MANAGER';
return ( return (
@ -170,16 +168,22 @@ const MainLayout = () => {
</svg> </svg>
)} )}
</button> </button>
<span className="text-sm text-gray-700 dark:text-gray-300">{user?.userName}</span> <span className="text-sm text-gray-500 dark:text-gray-400">Role:</span>
<span className="rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800"> <div className="flex rounded-lg overflow-hidden border border-gray-300 dark:border-gray-600">
{user?.role} {ROLES.map((role) => (
</span>
<button <button
onClick={handleLogout} key={role}
className="rounded-lg px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-100 transition-colors" onClick={() => setRole(role)}
className={`px-3 py-1.5 text-xs font-medium transition-colors ${
user?.role === role
? 'bg-blue-600 text-white'
: 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600'
}`}
> >
Logout {role}
</button> </button>
))}
</div>
</div> </div>
</header> </header>

파일 보기

@ -1,93 +0,0 @@
import { useState } from 'react';
import type { FormEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';
const LoginPage = () => {
const navigate = useNavigate();
const { login } = useAuth();
const [loginId, setLoginId] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError('');
if (!loginId.trim() || !password.trim()) {
setError('아이디와 비밀번호를 입력해주세요.');
return;
}
setIsSubmitting(true);
try {
await login(loginId, password);
navigate('/dashboard', { replace: true });
} catch (err) {
if (err instanceof Error) {
setError(err.message);
} else {
setError('로그인에 실패했습니다.');
}
} finally {
setIsSubmitting(false);
}
};
return (
<div className="w-full max-w-md">
<div className="rounded-xl bg-white dark:bg-gray-800 px-8 py-10 shadow-lg">
<h1 className="mb-8 text-center text-2xl font-bold text-gray-900 dark:text-gray-100">
SNP Connection Monitoring
</h1>
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label htmlFor="loginId" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
</label>
<input
id="loginId"
type="text"
value={loginId}
onChange={(e) => setLoginId(e.target.value)}
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-4 py-2.5 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
placeholder="아이디를 입력하세요"
autoComplete="username"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-4 py-2.5 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
placeholder="비밀번호를 입력하세요"
autoComplete="current-password"
/>
</div>
{error && (
<p className="text-sm text-red-600">{error}</p>
)}
<button
type="submit"
disabled={isSubmitting}
className="w-full rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-colors"
>
{isSubmitting ? '로그인 중...' : '로그인'}
</button>
</form>
</div>
</div>
);
};
export default LoginPage;

파일 보기

@ -2,93 +2,24 @@ import type { ApiResponse } from '../types/api';
const BASE_URL = '/snp-connection/api'; const BASE_URL = '/snp-connection/api';
let isRefreshing = false; let currentUserId: number = 1;
let refreshPromise: Promise<boolean> | null = null;
const getAccessToken = (): string | null => { export const setApiClientUserId = (userId: number) => {
return localStorage.getItem('snp_access_token'); currentUserId = userId;
};
const getRefreshToken = (): string | null => {
return localStorage.getItem('snp_refresh_token');
};
const clearStorage = () => {
localStorage.removeItem('snp_access_token');
localStorage.removeItem('snp_refresh_token');
localStorage.removeItem('snp_user');
};
const attemptRefresh = async (): Promise<boolean> => {
const refreshToken = getRefreshToken();
if (!refreshToken) return false;
try {
const response = await fetch(`${BASE_URL}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
if (!response.ok) return false;
const data = await response.json() as ApiResponse<{ accessToken: string }>;
if (data.success && data.data?.accessToken) {
localStorage.setItem('snp_access_token', data.data.accessToken);
return true;
}
return false;
} catch {
return false;
}
};
const handleTokenRefresh = async (): Promise<boolean> => {
if (isRefreshing && refreshPromise) {
return refreshPromise;
}
isRefreshing = true;
refreshPromise = attemptRefresh().finally(() => {
isRefreshing = false;
refreshPromise = null;
});
return refreshPromise;
}; };
const request = async <T>(url: string, options: RequestInit = {}): Promise<ApiResponse<T>> => { const request = async <T>(url: string, options: RequestInit = {}): Promise<ApiResponse<T>> => {
const token = getAccessToken();
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-User-Id': String(currentUserId),
...options.headers as Record<string, string>, ...options.headers as Record<string, string>,
}; };
if (token) { const response = await fetch(`${BASE_URL}${url}`, {
headers['Authorization'] = `Bearer ${token}`;
}
let response = await fetch(`${BASE_URL}${url}`, {
...options, ...options,
headers, headers,
}); });
if (response.status === 401) {
const refreshed = await handleTokenRefresh();
if (refreshed) {
const newToken = getAccessToken();
headers['Authorization'] = `Bearer ${newToken}`;
response = await fetch(`${BASE_URL}${url}`, {
...options,
headers,
});
} else {
clearStorage();
window.location.href = '/snp-connection/login';
return { success: false, message: 'Authentication failed' };
}
}
const data = await response.json() as ApiResponse<T>; const data = await response.json() as ApiResponse<T>;
return data; return data;
}; };

파일 보기

@ -1,67 +1,2 @@
import { post } from './apiClient'; // Auth service removed - login/logout no longer needed.
import type { LoginRequest, LoginResponse, User } from '../types/auth'; // Role is managed via AuthContext's setRole function.
const TOKEN_KEY = 'snp_access_token';
const REFRESH_TOKEN_KEY = 'snp_refresh_token';
const USER_KEY = 'snp_user';
export const login = async (req: LoginRequest): Promise<LoginResponse> => {
const response = await post<LoginResponse>('/auth/login', req);
if (!response.success || !response.data) {
throw new Error(response.message || '로그인에 실패했습니다.');
}
const data = response.data;
localStorage.setItem(TOKEN_KEY, data.accessToken);
localStorage.setItem(REFRESH_TOKEN_KEY, data.refreshToken);
localStorage.setItem(USER_KEY, JSON.stringify({
loginId: data.loginId,
userName: data.userName,
role: data.role,
}));
return data;
};
export const logout = async (): Promise<void> => {
try {
await post('/auth/logout');
} finally {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY);
localStorage.removeItem(USER_KEY);
}
};
export const refreshToken = async (): Promise<void> => {
const storedRefreshToken = localStorage.getItem(REFRESH_TOKEN_KEY);
if (!storedRefreshToken) {
throw new Error('No refresh token available');
}
const response = await post<{ accessToken: string }>('/auth/refresh', {
refreshToken: storedRefreshToken,
});
if (!response.success || !response.data) {
throw new Error('Token refresh failed');
}
localStorage.setItem(TOKEN_KEY, response.data.accessToken);
};
export const getStoredUser = (): User | null => {
const userStr = localStorage.getItem(USER_KEY);
if (!userStr) return null;
try {
return JSON.parse(userStr) as User;
} catch {
return null;
}
};
export const getAccessToken = (): string | null => {
return localStorage.getItem(TOKEN_KEY);
};

파일 보기

@ -1,17 +1,13 @@
import { createContext, useState, useEffect, useCallback } from 'react'; import { createContext, useState } from 'react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import type { User } from '../types/auth'; import type { User } from '../types/auth';
import * as authService from '../services/authService'; import { setApiClientUserId } from '../services/apiClient';
interface AuthState { interface AuthContextValue {
user: User | null; user: User | null;
isAuthenticated: boolean; isAuthenticated: boolean;
isLoading: boolean; isLoading: boolean;
} setRole: (role: 'ADMIN' | 'MANAGER' | 'USER') => void;
interface AuthContextValue extends AuthState {
login: (loginId: string, password: string) => Promise<void>;
logout: () => Promise<void>;
} }
export const AuthContext = createContext<AuthContextValue | null>(null); export const AuthContext = createContext<AuthContextValue | null>(null);
@ -20,56 +16,32 @@ interface AuthProviderProps {
children: ReactNode; children: ReactNode;
} }
const ROLE_USERS: Record<string, User> = {
ADMIN: { userId: 1, loginId: 'admin', userName: '관리자', role: 'ADMIN' },
MANAGER: { userId: 7, loginId: 'manager', userName: '매니저', role: 'MANAGER' },
USER: { userId: 2, loginId: 'user', userName: '사용자', role: 'USER' },
};
const getInitialUser = (): User => {
const savedRole = localStorage.getItem('snp-role') as 'ADMIN' | 'MANAGER' | 'USER' | null;
const u = ROLE_USERS[savedRole || 'ADMIN'];
setApiClientUserId(u.userId);
return u;
};
const AuthProvider = ({ children }: AuthProviderProps) => { const AuthProvider = ({ children }: AuthProviderProps) => {
const [state, setState] = useState<AuthState>({ const [user, setUser] = useState<User>(getInitialUser);
user: null,
isAuthenticated: false,
isLoading: true,
});
useEffect(() => { const setRole = (role: 'ADMIN' | 'MANAGER' | 'USER') => {
const storedUser = authService.getStoredUser(); localStorage.setItem('snp-role', role);
const token = authService.getAccessToken(); const u = ROLE_USERS[role];
setUser(u);
if (storedUser && token) { setApiClientUserId(u.userId);
setState({ window.location.reload();
user: storedUser, };
isAuthenticated: true,
isLoading: false,
});
} else {
setState({
user: null,
isAuthenticated: false,
isLoading: false,
});
}
}, []);
const login = useCallback(async (loginId: string, password: string) => {
const response = await authService.login({ loginId, password });
setState({
user: {
loginId: response.loginId,
userName: response.userName,
role: response.role,
},
isAuthenticated: true,
isLoading: false,
});
}, []);
const logout = useCallback(async () => {
await authService.logout();
setState({
user: null,
isAuthenticated: false,
isLoading: false,
});
}, []);
return ( return (
<AuthContext.Provider value={{ ...state, login, logout }}> <AuthContext.Provider value={{ user, isAuthenticated: true, isLoading: false, setRole }}>
{children} {children}
</AuthContext.Provider> </AuthContext.Provider>
); );

파일 보기

@ -1,18 +1,5 @@
export interface LoginRequest {
loginId: string;
password: string;
}
export interface LoginResponse {
accessToken: string;
refreshToken: string;
loginId: string;
userName: string;
role: string;
expiresIn: number;
}
export interface User { export interface User {
userId: number;
loginId: string; loginId: string;
userName: string; userName: string;
role: string; role: string;

파일 보기

@ -9,10 +9,10 @@ import com.gcsc.connection.apikey.dto.UpdatePermissionsRequest;
import com.gcsc.connection.apikey.service.ApiKeyPermissionService; import com.gcsc.connection.apikey.service.ApiKeyPermissionService;
import com.gcsc.connection.apikey.service.ApiKeyService; import com.gcsc.connection.apikey.service.ApiKeyService;
import com.gcsc.connection.common.dto.ApiResponse; import com.gcsc.connection.common.dto.ApiResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@ -34,6 +34,7 @@ public class ApiKeyController {
private final ApiKeyService apiKeyService; private final ApiKeyService apiKeyService;
private final ApiKeyPermissionService apiKeyPermissionService; private final ApiKeyPermissionService apiKeyPermissionService;
private final HttpServletRequest request;
/** /**
* API Key 목록 조회 * API Key 목록 조회
@ -49,7 +50,6 @@ public class ApiKeyController {
* 전체 API Key 목록 조회 (관리자용) * 전체 API Key 목록 조회 (관리자용)
*/ */
@GetMapping("/all") @GetMapping("/all")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ApiResponse<List<ApiKeyResponse>>> getAllKeys() { public ResponseEntity<ApiResponse<List<ApiKeyResponse>>> getAllKeys() {
List<ApiKeyResponse> keys = apiKeyService.getAllKeys(); List<ApiKeyResponse> keys = apiKeyService.getAllKeys();
return ResponseEntity.ok(ApiResponse.ok(keys)); return ResponseEntity.ok(ApiResponse.ok(keys));
@ -59,7 +59,6 @@ public class ApiKeyController {
* API Key 상세 조회 (복호화된 포함, 관리자 전용) * API Key 상세 조회 (복호화된 포함, 관리자 전용)
*/ */
@GetMapping("/{id}") @GetMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ApiResponse<ApiKeyDetailResponse>> getKeyDetail(@PathVariable Long id) { public ResponseEntity<ApiResponse<ApiKeyDetailResponse>> getKeyDetail(@PathVariable Long id) {
ApiKeyDetailResponse detail = apiKeyService.getKeyDetail(id); ApiKeyDetailResponse detail = apiKeyService.getKeyDetail(id);
return ResponseEntity.ok(ApiResponse.ok(detail)); return ResponseEntity.ok(ApiResponse.ok(detail));
@ -69,7 +68,6 @@ public class ApiKeyController {
* API Key 생성 (관리자 전용) * API Key 생성 (관리자 전용)
*/ */
@PostMapping @PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ApiResponse<ApiKeyCreateResponse>> createKey( public ResponseEntity<ApiResponse<ApiKeyCreateResponse>> createKey(
@RequestBody @Valid CreateApiKeyRequest request) { @RequestBody @Valid CreateApiKeyRequest request) {
Long userId = getCurrentUserId(); Long userId = getCurrentUserId();
@ -99,7 +97,6 @@ public class ApiKeyController {
* API Key 권한 수정 (관리자/매니저 전용) * API Key 권한 수정 (관리자/매니저 전용)
*/ */
@PutMapping("/{id}/permissions") @PutMapping("/{id}/permissions")
@PreAuthorize("hasAnyRole('ADMIN','MANAGER')")
public ResponseEntity<ApiResponse<List<PermissionResponse>>> updatePermissions( public ResponseEntity<ApiResponse<List<PermissionResponse>>> updatePermissions(
@PathVariable Long id, @PathVariable Long id,
@RequestBody @Valid UpdatePermissionsRequest request) { @RequestBody @Valid UpdatePermissionsRequest request) {
@ -109,6 +106,15 @@ public class ApiKeyController {
} }
private Long getCurrentUserId() { private Long getCurrentUserId() {
String userIdHeader = request.getHeader("X-User-Id");
if (userIdHeader != null && !userIdHeader.isBlank()) {
return Long.parseLong(userIdHeader);
}
// fallback: SecurityContext
try {
return Long.parseLong(SecurityContextHolder.getContext().getAuthentication().getName()); return Long.parseLong(SecurityContextHolder.getContext().getAuthentication().getName());
} catch (Exception e) {
return 1L; // default admin
}
} }
} }

파일 보기

@ -6,10 +6,10 @@ import com.gcsc.connection.apikey.dto.ApiKeyRequestResponse;
import com.gcsc.connection.apikey.dto.ApiKeyRequestReviewDto; import com.gcsc.connection.apikey.dto.ApiKeyRequestReviewDto;
import com.gcsc.connection.apikey.service.ApiKeyRequestService; import com.gcsc.connection.apikey.service.ApiKeyRequestService;
import com.gcsc.connection.common.dto.ApiResponse; import com.gcsc.connection.common.dto.ApiResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@ -31,6 +31,7 @@ import java.util.List;
public class ApiKeyRequestController { public class ApiKeyRequestController {
private final ApiKeyRequestService apiKeyRequestService; private final ApiKeyRequestService apiKeyRequestService;
private final HttpServletRequest request;
/** /**
* API Key 신청 생성 * API Key 신청 생성
@ -57,7 +58,6 @@ public class ApiKeyRequestController {
* 신청 목록 조회 (관리자/매니저용) * 신청 목록 조회 (관리자/매니저용)
*/ */
@GetMapping @GetMapping
@PreAuthorize("hasAnyRole('ADMIN','MANAGER')")
public ResponseEntity<ApiResponse<List<ApiKeyRequestResponse>>> getRequests( public ResponseEntity<ApiResponse<List<ApiKeyRequestResponse>>> getRequests(
@RequestParam(required = false) String status) { @RequestParam(required = false) String status) {
List<ApiKeyRequestResponse> responses; List<ApiKeyRequestResponse> responses;
@ -73,7 +73,6 @@ public class ApiKeyRequestController {
* 신청 심사 (승인/거절) * 신청 심사 (승인/거절)
*/ */
@PutMapping("/{id}/review") @PutMapping("/{id}/review")
@PreAuthorize("hasAnyRole('ADMIN','MANAGER')")
public ResponseEntity<ApiResponse<ApiKeyCreateResponse>> reviewRequest( public ResponseEntity<ApiResponse<ApiKeyCreateResponse>> reviewRequest(
@PathVariable Long id, @PathVariable Long id,
@RequestBody @Valid ApiKeyRequestReviewDto request) { @RequestBody @Valid ApiKeyRequestReviewDto request) {
@ -86,6 +85,14 @@ public class ApiKeyRequestController {
} }
private Long getCurrentUserId() { private Long getCurrentUserId() {
String userIdHeader = request.getHeader("X-User-Id");
if (userIdHeader != null && !userIdHeader.isBlank()) {
return Long.parseLong(userIdHeader);
}
try {
return Long.parseLong(SecurityContextHolder.getContext().getAuthentication().getName()); return Long.parseLong(SecurityContextHolder.getContext().getAuthentication().getName());
} catch (Exception e) {
return 1L;
}
} }
} }

파일 보기

@ -1,7 +1,5 @@
package com.gcsc.connection.config; package com.gcsc.connection.config;
import com.gcsc.connection.auth.jwt.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@ -13,16 +11,12 @@ import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@EnableMethodSecurity // @EnableMethodSecurity -- disabled (no login)
@RequiredArgsConstructor
public class SecurityConfig { public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();
@ -42,9 +36,8 @@ public class SecurityConfig {
.requestMatchers("/actuator/**").permitAll() .requestMatchers("/actuator/**").permitAll()
.requestMatchers("/", "/*.html", "/assets/**", "/favicon*", "/site.webmanifest").permitAll() .requestMatchers("/", "/*.html", "/assets/**", "/favicon*", "/site.webmanifest").permitAll()
.requestMatchers("/gateway/**").permitAll() .requestMatchers("/gateway/**").permitAll()
.requestMatchers("/api/**").authenticated() .requestMatchers("/api/**").permitAll()
.anyRequest().permitAll()) .anyRequest().permitAll());
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build(); return http.build();
} }

파일 보기

@ -12,8 +12,14 @@ import org.springframework.web.bind.annotation.GetMapping;
@Controller @Controller
public class WebViewController { public class WebViewController {
@GetMapping({"/", "/login", "/dashboard", "/dashboard/**", @GetMapping("/")
"/monitoring/**", "/apikeys", "/apikeys/**", public String root() {
return "redirect:/dashboard";
}
@GetMapping({"/dashboard", "/dashboard/**",
"/monitoring/**", "/statistics/**",
"/apikeys", "/apikeys/**",
"/admin/**"}) "/admin/**"})
public String forward() { public String forward() {
return "forward:/index.html"; return "forward:/index.html";

파일 보기

@ -7,7 +7,6 @@ import com.gcsc.connection.monitoring.dto.ServiceStatusDetailResponse;
import com.gcsc.connection.monitoring.service.HeartbeatService; import com.gcsc.connection.monitoring.service.HeartbeatService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@ -68,7 +67,6 @@ public class HeartbeatController {
* 수동 헬스체크 실행 * 수동 헬스체크 실행
*/ */
@PostMapping("/{serviceId}/check") @PostMapping("/{serviceId}/check")
@PreAuthorize("hasAnyRole('ADMIN','MANAGER')")
public ResponseEntity<ApiResponse<HeartbeatStatusResponse>> checkService( public ResponseEntity<ApiResponse<HeartbeatStatusResponse>> checkService(
@PathVariable Long serviceId) { @PathVariable Long serviceId) {
HeartbeatStatusResponse result = heartbeatService.checkService(serviceId); HeartbeatStatusResponse result = heartbeatService.checkService(serviceId);

파일 보기

@ -10,7 +10,6 @@ import com.gcsc.connection.service.service.ServiceManagementService;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@ -44,7 +43,6 @@ public class ServiceController {
* 서비스 생성 * 서비스 생성
*/ */
@PostMapping @PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ApiResponse<ServiceResponse>> createService( public ResponseEntity<ApiResponse<ServiceResponse>> createService(
@RequestBody @Valid CreateServiceRequest request) { @RequestBody @Valid CreateServiceRequest request) {
ServiceResponse service = serviceManagementService.createService(request); ServiceResponse service = serviceManagementService.createService(request);
@ -55,7 +53,6 @@ public class ServiceController {
* 서비스 수정 * 서비스 수정
*/ */
@PutMapping("/{id}") @PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ApiResponse<ServiceResponse>> updateService( public ResponseEntity<ApiResponse<ServiceResponse>> updateService(
@PathVariable Long id, @PathVariable Long id,
@RequestBody @Valid UpdateServiceRequest request) { @RequestBody @Valid UpdateServiceRequest request) {
@ -77,7 +74,6 @@ public class ServiceController {
* 서비스 API 생성 * 서비스 API 생성
*/ */
@PostMapping("/{id}/apis") @PostMapping("/{id}/apis")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ApiResponse<ServiceApiResponse>> createServiceApi( public ResponseEntity<ApiResponse<ServiceApiResponse>> createServiceApi(
@PathVariable Long id, @PathVariable Long id,
@RequestBody @Valid CreateServiceApiRequest request) { @RequestBody @Valid CreateServiceApiRequest request) {

파일 보기

@ -8,7 +8,6 @@ import com.gcsc.connection.tenant.service.TenantService;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@ -33,7 +32,6 @@ public class TenantController {
* 전체 테넌트 목록 조회 * 전체 테넌트 목록 조회
*/ */
@GetMapping @GetMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ApiResponse<List<TenantResponse>>> getTenants() { public ResponseEntity<ApiResponse<List<TenantResponse>>> getTenants() {
List<TenantResponse> tenants = tenantService.getTenants(); List<TenantResponse> tenants = tenantService.getTenants();
return ResponseEntity.ok(ApiResponse.ok(tenants)); return ResponseEntity.ok(ApiResponse.ok(tenants));
@ -43,7 +41,6 @@ public class TenantController {
* 테넌트 생성 * 테넌트 생성
*/ */
@PostMapping @PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ApiResponse<TenantResponse>> createTenant( public ResponseEntity<ApiResponse<TenantResponse>> createTenant(
@RequestBody @Valid CreateTenantRequest request) { @RequestBody @Valid CreateTenantRequest request) {
TenantResponse tenant = tenantService.createTenant(request); TenantResponse tenant = tenantService.createTenant(request);
@ -54,7 +51,6 @@ public class TenantController {
* 테넌트 수정 * 테넌트 수정
*/ */
@PutMapping("/{id}") @PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ApiResponse<TenantResponse>> updateTenant( public ResponseEntity<ApiResponse<TenantResponse>> updateTenant(
@PathVariable Long id, @PathVariable Long id,
@RequestBody @Valid UpdateTenantRequest request) { @RequestBody @Valid UpdateTenantRequest request) {

파일 보기

@ -9,7 +9,6 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@ -36,7 +35,6 @@ public class UserController {
* 전체 사용자 목록 조회 * 전체 사용자 목록 조회
*/ */
@GetMapping @GetMapping
@PreAuthorize("hasAnyRole('ADMIN','MANAGER')")
public ResponseEntity<ApiResponse<List<UserResponse>>> getUsers() { public ResponseEntity<ApiResponse<List<UserResponse>>> getUsers() {
List<UserResponse> users = userService.getUsers(); List<UserResponse> users = userService.getUsers();
return ResponseEntity.ok(ApiResponse.ok(users)); return ResponseEntity.ok(ApiResponse.ok(users));
@ -46,7 +44,6 @@ public class UserController {
* 사용자 생성 * 사용자 생성
*/ */
@PostMapping @PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ApiResponse<UserResponse>> createUser( public ResponseEntity<ApiResponse<UserResponse>> createUser(
@RequestBody @Valid CreateUserRequest request) { @RequestBody @Valid CreateUserRequest request) {
UserResponse user = userService.createUser(request); UserResponse user = userService.createUser(request);
@ -57,7 +54,6 @@ public class UserController {
* 사용자 단건 조회 (ADMIN/MANAGER 또는 본인) * 사용자 단건 조회 (ADMIN/MANAGER 또는 본인)
*/ */
@GetMapping("/{id}") @GetMapping("/{id}")
@PreAuthorize("hasAnyRole('ADMIN','MANAGER')")
public ResponseEntity<ApiResponse<UserResponse>> getUser(@PathVariable Long id) { public ResponseEntity<ApiResponse<UserResponse>> getUser(@PathVariable Long id) {
String currentUserId = SecurityContextHolder.getContext().getAuthentication().getName(); String currentUserId = SecurityContextHolder.getContext().getAuthentication().getName();
if (!isAdminOrManager() && !currentUserId.equals(String.valueOf(id))) { if (!isAdminOrManager() && !currentUserId.equals(String.valueOf(id))) {
@ -86,7 +82,6 @@ public class UserController {
* 사용자 비활성화 (소프트 삭제) * 사용자 비활성화 (소프트 삭제)
*/ */
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<ApiResponse<Void>> deactivateUser(@PathVariable Long id) { public ResponseEntity<ApiResponse<Void>> deactivateUser(@PathVariable Long id) {
userService.deactivateUser(id); userService.deactivateUser(id);
return ResponseEntity.ok(ApiResponse.ok(null, "사용자가 비활성화되었습니다")); return ResponseEntity.ok(ApiResponse.ok(null, "사용자가 비활성화되었습니다"));