AWS Back Day 78. "Spring Boot 전역 상태 관리와 비동기 데이터 처리"

이강용·2023년 4월 24일

Spring Boot

목록 보기
13/20

Front

전역 상태 관리(Recoil) 라이브러리 (동기)

atoms > Auth

AuthAtoms.js

import { atom } from "recoil";

export const authenticatedState = atom({
    key: "authenticatedState",
    default: false

});

Login.js (추가)

const Login = () => {

    const [loginUser, setLoginUser] = useState({ email:"",password:"" ,name: ""});
    const [errorMessages, setErrorMessages] = useState( { email: "", password: "" ,name: "" } );
    const [authenticated, setAuthenticated] = useRecoilState( authenticatedState ); 
    //useRecoilState가 authenticated에 상태가 저장됨
    const navigate = useNavigate();

    const handleChange = (e) => {
        const { name, value } = e.target;
        setLoginUser({ ...loginUser, [name]:value });

    }

    const loginHandleSubmit = async() => {
        
        const option = {
            headers: {
                "Content-Type" : "application/json"
            }
        }
        try{
            const response = await axios.post("http://localhost:8080/auth/login", JSON.stringify(loginUser),option);
            setErrorMessages({email: "", password: "" ,  });
            const accessToken = response.data.grantType + " " + response.data.accessToken;
            localStorage.setItem("accessToken", accessToken);
            setAuthenticated(true);
            navigate("/");

        }catch(error){
           
            setErrorMessages({email: "", password: "",  ...error.response.data.errorData});
        }
    }




    return (
        <div css= {container}>
            <header>
                <h1 css= { logo } >Login</h1>
            </header>
            <main css={ mainContainer }>
                <div css={authForm}>
                    <label css={ inputLabel }>Email</label>
                    <LoginInput type="email" placeholder="Type your email" onChange={handleChange} name="email" >
                        <FiUser />
                    </LoginInput>
                    <div css={errorMsg}>{errorMessages.email}</div>
                    <label css={ inputLabel }>Password</label>
                    <LoginInput type="password" placeholder="Type your password" onChange={handleChange} name="password" >
                        <FiLock />
                    </LoginInput>
                    <div css={errorMsg}>{errorMessages.password}</div>
                    <div css= { forgotPassword }><Link to="/forgot/password">Forgot Password?</Link></div>
                    <button css={ loginButton } onClick={loginHandleSubmit}>LOGIN</button>
                </div>

                
                <div></div>
                
            </main>

            <div css = { signupMessage }>Or Sign Up Using</div>

            <div css= {oauth2Container}>
                <div css={ oauth2("google") }><BsGoogle /></div>
                <div css={ oauth2("naver") }><SiNaver /></div>
                <div css={ oauth2("kakao") }><SiKakao /></div>
            </div>

            <div css= { signupMessage }>Or Sign Up Using</div>

            <footer>
                <div css = { register }><Link to="/register">SIGN UP</Link></div>
            </footer>
        </div>
    );
};

export default Login;

AuthRoute.js (수정)

코드를 입력하세요

App.js (수정)

import { Global } from '@emotion/react';
import { Reset } from './styles/Global/reset';
import { Route, Routes } from 'react-router-dom';
import Login from './pages/Login/Login';
import Register from './pages/Register/Register';
import Main from './pages/Main/Main';
import AuthRoute from './components/UI/Routes/AuthRoute/AuthRoute';


function App() {

  

  return (
    <>
      <Global styles={ Reset }></Global>
      <Routes>
        <Route exact path="/login" element={<AuthRoute path="/login" element={<Login />} />} />
        <Route path="/register" element={<AuthRoute path="/register" element={<Register />} /> } />
        <Route path="/" element={<AuthRoute path="/" element={<Main />} />
        }/>
      </Routes>

    </>
  );
}

export default App;

Back

AuthenticationController(추가)

package com.toyproject.bookmanagement.controller;

import javax.validation.Valid;

import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.toyproject.bookmanagement.aop.annotation.ValidAspect;
import com.toyproject.bookmanagement.dto.auth.LoginReqDto;
import com.toyproject.bookmanagement.dto.auth.SignupReqDto;
import com.toyproject.bookmanagement.service.AuthenticationService;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/auth") 
@RequiredArgsConstructor
public class AuthenticationController  {
	
	private final AuthenticationService authenticationService;
	
	@ValidAspect
	@PostMapping("/login")
	public ResponseEntity<?> login(@Valid @RequestBody LoginReqDto loginReqDto, BindingResult bindingResult) {
		return ResponseEntity.ok().body(authenticationService.signin(loginReqDto));
	}
	
	
	@ValidAspect
	@PostMapping("/signup")
	public ResponseEntity<?> signup(@Valid @RequestBody SignupReqDto signupReqDto, BindingResult bindingResult) {
		authenticationService.checkDuplicatedEmail(signupReqDto.getEmail());
		authenticationService.signup(signupReqDto);
		return ResponseEntity.ok().body(true);
	}
	
	
	@GetMapping("/authenticated")
	public ResponseEntity<?> authenticated(String accessToken){
		
		return ResponseEntity.ok().body(authenticationService.authenticated(accessToken)); //true, false
	}
}

JwtTokenProvider(추가)

package com.toyproject.bookmanagement.security;

import java.security.Key;
import java.util.Date;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import com.toyproject.bookmanagement.dto.auth.JwtRespDto;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class JwtTokenProvider {
	private final Key key;
	
	public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) {
	
		key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));
	}
	
	public JwtRespDto generateToken(Authentication authentication) {
		
		StringBuilder builder = new StringBuilder();
		
		authentication.getAuthorities().forEach(authority -> {
			builder.append(authority.getAuthority() +",");
		});
		
		builder.delete(builder.length() -1, builder.length()); // 마지막 쉼표 삭제 
		
		
		String authorities = builder.toString();
		Date tokenExpiresDate = new Date(new Date().getTime()+ (1000 * 60 * 60 * 24)); // 현재시간 + 하루 
		
		
		
		String accessToken = Jwts.builder()
				.setSubject(authentication.getName()) 				// 토큰의 이름 (email)
				.claim("auth", authorities)             			// auth
				.setExpiration(tokenExpiresDate) 					// 토큰 만료 시간
				.signWith(key, SignatureAlgorithm.HS256) 			// 토큰 암호화
				.compact();
		
		return JwtRespDto.builder()
				.grantType("Bearer")
				.accessToken(accessToken)
				.build();
	}
	
	public boolean validateToken(String token) {
		try {
			Jwts.parserBuilder()
			.setSigningKey(key)
			.build()
			.parseClaimsJws(token);
			
			return true;
			
		}catch(SecurityException | MalformedJwtException e) {
			// Security 라이브러리에 오유가 있거나, JSON의 포맷이 잘못된 형식의 JWT가 들어왔을 때 예외
			// SignatureException이 포함되어 있음
			log.info("Invalid JWT Token", e);
		}catch (ExpiredJwtException e) {
			// 토큰의 유효기간이 만료된 경우의 예외
			log.info("Expired JWT Token", e);
		}catch (UnsupportedJwtException e) {
			// jwt의 형식을 지키지 않은 경우 (Header.Payload.Signature)
			log.info("Unsupported JWT Token", e);
		}catch (IllegalArgumentException e) {
			// jwt 토큰이 없을때
			log.info("IllegalArgument JWT Token", e);
		}catch (Exception e) {
			log.info("JWT Token", e);
		}
		return false;
	}
	
	
	public String getToken(String token) {
		String type = "Bearer";
		if(StringUtils.hasText(token) && token.startsWith(type)) {
			return token.substring(type.length()+1);
		}
		return null;
	}
}

AuthenticationService(추가 & 수정)

package com.toyproject.bookmanagement.service;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.toyproject.bookmanagement.dto.auth.JwtRespDto;
import com.toyproject.bookmanagement.dto.auth.LoginReqDto;
import com.toyproject.bookmanagement.dto.auth.SignupReqDto;
import com.toyproject.bookmanagement.entity.Authority;
import com.toyproject.bookmanagement.entity.User;
import com.toyproject.bookmanagement.exception.CustomException;
import com.toyproject.bookmanagement.exception.ErrorMap;
import com.toyproject.bookmanagement.repository.UserRepository;
import com.toyproject.bookmanagement.security.JwtTokenProvider;

import lombok.RequiredArgsConstructor;



@Service
@RequiredArgsConstructor
public class AuthenticationService implements UserDetailsService {
	
	private final UserRepository userRepository;
	private final AuthenticationManagerBuilder authenticationManagerBuilder;
	private final JwtTokenProvider jwtTokenProvider;
	
	public void checkDuplicatedEmail(String email) {
		
		User userEntity = userRepository.findUserByEmail(email);
		if(userEntity != null) {
			
			throw new CustomException("Duplicated Email",
					ErrorMap.builder().put("email", "사용중인 이메일입니다.").build());
			
		}
	}
	
	public void signup(SignupReqDto signupReqDto) {
		
		User userEntity = signupReqDto.toEntity();
		userRepository.saveUser(userEntity);
		userRepository.saveAuthority(Authority.builder()
				.userId(userEntity.getUserId())
				.roleId(1)
				.build());
		
	}
	
	public JwtRespDto signin(LoginReqDto loginReqDto) {
		
		//AuthenticationManagerBuilder가 알아보게 하기 위함 (입력한 email , password와 DB에 저장된 email, password를 비교)
		UsernamePasswordAuthenticationToken authenticationToken =
				new UsernamePasswordAuthenticationToken(loginReqDto.getEmail(), loginReqDto.getPassword()); //암호화 안된 비밀번호
		// 이까지만 해도 로그인 성공 
		Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
		
		return jwtTokenProvider.generateToken(authentication);
		
	}

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		
		User userEntity = userRepository.findUserByEmail(username);
		
		if(userEntity == null) {
			throw new CustomException("로그인 실패",ErrorMap.builder().put("email", "사용자 정보를 확인하세요").build());
		}
		
		
		return userEntity.toPrincipal();
	}
	
	
	public boolean authenticated(String accessToken) {
		return jwtTokenProvider.validateToken(jwtTokenProvider.getToken(accessToken));
	}
}

로그인됐을때 로그인창, 회원가입 창 막기

App.js (수정)

import { Global } from '@emotion/react';
import { Reset } from './styles/Global/reset';
import { Route, Routes } from 'react-router-dom';
import Login from './pages/Login/Login';
import Register from './pages/Register/Register';
import Main from './pages/Main/Main';
import AuthRoute from './components/UI/Routes/AuthRoute/AuthRoute';


function App() {

  

  return (
    <>
      <Global styles={ Reset }></Global>
      <Routes>
        <Route exact path="/login" element={<AuthRoute path="/login" element={<Login />} />} />
        <Route path="/register" element={<AuthRoute path="/register" element={<Register />} /> } />
        <Route path="/" element={<AuthRoute path="/" element={<Main />} />
        }/>
      </Routes>

    </>
  );
}

export default App;

react-query 라이브러리 설치 (비동기)

npm i react-query

index.js (추가)

import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { RecoilRoot} from 'recoil';
import { QueryClient, QueryClientProvider } from 'react-query';



const queryClient = new QueryClient();


const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <RecoilRoot>
      <QueryClientProvider client={queryClient}>

      <BrowserRouter>
        <App />
      </BrowserRouter>
    </QueryClientProvider>
    </RecoilRoot>
  </React.StrictMode>
);

reportWebVitals();

api > auth

authApi.js

import axios from "axios"

export const getAuthenticated = (accessToken) => {
    return axios.get( "http://localhost:8080/auth/authenticated", {params: { accessToken }});
}

AuthRouteReactQuery.js

import axios from "axios";
import React, { useEffect, useState }  from "react";
import { useQuery } from "react-query";
import { Navigate } from "react-router-dom";
import { refreshState } from "../../../../atoms/Auth/AuthAtoms";
import { useRecoilState } from "recoil";


const AuthRouteReactQuery =  ({ path, element }) => {

    const [ refresh, setRefresh ]  = useRecoilState(refreshState);

    const { data, isLoading } = useQuery(["authenticated"], async() =>{
        const accessToken = localStorage.getItem("accessToken");
        const response = await axios.get("http://localhost:8080/auth/authenticated", {params: { accessToken }});
        return response;
    },{
        enabled: refresh
    });

    useEffect(() => {
        if(!refresh){
            setRefresh(true);
        }
    },[refresh]);

   
    if(isLoading){
        return (<div>로딩중...</div>);
    }


    if(!isLoading){
        const permitAll = ["/login", "/register", "/password/forgot"];

        if(!data.data) {
            if(permitAll.includes(path)){
                return element;
            }
            return <Navigate to="/login" />;
        }
        if(permitAll.includes( path )){
            return <Navigate to="/" />;
        }

    }

    return element;
};

export default AuthRouteReactQuery;

AuthAtoms.js

import { atom } from "recoil";


export const refreshState = atom({
    key: "refreshState",
    default: true
});

export const authenticatedState = atom({
    key: "authenticatedState",
    default: false

});
  • AuthRouteReactQuery -> Token의 유효성검사
  • 비동기를 순차적으로 쓰기위해 axios (promise) 를 쓴다.
    • 순차적 -> 동기 , 동시적 -> 비동기
profile
HW + SW = 1

0개의 댓글