본문 바로가기
개발/Spring

[SpringBoot] React + TypeScript + JPA 프로젝트 (4) - 회원가입, 로그인 하기

by 코딩하는 흰둥이 2024. 11. 1.

이전글

https://greed-yb.tistory.com/305

 

[SpringBoot] React + TypeScript + JPA 프로젝트 (3) - SpringSecurity 적용

이전글https://greed-yb.tistory.com/304 [SpringBoot] React + TypeScript + JPA 프로젝트 (2) - Bootstrap 적용이전글https://greed-yb.tistory.com/303 [SpringBoot] React + TypeScript + JPA 프로젝트 (1) - 생성Java 17GradleSprinb Boot 3.3.4Spri

greed-yb.tistory.com

 

 

 

 

application.properties
# 쿼리문을 보여준다
spring.jpa.show-sql=true

# 쿼리문을 가독성 있게 바꿔준다
spring.jpa.properties.hibernate.format_sql=true

JPA 를 적용한 상태고 쿼리를 Log 에 찍히게 한다

 

 

 

 

 

회원가입 페이지

https://greed-yb.tistory.com/301

 

[SpringBoot] Intellij + React + Bootstrap + Router 적용하기

이전글https://greed-yb.tistory.com/300 [SpringBoot] Intellij + React 생성 및 한번에 빌드하기node.js 설치하기https://nodejs.org/ Node.js — Run JavaScript EverywhereNode.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine

greed-yb.tistory.com

이전글을 참고하여 Bootstrap 에 있던 register.html 을 register.tsx 파일로 만들었다

 

import React, {useState} from "react";
import axios from "axios";
import {Link} from "react-router-dom";

function Register() {

    // 변수 선언
    const [name, setName] = useState('');
    const [email, setEmail] = useState('');
    const [username, setUsername] = useState('');
    const [password, setPassword] = useState('');

    // 회원가입 
    const handleRegister = async (e: { preventDefault: () => void; }) => {
        e.preventDefault();
        try {
            const response = await axios.post('http://localhost:7172/api/register', {
                name,
                email,
                username,
                password,
            }, {withCredentials: true }); // 쿠키를 자동 입력해 준다
            if(response.data.message == "success"){
                // 회원가입하면 로그인 페이지로
                alert("회원가입 되셨습니다.");
                window.location.href = '/signin';
            }else if(response.data.message == "duplication") {
                alert("이미 가입된 ID 입니다");
            }else {
                console.error('register failed');
            }
        } catch (error) {
            console.error('register error:', error);
        }
    };



    return(
        <main>
            <div className="container">

                <section
                    className="section register min-vh-100 d-flex flex-column align-items-center justify-content-center py-4">
                    <div className="container">
                        <div className="row justify-content-center">
                            <div
                                className="col-lg-4 col-md-6 d-flex flex-column align-items-center justify-content-center">

                                <div className="d-flex justify-content-center py-4">
                                    <a href="index.html" className="logo d-flex align-items-center w-auto">
                                        <img src="assets/img/logo.png" alt=""/>
                                            <span className="d-none d-lg-block">NiceAdmin</span>
                                    </a>
                                </div>

                                <div className="card mb-3">

                                    <div className="card-body">

                                        <div className="pt-4 pb-2">
                                            <h5 className="card-title text-center pb-0 fs-4">Create an Account</h5>
                                            <p className="text-center small">Enter your personal details to create
                                                account</p>
                                        </div>

                                        <form className="row g-3 needs-validation" noValidate onSubmit={handleRegister}>
                                            <div className="col-12">
                                                <label htmlFor="name" className="form-label">Your Name</label>
                                                <input type="text" name="name" className="form-control" id="name"
                                                       required onChange={(e)=> setName(e.target.value)}/>
                                                    <div className="invalid-feedback">Please, enter your name!</div>
                                            </div>

                                            <div className="col-12">
                                                <label htmlFor="email" className="form-label">Your Email</label>
                                                <input type="email" name="email" className="form-control" id="email"
                                                       required onChange={(e)=> setEmail(e.target.value)}/>
                                                    <div className="invalid-feedback">Please enter a valid Email
                                                        adddress!
                                                    </div>
                                            </div>

                                            <div className="col-12">
                                                <label htmlFor="username" className="form-label">ID</label>
                                                <div className="input-group has-validation">
                                                    <span className="input-group-text" id="inputGroupPrepend">@</span>
                                                    <input type="text" name="username" className="form-control"
                                                           id="username" required onChange={(e)=> setUsername(e.target.value)}/>
                                                        <div className="invalid-feedback">Please choose a username.
                                                        </div>
                                                </div>
                                            </div>

                                            <div className="col-12">
                                                <label htmlFor="password" className="form-label">Password</label>
                                                <input type="password" name="password" className="form-control"
                                                       id="password" required onChange={(e) => setPassword(e.target.value)}/>
                                                    <div className="invalid-feedback">Please enter your password!</div>
                                            </div>

                                            <div className="col-12">
                                                <button className="btn btn-primary w-100" type="submit">Create Account
                                                </button>
                                            </div>
                                            <div className="col-12">
                                                <p className="small mb-0">Already have an account?
                                                    <Link to="/signin">Log in</Link>
                                                </p>
                                            </div>
                                        </form>

                                    </div>
                                </div>

                                <div className="credits">
                                    Designed by <a href="https://bootstrapmade.com/">BootstrapMade</a>
                                </div>

                            </div>
                        </div>
                    </div>

                </section>

            </div>
        </main>

);
}

export default Register;

 

입력 필드의 변수들을 선언하고 

각각 태그에 onChange 함수를 , <form> 태그에 onSubmit 함수를 설정한다

 

 

 

 

 

login 페이지에서 해당 버튼을 누루면 회원가입 페이지로 이동될 수 있도록 코드를 수정한다

 

React 에 맞춰서 <a> 태그를 <Link> 태그로 변경해준다

 

 

 

 

App.tsx

이전글에서 layout 때문에 login 페이지만

분리를 하였었는데 register 페이지에도 적용을 하기 위해 변경한다

import {BrowserRouter, Route, Routes, useLocation} from "react-router-dom";
import Header from "./components/layout/header";
import Footer from "./components/layout/footer";
import Sidebar from "./components/layout/sidebar";
import Dashboard from "./components/pages/dashboard";
import Profile from "./components/pages/profile";
import Login from "./components/pages/login";
import PrivateRoute from "./components/utils/privateRoute";
import Register from "./components/pages/register";

// 페이지 layout 구분하기
function AppLayout(){
    const location = useLocation();
    const layoutPass = ["/signin" , "/register"];           // 로그인 , 회원가입 페이지 제외
    const pageCheck = layoutPass.includes(location.pathname);

    return (
            <div className="App">
                {!pageCheck && <Header/>}
                {!pageCheck && <Sidebar/>}
                <Routes>
                    <Route path="/signin" element={<Login />}/>
                    <Route path="/register" element={<Register />}/>
                    <Route path="/" element={
                    <PrivateRoute>
                        <Dashboard />
                    </PrivateRoute>
                    }/>
                    <Route path="/profile" element={
                    <PrivateRoute>
                        <Profile />
                    </PrivateRoute>
                    }/>
                </Routes>
                {!pageCheck && <Footer/>}
            </div>
    );
}


function App() {
    return (
        <BrowserRouter>
          <AppLayout/>
        </BrowserRouter>
    );
}

export default App;

 

 

layout 이 제외된 회원가입 페이지

 

 

 

 

 

 

 

UserVo
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;

@Data
@Entity
@Table(name = "usermember")
public class UserVo {

    @Id         // PK 지정
//    @GeneratedValue(strategy = GenerationType.IDENTITY) // 값 자동 생성
//    private Long id;
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String email;
    
}

@Data 는 lombok 의 어노테이션이며 Getter , Setter 를 자동완성 해준다

( 값이 증가되는 increment 의 기능이 필요하다면 

@GeneratedValue(strategy = GenerationType.IDENTITY) 을 이용하면 된다 )

 

 

 

 

UserRepository
import com.example.reactpractice.vo.userVo.UserVo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<UserVo, String> { // Entity 와 PK의 데이터 타입 지정
}

기본적인 CRUD 의 경우 따로 메서드를 작성할 필요가 없다

상속하는 JpaRepository<Entity 명 , PK 컬럼 데이터 타입 > 만 지정해 놓는다

 

 

 

ResigetController
import com.example.reactpractice.repository.user.UserRepository;
import com.example.reactpractice.vo.userVo.UserVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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 java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@RestController
@RequestMapping("/api/*")
public class RegisterController {

    @Autowired
    private UserRepository userRepository;

    @PostMapping("/register")
    public ResponseEntity<Map<String, Object>> register(@RequestBody UserVo vo) throws Exception{
        Map<String, Object> data = new HashMap<>();

        // Optional 을 사용하면 nullPoint 발생을 줄여준다
        // ID 가 있는지 select
        Optional<UserVo> idCheck = userRepository.findById(vo.getUsername());

        // ID 가 없으면 회원가입
        if(idCheck.isEmpty()){
            try{
                // 회원가입 시 비밀번호를 암호화 시킨다
                PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
                String changePassword = passwordEncoder.encode(vo.getPassword());

                vo.setPassword(changePassword);

                // 회원가입
                userRepository.save(vo);
                data.put("message", "success");

                return ResponseEntity.ok(data);
            }catch (Exception e){
                data.put("message", "fail");
                return ResponseEntity.ok(data);
            }
        }else{
            // ID 가 중복되면
            data.put("message", "duplication");
            return ResponseEntity.ok(data);
        }
    }
}

JPA 사용 시 간단한 CRUD 의 경우

따로 메서드 필요 없이

findById() - @ID 로 검색

findAll()  - 모두 검색 

save() - 저장, 수정

delete() - 삭제

...등등 

으로 가능하다

 

 

 

회원가입 - TEST

회원가입 과 ID 가 중복되었을 때

 

 

ID 중복확인을 위한 select 와 저장하는 insert

 

 

 

 

 

 

LoginController
@RestController
@RequestMapping("/api/*")
public class LoginController {

    @Autowired
    private UserRepository userRepository;

    private final JwtTokenUtil jwtTokenUtil;

    // jwtTokenUtil 에 설정한 시간과 동일한 시간으로 맞춘다
    private int accessTime = 1 * 60;  // 1분
    private int refreshTime = 5 * 60;  // 5분

    public LoginController(JwtTokenUtil jwtTokenUtil) {
        this.jwtTokenUtil = jwtTokenUtil;
    }

    @PostMapping("/login")
    public ResponseEntity<Map<String, Object>> loginCheck(@RequestBody UserVo vo, HttpServletResponse response) {
        // 반환할 데이터 구성
        Map<String, Object> data = new HashMap<>();
 
        try {

            /////////////////////////////////////////////////////////////////////
            // 세션 기반 인증 및 특정 로직일때 사용함 , 혹시 필요할까 싶어서 작성 , 지워도 무방하다
            // 사용자 인증
            Authentication authentication = new UsernamePasswordAuthenticationToken(vo.getUsername(), vo.getPassword());
            // 사용자 정보 set
            SecurityContextHolder.getContext().setAuthentication(authentication);
            /////////////////////////////////////////////////////////////////////


            // 사용자가 입력한 비밀번호와 DB에 암호화 되어 있는 사용자의 비밀번호를 비교하기 위함
            PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

            //Optional 을 사용하면 nullPoint 발생을 줄여준다
            Optional<UserVo> dbCheck = userRepository.findById(vo.getUsername());

            // 사용자가 입력한 비밀번호와 DB의 비밀번호가 일치하지 않으면 login 페이지로 이동
            if (dbCheck.isEmpty() || !passwordEncoder.matches(vo.getPassword(), dbCheck.get().getPassword())) {
                // 인증 실패 시 다시 로그인 페이지로
                data.put("message", "fail");

                return ResponseEntity.ok(data);
            }

            // JWT 토큰 생성
            String accessToken = jwtTokenUtil.generateAccessToken(vo.getUsername());
            String refreshToken = jwtTokenUtil.generateRefreshToken(vo.getUsername());

            // 쿠키에 JWT 저장
            Cookie accessTokenCookie = new Cookie("accessToken", accessToken);
            accessTokenCookie.setHttpOnly(true);
            accessTokenCookie.setDomain("localhost"); // localhost에 쿠키 설정
            accessTokenCookie.setPath("/");
            accessTokenCookie.setMaxAge(accessTime);
            accessTokenCookie.setHttpOnly(false); // javascript 로 cookie 접근 허용(false)
            response.addCookie(accessTokenCookie);

            Cookie refreshTokenCookie = new Cookie("refreshToken", refreshToken);
            refreshTokenCookie.setHttpOnly(true);
            refreshTokenCookie.setDomain("localhost"); // localhost에 쿠키 설정
            refreshTokenCookie.setPath("/");
            refreshTokenCookie.setMaxAge(refreshTime);
            refreshTokenCookie.setHttpOnly(false); // javascript 로 cookie 접근 허용(false)
            response.addCookie(refreshTokenCookie);

            data.put("accessToken", accessToken);
            data.put("refreshToken", refreshToken);
            data.put("role", "guest"); // test 용 임의 권한 지정
            data.put("message", "success");

        }catch (Exception e){
            e.printStackTrace();
        }
        return ResponseEntity.ok(data);
    }
    
    ....
    ....
    ....
}

 

Test 용으로 사용자 비밀번호를 1111 지정했던 코드를 삭제하고

JPA 를 이용하여 DB 에서 유저 정보를 가져와서

사용자가 입력한 PasswordDB의 Password비교하고 일치하면 login 하는 코드를 넣었다

 

 

 

 

 

 

login.tsx
import React, {useState} from "react";
import axios from "axios";
import {Link} from "react-router-dom";

function Login() {

    const [username, setUsername] = useState('');
    const [password, setPassword] = useState('');

    const handleLogin = async (e: { preventDefault: () => void; }) => {
        e.preventDefault();
        try {
            const response = await axios.post('http://localhost:7172/api/login', {
                username,
                password,
            }, {withCredentials: true }); // 쿠키를 자동 입력해 준다
            if(response.data.message == "success"){
                // 로그인 성공 후 홈 페이지로 이동
                window.location.href = '/';
            }else {
                alert("로그인 실패하였습니다.");
            }
        } catch (error) {
            console.error('Login failed:', error);
        }
    };
    
        return(
        ...
        ..
        ..);
        }
        export default Login;

 

 

 

로그인 - TEST

로그인 실패와 로그인 성공

 

 

 


 

입력 필드의 공백 또는 null 검증이나 if 문 또는 ResponseEntity 등

테스트를 위해 간단히 작성하거나 구현하지 않았기 때문에 참고 하길 바란다

댓글