AWS Back Day 77. "Spring Boot와 프론트엔드 연동: 회원가입, 로그인, 전역 상태관리

이강용·2023년 4월 20일
0

Spring Boot

목록 보기
12/20

회원가입

UserRepository (추가)

package com.toyproject.bookmanagement.repository;

import org.apache.ibatis.annotations.Mapper;

import com.toyproject.bookmanagement.entity.Authority;
import com.toyproject.bookmanagement.entity.User;

@Mapper
public interface UserRepository {
	// 이메일 중복확인
	public User findUserByEmail(String email);
	
	// 유저 등록
	public int saveUser (User user);
	public int saveAuthority(Authority authority);
	
}

UserMapper.xml (추가)

<insert id="saveUser" 
	parameterType="com.toyproject.bookmanagement.entity.User"
	useGeneratedKeys="true"
	keyProperty="userId">
		insert into user_tb
		values (0, #{email},#{password},#{name},#{provider})
	</insert>
	
	<insert id="saveAuthority" parameterType="com.toyproject.bookmanagement.entity.Authority">
		insert into authority_tb
		values (0, #{userId}, #{roleId})
	</insert>

AuthenticationService(추가)

package com.toyproject.bookmanagement.service;

import org.springframework.stereotype.Service;

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 lombok.RequiredArgsConstructor;



@Service
@RequiredArgsConstructor
public class AuthenticationService {
	
	private final UserRepository userRepository;
	
	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());
		
	}
}

회원가입

MySQL (DB 자동 저장) - 회원가입 완료

Front

  • 로그인 페이지로 이동 (추가)
  • alert창 팝업

Register.js (추가)

const Register = () => {

    const navigate = useNavigate();

    const [registerUser, setRegisterUser]  = useState( { email:"",password:"",name:"" } );
    const [errorMessages, setErrorMessages]  = useState( { email: "", password: "" , name: "" } );
   
    const onChangeHandle = (e) => {
        const { name, value } = e.target;
        setRegisterUser( { ...registerUser, [name]:value } );
    }

    const registeSubmit = async() => {
        const data = {
            ...registerUser
        }

        const option = {
            headers: {
                "Content-Type" : "application/json"
            }
        }

        try{
            await axios.post("http://localhost:8080/auth/signup", JSON.stringify(data), option);
            setErrorMessages({email: "", password: "" , name: ""}); //빈값 ( 로그인 성공 시, error 메시지 뜨지않음 )
            alert("회원가입 성공!");
            navigate("/login");
            
        }catch(error){
            setErrorMessages({email: "", password: "" , name: "",...error.response.data.errorData}); //객체 (error.response.data.errorData)
        }
    }

로그인 구현

Front

Login.js (수정)

/** @jsxImportSource @emotion/react */
import { css } from '@emotion/react'
import React, { useState } from 'react';

import LoginInput from '../../components/UI/Login/LoginInput/LoginInput';
import { FiUser, FiLock } from 'react-icons/fi';
import { Link } from 'react-router-dom';
import { BsGoogle } from 'react-icons/bs';
import { SiNaver,SiKakao } from 'react-icons/si';
import axios from 'axios';
;


const container = css`
    display: flex;
    flex-direction: column;
    align-items: center;
    padding: 70px 30px;
`;

const logo = css`
    margin: 50px 0px;
    font-size: 34px;
    font-weight: 600;

`;


const mainContainer = css`
    display: flex;
    flex-direction: column;
    align-items: center;
    border: 1px solid #dbdbdb;
    border-radius: 10px;
    padding: 40px 20px;
    width: 400px;
`;

const authForm = css`
  width: 100%;

`;

const inputLabel = css`
    margin-left: 5px;
    font-size: 12px;
    font-weight: 600;
`;

const forgotPassword = css`
    display: flex;
    justify-content: flex-end;
    align-content: center;
    margin-bottom: 45px;
    width: 100%;
    font-size: 12px;
    font-weight: 600;
    
`;

const loginButton = css`
    margin: 10px 0px ;
    border: 1px solid #dbdbdb;
    border-radius: 7px;
    width: 100%;
    height: 50px;
    background-color: white;
    font-weight: 900;
    cursor: pointer;
    &:hover {
        background-color: #fafafa;
    }
    &:active {
        background-color: #eee;
    }
`;

const oauth2Container = css`
    display: flex;
    justify-content: center;
    align-items: center;
    margin: 20px;
    width: 100%;
`;

const oauth2 = (provider) => css`
    display: flex;
    justify-content: center;
    align-items: center;
    margin: 0px 10px;
    
    border: 1px solid ${provider === "google" ? "#0075ff" : provider === "naver" ? "#19ce60":  "#ffdc00"};
    border-radius: 50%;
    width: 50px;
    height: 50px;
    font-size: ${provider === "kakao" ? "30px" : "20px"};
    cursor: pointer;
    &:hover {
        background-color: ${provider === "google" ? "#0075ff" : provider === "naver" ? "#19ce60":  "#ffdc00"};
    }
    
    
`;

const signupMessage = css`
    margin-top: 20px;
    font-size: 14px;
    font-weight: 600;
    color: #777;
`;

const register = css`
    margin-top: 10px;
    font-weight: 600;
`;

const errorMsg = css`
    margin-left: 5px;
    margin-bottom: 20px;
    font-size: 12px;
    color:red;
`;



const Login = () => {

    

    const [loginUser, setLoginUser] = useState({ email:"",password:"" ,name: ""});
    const [errorMessages, setErrorMessages]  = useState( { email: "", password: "" ,name: "" } );

    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: "" ,  });
            console.log(response.data);
    
        }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;

Back

config
WebMvcConfig

package com.toyproject.bookmanagement.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
	
	@Override
	public void addCorsMappings(CorsRegistry registry) {
		
		// `/**` : 모든 요청
		// .allowedMethods("*") : 모든 메서드에서 열어준다
		// .allowedOrigins("http://localHost:3000") : 해당 port에서 오는 요청을
		registry.addMapping("/**")
				.allowedMethods("*")
				.allowedOrigins("*"); // test시 , 모든 port 개방
//				.allowedOrigins("http://localHost:3000");
	}
}

service

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

dto > auth

JwtRespDto

package com.toyproject.bookmanagement.dto.auth;

import lombok.Builder;
import lombok.Data;

@Builder
@Data
public class JwtRespDto {
	private String grantType;
	private String accessToken;
}

security

PrincipalUser

package com.toyproject.bookmanagement.security;

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

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.toyproject.bookmanagement.entity.Authority;

import lombok.Builder;
import lombok.Getter;

@Builder
@Getter
public class PrincipalUser implements UserDetails {
	
	
	private static final long serialVersionUID = 3893676052625302075L;
	
	
	private int userId;
	private String email;
	private String password;

	private List<Authority> authorities;
	

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		
		List<SimpleGrantedAuthority> authorrities= new ArrayList<>();
		
		this.authorities.forEach(authority -> {
			authorrities.add(new SimpleGrantedAuthority(authority.getRole().getRoleName()));
		});
		
		return authorrities;
	}

	@Override
	public String getPassword() {
		
		return password; //암호화 된 비밀번호 
	}

	@Override
	public String getUsername() {
		
		return email;
	}

	@Override
	public boolean isAccountNonExpired() {
		
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		
		return true;
	}

	@Override
	public boolean isEnabled() {
		
		return true;
	}

}

entity

User(추가)

package com.toyproject.bookmanagement.entity;

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

import com.toyproject.bookmanagement.security.PrincipalUser;

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

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {
	private int userId;
	private String email;
	private String password;
	private String name;
	private String provider;
	
	private List<Authority> authorities;
	
	
	public PrincipalUser toPrincipal() {
		
		List<String> roles = new ArrayList<>();
		
		authorities.forEach(authority ->{
			roles.add(authority.getRole().getRoleName());
		});
		
		return PrincipalUser.builder()
				.userId(userId)
				.email(email)
				.password(password)
				.authorities(authorities)
				.build();
	}
}

MVN Token 관련 의존성 추가

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-api</artifactId>
  <version>0.11.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-impl</artifactId>
  <version>0.11.5</version>
  <scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-jackson</artifactId>
  <version>0.11.5</version>
  <scope>runtime</scope>
</dependency>

security

JwtTokenProvider(핵심)

package com.toyproject.bookmanagement.security;

import java.security.Key;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import com.toyproject.bookmanagement.dto.auth.JwtRespDto;
import com.toyproject.bookmanagement.entity.Authority;
import com.toyproject.bookmanagement.exception.CustomException;

import lombok.extern.slf4j.Slf4j;
import io.jsonwebtoken.Claims;
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;

@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 Authentication getAuthentication(String accessToken) {
		
		Claims claims = parseClaims(accessToken);
		
		Object roles = claims.get("auth");
		
		if(roles == null) {
			throw new CustomException("권한 정보가 없는 토큰입니다.");
		}
		
		List<SimpleGrantedAuthority> authorities = new ArrayList<>();
		String[] rolesArray = roles.toString().split(",");
		Arrays.asList(rolesArray).forEach(role -> {
			
			authorities.add(new SimpleGrantedAuthority(role));
		});
		
		UserDetails userDetails = new User(claims.getSubject(),"",authorities);
		
		return new UsernamePasswordAuthenticationToken(userDetails,"",authorities);
	}
	
	private Claims parseClaims(String accessToken) {
		try {
			return Jwts.parserBuilder()
					.setSigningKey(key)
					.build()
					.parseClaimsJws(accessToken)
					.getBody();
		}catch (ExpiredJwtException e) {
			return e.getClaims();
		}
		
	}
}

controller
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.CrossOrigin;
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.dto.response.DataResponseDto;
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(DataResponseDto.of(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);
	}
}

로그인 결과

Front localStorage 저장

Login.js

코드를 입력하세요

pages > Main

Main.js

코드를 입력하세요

서브 라우터

components > Routes > AuthRoute

AuthRoute.js

코드를 입력하세요

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

npm install recoil 설치

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, atom, selector } from 'recoil';

export const authenticated = atom({
  key: "authenticated",
  default: false
})

export const authenticatedState = selector( { 
  key: "authenticatedState",
  get: ({ get }) => {
    const auth = get(authenticated);
    return auth;
  }
 })

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <RecoilRoot>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </RecoilRoot>
  </React.StrictMode>
);

reportWebVitals();

Login.js (추가)

코드를 입력하세요
profile
HW + SW = 1

0개의 댓글