BCrypt 인증과 CORS 해결

📌 백엔드 구현 단계

BCrypt로 비밀번호 암호화 및 검증
Spring Data JPA와 MySQL 사용
CORS 문제 해결

1. DB 생성

```sql
CREATE TABLE user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL
);
```

2. 의존성 추가

<pom.xml>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-crypto</artifactId>
</dependency>

3. Security 설정

<SecurityConfig.java>

package com.dearu.backend.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable()  // CSRF 비활성화
                .authorizeHttpRequests()
                .requestMatchers("/api/users/**", "/api/invitations/**").permitAll()  // 사용자, 초대장 API는 인증 없이 접근 허용
                .anyRequest().authenticated()
                .and()
                .formLogin().disable(); // 기본 로그인 폼 비활성화
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

4. 회원 엔티티 및 DTO 작성

<User.java>

  • BCrypt를 사용하여 비밀번호 암호화 및 검증
package com.dearu.backend.model;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Entity
@Table(name = "user")
@Getter
@Setter
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String username;

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

<UserDTO.java>

package com.dearu.backend.dto;

import lombok.Data;

@Data
public class UserDTO {
    private String username;
    private String password;
}

5. 회원 저장 및 조회

<UserRepository.java>

package com.dearu.backend.repository;

import com.dearu.backend.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username);
}

<UserService.java>

  • BCrypt로 비밀번호를 검증
package com.dearu.backend.service;

import com.dearu.backend.model.User;
import com.dearu.backend.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    // 회원가입
    public User register(User user) {
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        return userRepository.save(user);
    }

    // 로그인
    public boolean login(String username, String rawPassword) {
        User user = userRepository.findByUsername(username);
        if (user != null) {
            return passwordEncoder.matches(rawPassword, user.getPassword());
        }
        return false;
    }
}

6. BCrypt를 이용한 비밀번호 암호화 및 비교

  • 회원가입: passwordEncoder.encode(rawPassword) 사용하여 암호화 후 저장
  • 로그인: passwordEncoder.matches(rawPassword, encodedPassword) 사용하여 비교

7. 로그인 API

<UserController.java>

package com.dearu.backend.controller;

import com.dearu.backend.dto.UserDTO;
import com.dearu.backend.model.User;
import com.dearu.backend.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@CrossOrigin(origins = "http://localhost:3001")
@RestController
@RequestMapping("/api/users")
public class UserController {

    @Autowired
    private UserService userService;

    // 회원가입
    @PostMapping("/register")
    public String register(@RequestBody UserDTO userDTO) {
        User user = new User();
        user.setUsername(userDTO.getUsername());
        user.setPassword(userDTO.getPassword());
        userService.register(user);
        return "회원가입 성공!";
    }

    // 로그인
    @PostMapping("/login")
    public String login(@RequestBody UserDTO userDTO) {
        boolean isAuthenticated = userService.login(userDTO.getUsername(), userDTO.getPassword());
        return isAuthenticated ? "로그인 성공!" : "로그인 실패!";
    }
}

8. PostMan Test

<회원가입>

<로그인>

📌 프론트엔드 구현 단계 - 매우 간단하게 구현함

Axios를 사용한 HTTP 요청
로그인 UI 및 상태 관리
로그인 성공 시 토큰 저장 및 페이지 이동

1. API 연결 설정

<api.ts>

// 회원가입
export const registerUser = async (userData: { username: string; password: string }) => {
    try {
        const response = await axios.post(`${API_BASE_URL}/users/register`, userData, {
            headers: {
                "Content-Type": "application/json"
            },
            withCredentials: true
        });
        return response.data;
    } catch (error) {
        console.error("Error registering user:", error);
        throw error;
    }
};

// 로그인
export const loginUser = async (userData: { username: string; password: string }) => {
    try {
        const response = await axios.post(`${API_BASE_URL}/users/login`, userData, {
            headers: {
                "Content-Type": "application/json"
            },
            withCredentials: true
        });
        return response.data;
    } catch (error) {
        console.error("Error logging in:", error);
        throw error;
    }
};

2. Login 컴포넌트 작성

<Login.tsx>

  • 로그인 성공 시 토큰을 Local Storage에 저장
import React, { useState } from "react";
import { loginUser } from "../services/api";
import { useNavigate } from "react-router-dom";

const Login: React.FC = () => {
    const [username, setUsername] = useState("");
    const [password, setPassword] = useState("");
    const navigate = useNavigate();

    const handleLogin = async (event: React.FormEvent) => {
        event.preventDefault();
        try {
            const response = await loginUser({ username, password });
            alert(response); // 로그인 성공 메시지
            navigate("/");   // 로그인 성공 시 홈으로 이동
        } catch (error) {
            alert("로그인 실패. 다시 시도하세요.");
        }
    };

    return (
        <div className="login-container">
            <h2>로그인</h2>
            <form onSubmit={handleLogin}>
                <input
                    type="text"
                    placeholder="아이디"
                    value={username}
                    onChange={(e) => setUsername(e.target.value)}
                    required
                />
                <input
                    type="password"
                    placeholder="비밀번호"
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                    required
                />
                <button type="submit">로그인</button>
            </form>
            <p>
                계정이 없으신가요? <a href="/register">회원가입</a>
            </p>
        </div>
    );
};

export default Login;

3. Register 컴포넌트 작성

<Register.tsx>

import React, { useState } from "react";
import { registerUser } from "../services/api";
import { useNavigate } from "react-router-dom";

const Register: React.FC = () => {
    const [username, setUsername] = useState("");
    const [password, setPassword] = useState("");
    const navigate = useNavigate();

    const handleRegister = async (event: React.FormEvent) => {
        event.preventDefault();
        try {
            const response = await registerUser({ username, password });
            alert(response); // 회원가입 성공 메시지
            navigate("/login"); // 회원가입 후 로그인 페이지로 이동
        } catch (error) {
            alert("회원가입 실패. 다시 시도하세요.");
        }
    };

    return (
        <div className="register-container">
            <h2>회원가입</h2>
            <form onSubmit={handleRegister}>
                <input
                    type="text"
                    placeholder="아이디"
                    value={username}
                    onChange={(e) => setUsername(e.target.value)}
                    required
                />
                <input
                    type="password"
                    placeholder="비밀번호"
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                    required
                />
                <button type="submit">회원가입</button>
            </form>
            <p>
                이미 계정이 있으신가요? <a href="/login">로그인</a>
            </p>
        </div>
    );
};

export default Register;

4. React Router 설정

<App.tsx>

  • react-router-dom을 사용하여 페이지 이동
{/* ✅ 로그인 & 회원가입 추가 */}
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />

5. 화면 테스트

<로그인>


<회원가입>


6. CORS 문제 해결 (Spring Boot)

  1. Controller 에 @CrossOrigin 어노테이션 사용
@CrossOrigin(origins = "http://localhost:3000")
  1. WebConfig.java에서 글로벌 설정
@Configuration
public class WebConfig {

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                        .allowedOrigins("http://localhost:3000")
                        .allowedMethods("GET", "POST", "PUT", "DELETE")
                        .allowCredentials(true);
            }
        };
    }
}
profile
개발감자

0개의 댓글

Powered by GraphCDN, the GraphQL CDN