페이지 만들기

// 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;
    }
}