React + Springboot(2) - React와 Springboot 연동, Spring Security(JWT) 설정 주의

김정훈·2024년 1월 8일
0

이번에는 React와 Springboot를 연동하고 Jwt를 이용하여 로그인을 처리하겠다. React는 포트가 3000이고 Springboot는 8080이기 때문에 포트 번호를 맞춰줘야 한다.

React 설정

Axios 설치
Axios는 브라우저와 Node.js를 위한 Promise 기반의 HTTP 클라이언트이다. React와 같은 프론트엔드 라이브러리 혹은 프레임워크에서 REST API를 호출하기 위해 사용된다.
Terminal에 내 react 폴더로 접근한 후에 axios 설치

npm install axios

proxy 설정
package.json에서 proxy 설정을 내 서버의 오리진(ex: http://localhost:8080)으로 해주면 /api 주소로 api를 요청하면 proxy가 알아서 도메인을 추적해준다. 만약 api를 요청할 때 오리진/api로 요청할거라면 설정하지 않아도 된다.(이건 제가 해봤을 때 가능했었고 다른 예외 오류가 발생할 수도 있습니다.)

Springboot 설정

서버는 CORS(Cross-Origin Resource Sharing) 정책을 허용해줘야 한다.
내 Api에 @CrossOrigin 설정을 통해 허용할 수 있다.

@RestController
@CrossOrigin(origins = "http://localhost:3000" ,exposedHeaders = "Authorization")
public class WriteController {
    @GetMapping("/write")
    public Map<String, String> write(){
        Map<String, String> hashMap = new HashMap<>();
        hashMap.put("message","글쓰기");
        return hashMap;
    }
}

origins = "http://localhost:3000" 은 localhost:3000에 대한 접근을 허용하는 것이고 exposedHeaders = "Authorization"는 network에 Authorization을 보이게 하는 것인데 이건 공부용으로 해놓은 것이지 실제로는 안하는게 좋다.

SpringSecurity 및 JWT 설정

Security를 사용한다면 추가적으로 해야할 설정이 있다.

우선 SecurityConfig에 cors 설정을 해줘야 한다. (이걸 몰라서 시간을 많이 날렸다.)

 @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
       return
               httpSecurity
                       .cors(cors -> cors.configurationSource(request -> {
                           var corsConfiguration = new CorsConfiguration();
                           if (request.getRequestURI().startsWith("/write")) {
                               corsConfiguration.setAllowedOrigins(List.of("http://localhost:3000"));
                               corsConfiguration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
                               corsConfiguration.setAllowedHeaders(List.of("*"));
                           }
                               return corsConfiguration;
                       }))
                       ...

클라이언트에게 보낼 reponse의 header에 다음 설정을 해준다.
/write에 대한 접근만을 허용했지만 다른 api에 @CrossOrigin을 설정해주면 해당 api도 cors가 적용됩니다. 확실하지 않지만 Security에 cors에 관한 설정을 ON 해서 @CrossOrigin도 적용되는 것 같습니다.

    private void setHeader(HttpServletResponse response, LoginResponseDto loginResponseDto) {
        response.setHeader("ACCESS", loginResponseDto.getTokenDto().getAccessToken());
        response.setHeader("REFRESH", loginResponseDto.getTokenDto().getRefreshToken());
    }

이 설정을 하면 서버가 보낸 응답에 들어있는 각 토큰들에 클라이언트가 접근할 수 있다.

실행

  1. 메인 설정

우선 App.js에 다음 설정을 해준다.

function App() {
    return (
        <Router>
            <Routes>
                <Route path="/" element={<Login/>}></Route>
                <Route path="/main" element={<Main/>}></Route>
            </Routes>
        </Router>
    );
}
export default App;

react는 기본적으로 싱글 페이지 기반이므로 App.js에서 각 컴포는트를 모두 관리하는데 router 설정을 해줘야 요청에 따른 컴포넌트를 랜더링 할 수 있다.

  1. Login
import axios from "axios";
import {useState} from "react";
import {useNavigate} from 'react-router-dom';
import "../css/Login.css"

function Login() {
    const [emailId, setEmail] = useState('')
    const [password, setPassword] = useState('')
    const navigate = useNavigate(); // 추가
    const loginHandler = async (e) => {
        e.preventDefault()
        try {
            const response = await axios.post('/login', {
                emailId: emailId,
                password: password
            });
            const accessToken = response.headers['access']

            localStorage.setItem('accessToken', accessToken);
            axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;

            // 로그인 성공 후, main 페이지로 이동하면서 response를 전달
            navigate('/main', {state:{response:response.data}})

        } catch (error) {
            console.log("로그인 에러", error);
        }
    }
    return (
        <div className={'login'}>
            <h1>Login</h1>
            <form className={'form'}>
                <input
                    type="text"
                    className="input"
                    value={emailId}
                    onChange={e => setEmail(e.target.value)}
                    placeholder="이메일"
                />
                <input
                    type="password"
                    className="input"
                    value={password}
                    onChange={e => setPassword(e.target.value)}
                    placeholder="비밀번호"
                />
                <button type="submit" className="button" onClick={loginHandler}>로그인</button>
            </form>
        </div>
    )
}

export default Login;

/login api를 요청하고 response에 담긴 token을 얻는다. 서버마다 Authorization에 설정할 내용이 다른데 난 보편적인 accessToken 값을 줬다.
localstorage에 token을 담아 클라이언트가 사용하기 쉽게 했고
이후에 실행할 컴포는트에 로그인을 통해 받은 reponse값을 담아서 navigate 하면 바로 다음 컴포넌트로 넘어간다.

  1. 확인

    이후 토큰이 잘 담겼다면 성공이다. 난 로그인 후 Main.js 컴포논트를 불러왔고 이 컴포는트는 /write를 요청하여 {"message" : "글쓰기"}라는 결과를 받아온다.

import {useLocation} from "react-router-dom";
import {useEffect, useState} from "react";
import axios from "axios";

function Main() {
    const token = localStorage.getItem('accessToken');
    const [data, setData] = useState(null);
    const location = useLocation();
    const response = location.state.response;
    useEffect(() => {
        const fetchData = async () => {
            try {
                const res = await axios.get("/write",   {

                    headers: {'Authorization': `Bearer ${token}`}
                });
                setData(res.data);

            } catch (err) {
                console.error(err);
            }
        };
        fetchData();
    }, []); // 빈 dependency 배열을 추가하여 컴포넌트가 마운트될 때 한 번만 실행하게 합니다.

    return (
        <div style={{
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            height: '100vh', // 뷰포트 높이의 100%를 사용하여 전체 화면 높이를 채웁니다.
            fontSize: '2em' // 글자 크기를 2em으로 설정합니다.
        }}>
            {data ? data.message : "Loading..."}
        </div>
    )
}

export default Main;

response에는 Login.js로부터 받은 응답값들이 들어있지만 현재는 활용하지 않았다.
/write 요청을 token과 함께 진행하면 서버가 token을 분석하고 권한을 승인한다.


Authorization에 request header에 잘 담겼고


응답도 잘 받았다.

profile
백엔드 개발자

0개의 댓글