페이지 만들기
// import React from 'react'
// import ReactDOM from 'react-dom/client'
// import { BrowserRouter } from 'react-router-dom'
import './Login.css'
// 연결했음 내보는게 있어야 되고 리턴값으로 화면에 출력함.(필수)
export function Login() {
return <div>123</div>
}
// default를 붙일 시 이 파일의 대표 컴포넌트는 1개 라는 의미이기때문에
// App.jsx에서는 import 대괄호를 안써도 됨.
// export default function Login(){
// return <div>123</div>
// }
App.jsx에 이동할 페이지 임포트 및 라우터 추가 하기
- 기존 레이아웃에서 벗어나려면 메인 레이아웃 밖에 작성
import { Routes, Route } from 'react-router-dom'
import MainLayout from './layouts/MainLayout'
import Dashboard from './pages/Dashboard'
import {Login} from './pages/Login'
export default function App() {
return (
<Routes>
<Route element={<MainLayout />}>
<Route path="/" element={<Dashboard />} />
</Route>
<Route path="/login" element={<Login />}></Route>
</Routes>
)
}
버튼 있는 곳에 링크 달아주기
- 리액트 라우터 돔 임포트 되어 있는 곳에 변수명을 이용해서 아래와 같이 링크 걸어주기
<NavLink to="/login"><button className="ghostBtn">Login</button></NavLink>
// src/layouts/MainLayout.jsx
import { Outlet, NavLink } from 'react-router-dom'
import './MainLayout.css'
import logo from "../assets/images/Logo.png";
export default function MainLayout() {
return (
<div className="appShell">
<aside className="sidebar">
<div className="sidebarTitle"><img className="logo" src={logo} alt="logo"/></div>
<nav className="sidebarNav">
<NavLink to="/" end className={({ isActive }) => `navItem ${isActive ? 'active' : ''}`}>
Dashboard
</NavLink>
<NavLink to="/tasks" className={({ isActive }) => `navItem ${isActive ? 'active' : ''}`}>
My Tasks
</NavLink>
<NavLink to="/projects" className={({ isActive }) => `navItem ${isActive ? 'active' : ''}`}>
Projects
</NavLink>
<NavLink to="/team" className={({ isActive }) => `navItem ${isActive ? 'active' : ''}`}>
Team
</NavLink>
<NavLink to="/calendar" className={({ isActive }) => `navItem ${isActive ? 'active' : ''}`}>
Calendar
</NavLink>
<NavLink to="/reports" className={({ isActive }) => `navItem ${isActive ? 'active' : ''}`}>
Reports
</NavLink>
<NavLink to="/settings" className={({ isActive }) => `navItem ${isActive ? 'active' : ''}`}>
Settings
</NavLink>
</nav>
</aside>
<div className="mainCol">
<header className="header">
<div className="headerTitle">Dashboard</div>
<div className="headerActions">
<button className="ghostBtn">Search</button>
<button className="ghostBtn">Notifications</button>
<button className="ghostBtn">Profile</button>
<NavLink to="/login"><button className="ghostBtn">Login</button></NavLink>
</div>
</header>
<main className="content">
<Outlet />
</main>
</div>
</div>
)
}
로그인 페이지 만들기
import './Login.css'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
function Login() {
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
// 여기서 값을 받아옴
const loginHandle = async () => {
try {
const response = await fetch("<http://localhost:8081/api/login>", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
email,
password
})
});
// 로그 출력
// const log = await response.text();
// console.log(response.status);
// console.log(log);
if (!response.ok){
throw new Error("로그인 실패");
}
// 로그인 후 이동
navigate("/")
} catch (error){
console.error(error);
alert("아이디 또는 비밀번호가 틀렸습니다.");
}
};
// 받은 값 출력
return (
<div>
<h2>로그인</h2>
<input
type='email'
placeholder='이메일'
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type='password'
placeholder='비밀번호'
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button onClick={loginHandle}>로그인</button>
</div>
);
}
export default Login;
package com.workflow.common.config;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import com.workflow.auth.jwt.JwtAuthFilter;
import com.workflow.auth.jwt.JwtProvider;
// 스프링 설정 클래스, 클래스 안의 @Bean들을 스프링이 등록함.
@Configuration
// 스프링 시큐리티 기능을 활성화하고 아래의 설정들을 적용
@EnableWebSecurity
public class SecurityConfig {
// 핵심
// SecurityFilterChain: 보안 필터 규칙 세트
// HttpSecurity로 어떤 요청을 허용/차단할지, 어떤 인증 방식을 쓸지 설정
// JwtAuthFilter를 여기에 끼워 넣는 구조
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtAuthFilter) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.cors(cors -> {}) // 아래 CORS Bean 필요
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// React 연동 중엔 폼로그인/기본인증 끄기 (리다이렉트 방지)
.formLogin(form -> form.disable())
.httpBasic(basic -> basic.disable())
// 인증 실패시 302 말고 401/403
.exceptionHandling(ex -> ex
.authenticationEntryPoint((req, res, e) -> res.sendError(401))
.accessDeniedHandler((req, res, e) -> res.sendError(403))
)
// 지금은 일단: auth API만 열고, 나머지 API 보호
.authorizeHttpRequests(auth -> auth
// /api/health만 임시로 열어서 200뜨게 하기(연결 확인용) 추가
// .requestMatchers("/api/health").permitAll()
.requestMatchers("/api/kpi", "/api/login").permitAll()
.requestMatchers("/api/auth/**").permitAll()
// 로그인 후에만 api접근 가능
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("<http://localhost:5173>"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setExposedHeaders(List.of("Authorization"));
config.setAllowCredentials(false); // JWT를 헤더로만 쓸 거면 false 권장
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
// DB 없이 메모리 유저
// UserDetailsService 시큐리티가 이 username의 사용자 정보 가져와 할때 쓰이는 인터페이스
// DB가 있다면 보통 userRepositiry로 조회해서 반환
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
UserDetails user = User.builder().username("user").password(encoder.encode("1234")).roles("USER").build();
return new InMemoryUserDetailsManager(user);
}
// 비밀번호를 평문으로 비교하면 위험하니 해시로 비교
// BCryptPasswordEncoder가 가장 흔한 표준
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// /login에서 authenticate() 쓸 때 필요
// ex) uthenticationManager.authenticate(new
// UsernamePasswordAuthenticationToken(id, pw)) 이걸로 시큐리티 방식대로 인증 수행
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
// 필터도 Bean으로 등록 (순환참조 끊기)
// JwtAuthFilter를 스프링 빈으로 등록해서 위의 .addFilterBefore(jwtAuthFilter, ...)에 주입되게 함.
@Bean
public JwtAuthFilter jwtAuthFilter(JwtProvider jwtProvider, UserDetailsService userDetailsService) {
return new JwtAuthFilter(jwtProvider, userDetailsService);
}
}
package com.workflow.common.config;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> {}) // 아래 CORS Bean 필요
// React 연동 중엔 폼로그인/기본인증 끄기 (리다이렉트 방지)
.formLogin(form -> form.disable())
.httpBasic(basic -> basic.disable())
// 인증 실패시 302 말고 401/403
.exceptionHandling(ex -> ex
.authenticationEntryPoint((req, res, e) -> res.sendError(401))
.accessDeniedHandler((req, res, e) -> res.sendError(403))
)
// 지금은 일단: auth API만 열고, 나머지 API 보호
.authorizeHttpRequests(auth -> auth
// /api/health만 임시로 열어서 200뜨게 하기(연결 확인용) 추가
// .requestMatchers("/api/health").permitAll()
.requestMatchers("/api/kpi", "/api/login").permitAll()
.requestMatchers("/api/auth/**").permitAll()
// 로그인 후에만 api접근 가능
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
);
return http.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("<http://localhost:5173>"));
config.setAllowedMethods(List.of("GET","POST","PUT","PATCH","DELETE","OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setExposedHeaders(List.of("Authorization"));
config.setAllowCredentials(false); // JWT를 헤더로만 쓸 거면 false 권장
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}