100일 챌린지/빅데이터기반 인공지능 융합 서비스 개발자

Day 81 - React에서 login 프로그램 만들기 (4) Spring backend 활용하기 + Oauth

ksyke 2024. 11. 22. 17:32

프로젝트 만들기

application.properties

spring.application.name=sts15

spring.h2.console.path=/h2
spring.h2.console.enabled=true

spring.datasource.url=jdbc:h2:mem:testDB
spring.datasource.username=sa

spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true

logging.level.com.gimhae.sts15.controller=debug

Dept.class

package com.gimhae.sts15.model.entity;

import com.gimhae.sts15.model.Deptvo;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class Dept {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	int deptno;
	String dname;
	String loc;
	public Deptvo toVo() {
		return Deptvo.builder().deptno(deptno).dname(dname).loc(loc).build();
		
	}
}

DeptVo.class

package com.gimhae.sts15.model;

import com.gimhae.sts15.model.entity.Dept;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Deptvo {

	int deptno;
	String dname;
	String loc;
	
	public Dept toEntity() {
		return Dept.builder().deptno(deptno).dname(dname).loc(loc).build();
		
	}
}

Users.class

package com.gimhae.sts15.model.entity;

import com.gimhae.sts15.model.UsersVo;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Users {
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	int num;
	String email;
	String pw;
	String name;
	
	public UsersVo toVo() {
		return UsersVo.builder().num(num).email(email).pw(pw).name(name).build();
		
	}
}

UsersVo.class

package com.gimhae.sts15.model;

import com.gimhae.sts15.model.entity.Users;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UsersVo {

	int num;
	String email,pw,name;
	
	public Users toEntity() {
		return Users.builder().num(num).email(email).pw(pw).name(name).build();
	}
}

DeptRepo.Interface

package com.gimhae.sts15.model;

import org.springframework.data.repository.CrudRepository;

import com.gimhae.sts15.model.entity.Dept;

public interface DeptRepo extends CrudRepository<Dept, Integer> {

}

UsersRepo.Class

package com.gimhae.sts15.model;

import java.util.Optional;

import org.springframework.data.repository.CrudRepository;

import com.gimhae.sts15.model.entity.Users;

public interface UsersRepo extends CrudRepository<Users, Integer> {

	Optional<Users> findByEmailAndPw(String email,String pw);
}

ProjectApplication

package com.gimhae.sts15;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

import com.gimhae.sts15.model.DeptRepo;
import com.gimhae.sts15.model.Deptvo;
import com.gimhae.sts15.model.UsersRepo;
import com.gimhae.sts15.model.UsersVo;

@SpringBootApplication
public class Sts15Application implements CommandLineRunner{

	public static void main(String[] args) {
		SpringApplication.run(Sts15Application.class, args);
	}
	
	@Autowired
	UsersRepo usersRepo;
	@Autowired
	DeptRepo deptRepo;

	@Override
	public void run(String... args) throws Exception {
		List<UsersVo> userList=List.of(
			UsersVo.builder().email("user1@localhost").pw("1234").name("user1").build(),
			UsersVo.builder().email("user2@localhost").pw("1234").name("user2").build()
		);
		
		userList.forEach(vo->usersRepo.save(vo.toEntity()));		
//		usersRepo.saveAll(()->userList.iterator());
		
		List.of(
				Deptvo.builder().dname("test1").loc("test").build(),
				Deptvo.builder().dname("test2").loc("test").build(),
				Deptvo.builder().dname("test3").loc("test").build(),
				Deptvo.builder().dname("test4").loc("test").build()
				).forEach(vo->deptRepo.save(vo.toEntity()));
	}

}

DeptController

package com.gimhae.sts15.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.gimhae.sts15.model.DeptRepo;
import com.gimhae.sts15.model.Deptvo;

import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;


@RestController
@RequiredArgsConstructor
@RequestMapping("/dept/")
@CrossOrigin(origins = "http://localhost:3000",methods = {RequestMethod.GET,RequestMethod.POST})
@Slf4j
public class DeptController {
	final DeptRepo deptRepo;
	ResponseEntity resp=ResponseEntity.status(401).build();

	@GetMapping("")
	public ResponseEntity<?> list(HttpSession session) {
//		log.debug(session.getAttribute("result").toString());
		if(session.getAttribute("result")==null) return resp;
		log.debug("login ok");
		List<Object> list=new ArrayList<>();
		deptRepo.findAll().forEach(entity->list.add(entity.toVo()));
		return ResponseEntity.ok().body(list);
	}
	
	@PostMapping("")
	public ResponseEntity<?> add(@RequestBody Deptvo bean,HttpSession session) {
		if(session.getAttribute("result")==null) return resp;
		deptRepo.save(bean.toEntity());
		return ResponseEntity.ok().build();
	}
	
	@GetMapping("{deptno}")
	public ResponseEntity<?> detail(@PathVariable int deptno,HttpSession session) {
		if(session.getAttribute("result")==null) return resp;
		deptRepo.findById(deptno).ifPresentOrElse(t->{
			resp=ResponseEntity.ok().body(t.toVo());
		}, ()->{
			resp=ResponseEntity.badRequest().build();
		});
		return resp;
	}
}

LoginController

package com.gimhae.sts15.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.gimhae.sts15.model.UsersRepo;
import com.gimhae.sts15.model.UsersVo;

import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;



@RestController
@RequestMapping("/login/")
@RequiredArgsConstructor
@Slf4j
@CrossOrigin(origins = "http://localhost:3000",methods = {RequestMethod.GET,RequestMethod.POST})
public class LoginController {
	final UsersRepo usersRepo;
	ResponseEntity<?> resp;

	@PostMapping("")
	public ResponseEntity<?> login(String id,String pw,HttpSession session) {
//		log.debug("{1}:{2}".format(id, pw));
		log.debug("id:"+id);
		log.debug("pw:"+pw);
		
		usersRepo.findByEmailAndPw(id, pw).ifPresentOrElse(entity->{
			UsersVo bean=entity.toVo();
//			resp=ResponseEntity.ok(bean);
			resp=ResponseEntity.ok().body(bean);
			session.setAttribute("result", true);
			session.setAttribute("num", bean.getNum());
			log.debug("로그인됨");
		}, ()->{
			resp=ResponseEntity.badRequest().build();
		});
		return resp;
	}
	
	@GetMapping("logout")
	public ResponseEntity logout(HttpSession session) {
		session.invalidate();
		return ResponseEntity.ok().build();
	}
	
}

LoginFilter

package com.gimhae.sts15;

import java.io.IOException;

import org.springframework.http.HttpRequest;
import org.springframework.stereotype.Component;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

@Component
public class LoginFilter implements Filter{

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {

		System.out.println("before filter...");
		
		HttpServletRequest req=(HttpServletRequest) request;
		HttpServletResponse res=(HttpServletResponse) response;
		
		res.setHeader("Access-Control-Allow-Credentials", "true");
		if(req.getRequestURI().startsWith("/dept")) {
			HttpSession session=req.getSession();
			if(session.getAttribute("result")!=null) {
				chain.doFilter(request, response);
			}else {
				res.sendError(401);
			}
		}else {
			chain.doFilter(request, response);
		}
		System.out.println("after filter...");			
	}

}

React 프로젝트 만들기

App.js

import logo from './logo.svg';
import './App.css';
import { BrowserRouter, Route, Router, Routes } from 'react-router-dom';
import Main from './pages/Main';
import Intro from './pages/Intro';
import Depts from './pages/Depts';
import Login from './pages/Login';
import { createContext, useState } from 'react';
import Logout from './pages/Logout';
import Dept from './pages/Dept';

export const LoginContext=createContext();

function App() {
  const [login,setLogin]=useState(false);
  return (
    <LoginContext.Provider value={[login,setLogin]}>
    <BrowserRouter>
      <Routes>
          <Route path='/' element={<Main/>}/>
          <Route path='/intro' element={<Intro/>}/>
          <Route path='/dept/' element={<Depts/>}/>
          <Route path='/dept/:deptno' element={<Dept/>}/>
          <Route path='/login/' element={<Login/>}/>
          <Route path='/logout/' element={<Logout/>}/>
      </Routes>
    </BrowserRouter>
    </LoginContext.Provider>
  );
}

export default App;

Main.js

import React, { useContext } from 'react'
import { Link } from 'react-router-dom'
import { LoginContext } from '../App';

function Main() {
    const [login,setLogin]=useContext(LoginContext);
  return (
    <>
        <nav>
            <Link to={'/'}>home</Link>{'    '}
            <Link to={'/intro'}>intro</Link>{'    '}
            <Link to={'/dept/'}>dept</Link>{'    '}
            {login?<Link to={'/logout'}>logout</Link>:<Link to={'/login'}>login</Link>}
            
        </nav>
        <div className='container'>
            <h2>Index page</h2>
        </div>
    </>
  )
}

export default Main

Intro.js

import React, { useContext } from 'react'
import { Link } from 'react-router-dom'
import { LoginContext } from '../App';

function Main() {
    const [login,setLogin]=useContext(LoginContext);
  return (
    <>
        <nav>
            <Link to={'/'}>home</Link>{'    '}
            <Link to={'/intro'}>intro</Link>{'    '}
            <Link to={'/dept/'}>dept</Link>{'    '}
            {login?<Link to={'/logout'}>logout</Link>:<Link to={'/login'}>login</Link>}
            
        </nav>
        <div className='container'>
            <h2>Index page</h2>
        </div>
    </>
  )
}

export default Main

Depts.js

import React, { useEffect, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'

function Depts() {
    const navigate=useNavigate();
    const [arr,setArr]=useState([]);
    useEffect(()=>{
        fetch('http://localhost:8080/dept/',{
            method:'GET',
            credentials:'include'
        })
        .then(res=>res.json())
        .then(json=>setArr(json))
        .catch(err=>{
            console.log(err);
            navigate('/login/');
        })
    },[]);
  return (
    <>
        <nav>
            <Link to={'/'}>home</Link>{'    '}
            <Link to={'/intro'}>intro</Link>{'    '}
            <Link to={'/dept/'}>dept</Link>{'    '}
            <Link to={'/login/'}>login</Link>
        </nav>
        <div className='container'>
            <h2>list page</h2>
            {arr.map(ele=>(
            <Link to={'/dept/'+ele.deptno}>
            <dl>
                <dt>{ele.dname}</dt>
                <dd>{ele.loc}</dd>
            </dl>
            </Link>
        ))}
        </div>
    </>
  )
}

export default Depts

Dept.js

import React, { useEffect, useState } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'

function Dept() {
    const navigate=useNavigate();
    const {deptno}=useParams();
    const [bean,setBean]=useState(null);
    const [editable,setEditable]=useState(true);
    useEffect(()=>{
        fetch('http://localhost:8080/dept/'+deptno,{
            method:'GET',
            credentials:'include'
        })
        .then(res=>res.json()).then(json=>setBean(json)).catch(alert);
    },[]);

    const editAction=(e)=>{
        e.preventDefault();
        if(editable)
            setEditable(!editable);
        else
            navigate('/dept/');
    };
  return (
    <>
        <nav>
            <Link to={'/'}>home</Link>{'    '}
            <Link to={'/intro'}>intro</Link>{'    '}
            <Link to={'/dept/'}>dept</Link>{'    '}
            <Link to={'/login/'}>login</Link>
        </nav>
        <div className='container'>
            <h2>{editable?'edit':'detail'} page</h2>
            <form onSubmit={editAction}>
                <div>
                    <input name='deptno' value={bean==null?'':bean.deptno} readOnly={true}/>
                </div>
                <div>
                    <input name='dname' value={bean==null?'':bean.dname} 
                        onChange={e=>setBean({...bean,dname:e.target.value})} readOnly={editable}/>
                </div>
                <div>
                    <input name='loc' value={bean==null?'':bean.loc}
                        onChange={e=>setBean({...bean,loc:e.target.value})} readOnly={editable}/>
                </div>
                <div>
                    <button type='submit'>수정</button>
                </div>
            </form>
        </div>
    </>
  )
}

export default Dept

Login.js

import React, { useContext, useRef } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { LoginContext } from '../App';

function Login() {
    const [login,setLogin]=useContext(LoginContext);
    const navigate=useNavigate();
    const refId=useRef();
    const refPw=useRef();
    const loginAction=(e)=>{
        console.log(`id=${refId.current.value}&pw=${refPw.current.value}`);
        e.preventDefault();
        fetch('http://localhost:8080/login/',{
            method:'POST',
            body:`id=${refId.current.value}&pw=${refPw.current.value}`,
            credentials:'include',
            headers:{
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        })
        .then(res=>res.json())
        .then(json=>{
            setLogin(true);
            navigate('/');
        })
        .catch(alert);
    };
  return (
    <>
        <nav>
            <Link to={'/'}>home</Link>{'    '}
            <Link to={'/intro'}>intro</Link>{'    '}
            <Link to={'/dept/'}>dept</Link>{'    '}
            <Link to={'/login/'}>login</Link>
        </nav>
        <div className='container'>
            <h2>login page</h2>
            <form onSubmit={loginAction}>
                <div>
                    <input ref={refId} name='id' placeholder='email'/>
                </div>
                <div>
                    <input ref={refPw} name='pw' placeholder='password'/>
                </div>
                <div>
                    <button>로그인</button>
                </div>
            </form>
        </div>
    </>
  )
}

export default Login

Logout.js

import React, { useContext, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { LoginContext } from '../App';

function Logout() {
    const [login,setLogin]=useContext(LoginContext);
    useEffect(()=>{
        fetch('http://localhost:8080/login/logout',{
            method:'GET',
            credentials:'include'
        })
        .then(res=>res.ok)
        .catch(alert)
        .finally(()=>{
            setLogin(false);
        });
    },[]);
  return (
    <>
        <nav>
            <Link to={'/'}>home</Link>{'    '}
            <Link to={'/intro'}>intro</Link>{'    '}
            <Link to={'/dept/'}>dept</Link>{'    '}
            <Link to={'/login/'}>login</Link>
        </nav>
        <div className='container'>
            <h2>logout page</h2>
            <p>이용해주셔서 감사합니다</p>
        </div>
    </>
  )
}

export default Logout

Oauth

jwt(표준규격모델) 토큰을 이용해 로그인을 관리한다. 

https://jwt.io/libraries?language=Java

 

JWT.IO

JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

jwt.io

 

pom.xml

		<dependency>
			<groupId>oi.jsonwebtoken</groupId>
			<artifactId>jjwt-api</artifactId>
			<vresion>0.11.2</vresion>
		</dependency>
		<dependency>
		    <groupId>io.jsonwebtoken</groupId>
		    <artifactId>jjwt-impl</artifactId>
		    <version>0.11.2</version>
		    <scope>runtime</scope>
		</dependency>
		<dependency>
		    <groupId>io.jsonwebtoken</groupId>
		    <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
		    <version>0.11.2</version>
		    <scope>runtime</scope>
		</dependency>

JwtService

package com.gimhae.sts15.service;

import java.security.Key;
import java.util.Base64;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap.KeySetView;

import org.springframework.stereotype.Service;

import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

@Service
public class JwtService {
	final Key key=Keys.secretKeyFor(SignatureAlgorithm.HS256);
	
	public String createToken(String email) {
		System.out.println(key.toString());
		System.out.println(Base64.getEncoder().encode(key.getEncoded()));
		String token=Jwts.builder()
				.addClaims(Map.of("email",email))
				.setExpiration(new Date(System.currentTimeMillis()+1000*100))
				.signWith(key)
				.compact();
		
		return token;
	}
	
	public String getAuthEmail(String token) {
		JwtParser parser=Jwts.parserBuilder()
				.setSigningKey(key)
				.build();
		
		String email=(String) parser.parseClaimsJws(token).getBody().get("email");
		
		return email;
	}

}

Project Application

package com.gimhae.sts15;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

import com.gimhae.sts15.model.DeptRepo;
import com.gimhae.sts15.model.Deptvo;
import com.gimhae.sts15.model.UsersRepo;
import com.gimhae.sts15.model.UsersVo;
import com.gimhae.sts15.service.JwtService;

@SpringBootApplication
public class Sts15Application implements CommandLineRunner{

	public static void main(String[] args) {
		SpringApplication.run(Sts15Application.class, args);
	}
	
	@Autowired
	UsersRepo usersRepo;
	@Autowired
	DeptRepo deptRepo;

	@Override
	public void run(String... args) throws Exception {
		List<UsersVo> userList=List.of(
			UsersVo.builder().email("user1@localhost").pw("1234").name("user1").build(),
			UsersVo.builder().email("user2@localhost").pw("1234").name("user2").build()
		);
		
		userList.forEach(vo->usersRepo.save(vo.toEntity()));		
//		usersRepo.saveAll(()->userList.iterator());
		
		List.of(
				Deptvo.builder().dname("test1").loc("test").build(),
				Deptvo.builder().dname("test2").loc("test").build(),
				Deptvo.builder().dname("test3").loc("test").build(),
				Deptvo.builder().dname("test4").loc("test").build()
				).forEach(vo->deptRepo.save(vo.toEntity()));
		
		String token=jwtService.createToken("user1@localhost");
		System.out.println("token:"+token);
		String email=jwtService.getAuthEmail(token);
		System.out.println("email:"+email);
	}
	
	@Autowired
	JwtService jwtService;
	
	

}

LoginFilter

package com.gimhae.sts15;

import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpRequest;
import org.springframework.stereotype.Component;

import com.gimhae.sts15.model.UsersRepo;
import com.gimhae.sts15.model.entity.Users;
import com.gimhae.sts15.service.JwtService;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

@Component
public class LoginFilter implements Filter{
	@Autowired
	JwtService jwtService;
	@Autowired
	UsersRepo usersRepo;

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {

		System.out.println("before filter...");
		
		HttpServletRequest req=(HttpServletRequest) request;
		HttpServletResponse res=(HttpServletResponse) response;
		System.out.println(req.getRequestURI());
		
		res.setHeader("Access-Control-Allow-Credentials", "true");
		if(req.getRequestURI().startsWith("/dept")) {
//			HttpSession session=req.getSession();
//			if(session.getAttribute("result")!=null) {
//				chain.doFilter(request, response);
//			}else {
//				res.sendError(401);
//			}
			try {
			String token=req.getHeader("Authorization");
//			if(!token.startsWith("Bearer ")) {
//				throw new RuntimeException();
//			}
			System.out.println("test"+token);
			token=token.split(" ")[1];
			System.out.println("token:"+token);
			String email=jwtService.getAuthEmail(token);
			System.out.println("email:"+email);
			Users user=usersRepo.findByEmail(email);
			System.out.println("user:"+user);
			chain.doFilter(request, response);
			}catch(Exception e) {
				res.sendError(401);
			}
		}else {
			chain.doFilter(request, response);
		}
		System.out.println("after filter...");			
	}

}

LoginControl

package com.gimhae.sts15.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.gimhae.sts15.model.UsersRepo;
import com.gimhae.sts15.model.UsersVo;
import com.gimhae.sts15.service.JwtService;

import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;



@RestController
@RequestMapping("/login/")
@RequiredArgsConstructor
@Slf4j
@CrossOrigin(origins = "http://localhost:3000",methods = {RequestMethod.GET,RequestMethod.POST})
public class LoginController {
	final UsersRepo usersRepo;
	final JwtService jwtService;
	ResponseEntity<?> resp;

	@PostMapping("")
	public ResponseEntity<?> login(String id,String pw,HttpSession session) {
		log.debug("id:"+id);
		log.debug("pw:"+pw);
		
		usersRepo.findByEmailAndPw(id, pw).ifPresentOrElse(entity->{
			UsersVo bean=entity.toVo();
			String token=jwtService.createToken(id);
			resp=ResponseEntity.ok().body(token);
			log.debug("로그인됨");
		}, ()->{
			resp=ResponseEntity.badRequest().build();
		});
		return resp;
	}
	
	@GetMapping("logout")
	public ResponseEntity logout(HttpSession session) {
		session.invalidate();
		return ResponseEntity.ok().build();
	}
	
}

UsersRepo

package com.gimhae.sts15.model;

import java.util.Optional;

import org.springframework.data.repository.CrudRepository;

import com.gimhae.sts15.model.entity.Users;

public interface UsersRepo extends CrudRepository<Users, Integer> {

	Optional<Users> findByEmailAndPw(String email,String pw);

	Users findByEmail(String email);
}

DeptControl

package com.gimhae.sts15.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.gimhae.sts15.model.DeptRepo;
import com.gimhae.sts15.model.Deptvo;

import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;


@RestController
@RequiredArgsConstructor
@RequestMapping("/dept/")
@CrossOrigin(origins = "http://localhost:3000",methods = {RequestMethod.GET,RequestMethod.POST})
@Slf4j
public class DeptController {
	final DeptRepo deptRepo;
	ResponseEntity resp=ResponseEntity.status(401).build();

	@GetMapping("")
	public ResponseEntity<?> list(HttpSession session) {
//		log.debug(session.getAttribute("result").toString());
//		if(session.getAttribute("result")==null) return resp;
		log.debug("login ok");
		List<Object> list=new ArrayList<>();
		deptRepo.findAll().forEach(entity->list.add(entity.toVo()));
		return ResponseEntity.ok().body(list);
	}
	
	@PostMapping("")
	public ResponseEntity<?> add(@RequestBody Deptvo bean,HttpSession session) {
		if(session.getAttribute("result")==null) return resp;
		deptRepo.save(bean.toEntity());
		return ResponseEntity.ok().build();
	}
	
	@GetMapping("{deptno}")
	public ResponseEntity<?> detail(@PathVariable int deptno,HttpSession session) {
		if(session.getAttribute("result")==null) return resp;
		deptRepo.findById(deptno).ifPresentOrElse(t->{
			resp=ResponseEntity.ok().body(t.toVo());
		}, ()->{
			resp=ResponseEntity.badRequest().build();
		});
		return resp;
	}
}

Login.js

import React, { useContext, useRef } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { LoginContext } from '../App';

function Login() {
    const [login,setLogin]=useContext(LoginContext);
    const navigate=useNavigate();
    const refId=useRef();
    const refPw=useRef();

    const loginAction=(e)=>{
        console.log(`id=${refId.current.value}&pw=${refPw.current.value}`);
        e.preventDefault();
        fetch('http://localhost:8080/login/',{
            method:'POST',
            body:`id=${refId.current.value}&pw=${refPw.current.value}`,
            credentials:'include',
            headers:{
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        })
        .then(res=>res.text())
        .then(token=>{
            console.log('login token:'+token);
            setLogin('Bearer '+token);
            navigate('/');
        })
        .catch(alert);
    };
  return (
    <>
        <nav>
            <Link to={'/'}>home</Link>{'    '}
            <Link to={'/intro'}>intro</Link>{'    '}
            <Link to={'/dept/'}>dept</Link>{'    '}
            <Link to={'/login/'}>login</Link>
        </nav>
        <div className='container'>
            <h2>login page</h2>
            <form onSubmit={loginAction}>
                <div>
                    <input ref={refId} name='id' placeholder='email'/>
                </div>
                <div>
                    <input ref={refPw} name='pw' placeholder='password'/>
                </div>
                <div>
                    <button>로그인</button>
                </div>
            </form>
        </div>
    </>
  )
}

export default Login

Depts.js

import React, { useContext, useEffect, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { LoginContext } from '../App';

function Depts() {

    const [login,setLogin]=useContext(LoginContext);
    const navigate=useNavigate();
    const [arr,setArr]=useState([]);

    useEffect(()=>{
        console.log(login);
        //if(login==null) return navigate('/login/');
        fetch('http://localhost:8080/dept/',{
            method:'GET',
            credentials:'include',
            withCredentials:true,
            headers:{
                'Authorization':login,
                'Content-Type':'application/json'
            }
        })
        .then(res=>{res.json();
            console.log('login then:'+login);})
        .then(json=>setArr(json))
        .catch(err=>{
            console.log(err);
            // navigate('/login/');
        });
    },[]);

  return (
    <>
        <nav>
            <Link to={'/'}>home</Link>{'    '}
            <Link to={'/intro'}>intro</Link>{'    '}
            <Link to={'/dept/'}>dept</Link>{'    '}
            <Link to={'/login/'}>login</Link>
        </nav>
        <div className='container'>
            <h2>list page</h2>
            {arr.map(ele=>(
            <Link to={'/dept/'+ele.deptno}>
            <dl>
                <dt>{ele.dname}</dt>
                <dd>{ele.loc}</dd>
            </dl>
            </Link>
        ))}
        </div>
    </>
  )
}

export default Depts

App.js

import logo from './logo.svg';
import './App.css';
import { BrowserRouter, Route, Router, Routes } from 'react-router-dom';
import Main from './pages/Main';
import Intro from './pages/Intro';
import Depts from './pages/Depts';
import Login from './pages/Login';
import { createContext, useState } from 'react';
import Logout from './pages/Logout';
import Dept from './pages/Dept';

export const LoginContext=createContext();

function App() {
  const [login,setLogin]=useState(null);
  return (
    <LoginContext.Provider value={[login,setLogin]}>
    <BrowserRouter>
      <Routes>
          <Route path='/' element={<Main/>}/>
          <Route path='/intro' element={<Intro/>}/>
          <Route path='/dept/' element={<Depts/>}/>
          <Route path='/dept/:deptno' element={<Dept/>}/>
          <Route path='/login/' element={<Login/>}/>
          <Route path='/logout/' element={<Logout/>}/>
      </Routes>
    </BrowserRouter>
    </LoginContext.Provider>
  );
}

export default App;