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