본문 바로가기
Language/Java & JavaScript

Spring Security / jwt 구현 후, React-Redux 환경, 로그인 연결 복습

by 쿠키오빠 2025. 2. 18.
반응형

Spring Security / jwt 토큰을 이용한 '로그인'을 postman으로 검증 후, 바로 프론트 단과 연결하였다.

 

React 환경으로 작업을 하는데, 수많은(코드 복호화 안 됨, 응답 못함 등등...) 에러가 있었지만....

 

당시엔 수정하느냐고 기록을 하지 못하였다.

 

그리하여 오늘은 복습 겸 어떤식으로 구성했는지 남겨보려고 한다.


일단 서버에서 인증 성공 후 토큰을 생성을 위한 onAuthenticationSuccess 메소드에서 프론트에 넘겨줄 값을 담아준다.

@Configuration
public class CustomAuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        System.out.println(" 로그인 성공 - JWT 토큰 생성 중...");
        MemberDTO member  = ((MemberDTO) authentication.getPrincipal());

        HashMap<String, Object> responseMap = new HashMap<>();
        JSONObject jsonValue = null;
        JSONObject jsonObject;

        if(member.getMemberRole().equals("LIMIT")){
            System.out.println("LIMIT으로 왔는지");
            responseMap.put("userInfo", jsonValue);
            responseMap.put("status", 500);
            responseMap.put("message","휴먼상태인 계정입니다.");
        }

        else{

            String token = TokenUtils.generateJwtToken(member);
            System.out.println("토큰 생성 됐는지 token = " + token);
            // tokenDTO response
            TokenDTO tokenDTO = TokenDTO.builder()
                                .memberName(member.getUsername())
                                .accessToken(token)
                                .grantType(AuthConstants.TOKEN_TYPE)
                                .memberRole(member.getMemberRole())
                                .memberId(member.getMemberId())
                                .build();

            jsonValue = (JSONObject) ConvertUtil.convertObjectToJsonObject(tokenDTO);
            System.out.println("userInfo에 담기는 jsonValue = " + jsonValue);
            responseMap.put("userInfo", jsonValue);
            responseMap.put("status", 200);
            responseMap.put("message", "로그인 성공");
        }

        jsonObject = new JSONObject(responseMap);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        PrintWriter printWriter = response.getWriter();
        printWriter.println(jsonObject);
        printWriter.flush();
        printWriter.close();
    }
}

builder()를 이용하여 tokenDTO와 매칭시켜 담아주는데 나는 회원 이름 , JWT 토큰, 토큰 타입, 회원 권한, 회원 ID 등이 필요하여 요것만 담아줬다.

 


최초 로그인은 시, 서버에 요청 url 설정을 해준다 ( `http://localhost:8080/api/v1/auth/login`)

(localhos:8080을 암호화 하여 변수로 담아서 사용도 가능하지만 생략)

 

그 후 return 구문에 async / await 방식으로 fecth로 데이터를 넘겨준다. (메서드, 헤더, 바디 설정)

body에는 id값인 email과 password를 body에 담아줬고, JSON.stringfy 는 객체를 json 문자열 형식으로 바꿔주는 메소드이고 적용해줘야 서버에서 받을 수 있다.

export const callLoginAPI = ({ form }) => {
    const loginURL = `http://localhost:8080/api/v1/auth/login`
    console.log('form', form);
    return async (dispatch, getState) => {
        const result = await fetch(loginURL,{
            method : 'POST',
            headers : {
                'Content-Type': 'application/json',
                Accept: '*/*',
                'Access-Control-Allow-Origin': '*', // 모든 도멘인에서 접근할 수 있음을 의미
            },
            body : JSON.stringify({
                email : form.email,
                password : form.password
            }),
        }).then(res => res.json());

        console.log('로그인 시도 후 반환 받은 데이터 result : ', result);

        if (result.status == 200) {
            console.log('로그인 성공 result.status : ', result.status);
            window.localStorage.setItem('accessToken', result.userInfo.accessToken);
            dispatch({ type: POST_LOGIN, payload: result });
            alert(result.message);
            return true;
        } else {
            console.log('로그인 실패 : ', result.status);
            alert(result.failType);
            return false;
        }
    }
}

요청이 성공적으로 보내지면 then절에 서버에서 보낸 jwt token 및 데이터들이 담기고,

 

서버에서 받은 데이터를 window.localStorage.setItem('accessToken', result.userInfo.accessToken); 를 이용하여 세팅해준다.

이후 dispatch 요청을 보내는데 type은 미리 정의해 둔 주문서 개념으로  POST_LOGIN 에 맞는 주문서가 동작하고, 서버에서 받은 result를 payload에 담아 보내준다.

 

아래는 Redux를 사용하여 상태관리하는 MemberModules.

import { createActions , handleActions } from 'redux-actions';

// 초기값 설정
const initialState = [];

// 액션 정의 (액션 식별에 사용)
export const POST_REGISTER = 'member/POST_REGISTER';
export const POST_LOGIN = 'member/POST_LOGIN';
export const GET_MEMBER = 'member/GET_MEMBER';
 
 // 액션 생성자 자동 생성 함수
const actions = createActions({
    [POST_REGISTER] : () => {},
    [POST_LOGIN] : () => {},
    [GET_MEMBER] : () => {}
});

const memberReducer = handleActions({  // 리듀서 생성
    [POST_REGISTER] : (state, {payload}) => {
        console.log('memberReducer POST_REGISTER의 state : ', state);
        console.log('memberReducer POST_REGISTER의 {payload} : ', payload);
        return payload;
    },
    [POST_LOGIN] : (state, {payload}) => {
        console.log('memberReducer POST_LOGIN state : ', state);
        console.log('memberReducer POST_LOGIN {payload} : ', payload);
        return payload;
    },
    [GET_MEMBER] : (state, {payload}) => {
        console.log('memberReducer SET_MEMBER_LIST state : ', state);
        console.log('memberReducer SET_MEMBER_LIST {payload} : ', payload);
        return payload;
    }
}, initialState);

export default memberReducer;

 

memberReducer의 POST_LOGIN이 동작하고,  result를 담았던 payload가 return 된다.

 

(두 번째 인자로 초기 값을 넣는 이유는 처음 호출될 때 상태를 빈 배열로 하기 위함)

 

import { combineReducers } from "redux";
import productReducer from "./productReducer";
import categoryReducer from "./CategoryModuls";
import memberReducer from "./MemberModule";

export default combineReducers({
    // product: productReducer,
    category: categoryReducer,
    member : memberReducer
})

추가로 위의 memberReducer를 Reducer를 묶어서 관리하는 combineReducers에 member라는 key로 관리해주고,

import { createStore, applyMiddleware } from "redux";
import { thunk } from "redux-thunk";
import productReducer from "./redux/modules/productReducer";
import rootReducer from './redux/modules'

let store = createStore(rootReducer, applyMiddleware(thunk))

export default store;

 

스토어에 저장! (rootReducer는 combineReducers를 통해 결합된 결과)

이제 언제든 스토어에서 꺼내쓸 수 있다.


사실 최초에 '로그인' 버튼을 눌렀을 때 아래의 함수가 동작하고 dispatch로 위의 로직(서버로 요청 및 리턴)이 발동된다.

const onClickLoginHandler = async () => {
        const isLoginSuccess = await dispatch(callLoginAPI({ form }));
        if (isLoginSuccess) {
            console.log('isLoginSuccess : ', isLoginSuccess);
            // 로그인 하고 decoding 해야 함.
            const token = decodeJwt(window.localStorage.getItem("accessToken"));
            console.log('token : ', token);
            console.log('token.sub : ', token?.sub);
            if(token){
            dispatch(callGetMemberAPI({memberId : token.sub}));
            navigate("/");
            } else {
                console.error("유효하지 않은 토큰!!");
            }
        }
    }

 

만약 isLoginSuccess가 true 라면 받아온 토큰을 꺼내준다.

 

여기서 시간 소모가 많았는데, const token = decodeJwt(window.localStorage.getItem("accessToken"));  이 구문을 전역으로 설정해둬서 최초 로그인 시 토큰을 찾을 수 없다는 경고가 뜨고, 새로고침 후에는 정상적으로 로그인 되었음!!

 

아무리 리액트가 왔다갔다 한다고 해도 흐름이란 게 있는건데.... 마음만 급했던 것 같다.


무튼... 토큰이 잘 출력되는지 확인하고, 만약 토큰이 있다면 dispatch(callGetMemberAPI({memberId : token.sub}));  callGetMemberAPI로 token.sub(memberId)를 담아 호출해준다.

 

export const callGetMemberAPI = ({memberId}) => {
    const memberRequestURL = `http://localhost:8080/api/v1/member/${memberId}`;

    return async (dispatch, getState) => {
        const result = await fetch(memberRequestURL,{
            method : 'GET',
            headers: {
                'Content-Type' : 'application/json',
                Accept: '*/*',
                Authorization : 'Bearer' + window.localStorage.getItem('accessToken'),
            },
        }).then((res) => res.json());

        console.log('callGetMemberAPI result : ', result);

        dispatch({type: GET_MEMBER, payload:result});
    }
}

이 구문은 받아온 토큰의 memberId를 이용하여 로그인한 회원의 전체 정보를 호출하는 구문이다.

 

받아온 데이터 결과를 다시 dispatch({type: GET_MEMBER, payload:result}); 호출한다.

 

검증 결과, result key 값에 회원 정보가 담겨있다.

 

이제 더 필요한 정보가 있으면 서버에서 추가하면 되고, 프론트에서는 필요한 곳에 꺼내서 사용하면 되겠다.

 

<로그인 전체 로직> ( 아직 좀 더 수정 필요!)

import './login.css'
import { useNavigate } from 'react-router-dom';
import mainLogo from '../../assets/images/mainLogo.png';
import { useEffect, useState } from 'react';
import { useSelector,useDispatch } from 'react-redux';
import { callGetMemberAPI, callLoginAPI, callMemberListAPI } from '../../apis/MemberAPI';
import decodeJwt from '../../utils/tokenUtils';

function Login() {
// useSelect로 store에 저장된 memer를 꺼내 사용하겠다~~
    const loginMember = useSelector(state => state.member);

    const [form, setForm] = useState({
        email: '',
        password: ''
    });

    const navigate = useNavigate();

    const dispatch = useDispatch();

    useEffect(() => {
        if(loginMember.state === 200){
            console.log('useEffect의 loginMember : ', loginMember);
        }
    }, [loginMember]);

    const onChangeHandler = (e) => {
        setForm({
            ...form,
            [e.target.name]: e.target.value
        });
    };

    const onClickLoginHandler = async () => {
        const isLoginSuccess = await dispatch(callLoginAPI({ form }));
        if (isLoginSuccess) {
            console.log('isLoginSuccess : ', isLoginSuccess);
            // 로그인 하고 decoding 해야 함.
            const token = decodeJwt(window.localStorage.getItem("accessToken"));
            console.log('token : ', token);
            console.log('token.sub : ', token?.sub);
            if(token){
            dispatch(callGetMemberAPI({memberId : token.sub}));
            navigate("/");
            } else {
                console.error("유효하지 않은 토큰!!");
            }
        }
    }

    return (
        <>
            <div>
                <div className="loginLayout">
                    <div className="loginContainer">
                        <div className="loginMainLogo">
                            <img src={mainLogo} alt="메인 로고" onClick={() => navigate('/')} />
                        </div>
                        <div className="loginForm">
                            <label style={{ fontWeight: 'bold' }}> 환영합니다 고객님! </label>

                            <div className="loginInput">
                                <input
                                    type="text"
                                    placeholder='아이디 (이메일)'
                                    name="email"
                                    onChange={onChangeHandler}
                                />
                                <input
                                    type="password"
                                    placeholder='비밀번호 입력'
                                    name="password"
                                    onChange={onChangeHandler}
                                />
                            </div>

                            <div className="loginBtn">
                                <button
                                    onClick={onClickLoginHandler}
                                >로그인
                                </button>
                            </div>
                        </div>
                        <div onClick={() => navigate('/signup')}>이메일로 회원가입</div>
                    </div>
                </div>
            </div>
        </>
    );
}

export default Login;

 

 

개발할 때 어떤 걸 우선적으로 할지 생각하기!

(어떤 데이터가 필요한지, 그러면 데이터를 먼저 담을지 등등...)

 

오늘도 고생 많았다.

반응형