BCrypt로 비밀번호 암호화 및 검증
Spring Data JPA와 MySQL 사용
CORS 문제 해결
```sql
CREATE TABLE user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL
);
```
<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>
<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();
}
}
<User.java>
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;
}
<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>
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;
}
}
passwordEncoder.encode(rawPassword)
사용하여 암호화 후 저장passwordEncoder.matches(rawPassword, encodedPassword)
사용하여 비교<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 ? "로그인 성공!" : "로그인 실패!";
}
}
<회원가입>
<로그인>
Axios를 사용한 HTTP 요청
로그인 UI 및 상태 관리
로그인 성공 시 토큰 저장 및 페이지 이동
<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;
}
};
<Login.tsx>
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;
<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;
<App.tsx>
{/* ✅ 로그인 & 회원가입 추가 */}
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<로그인>
<회원가입>
@CrossOrigin(origins = "http://localhost:3000")
@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);
}
};
}
}