Spring Boot/React 프로젝트를 하면서
어려웠던 개념을 다 잡기 위해 다시 하는 공부!
⭐️ 작동 순서
1. 로그인 시 스프링부트에서 시큐리티 객체를 생성하고, 세션 식별자가 있는 JSESSIONID를 쿠키에 저장 시킴
2. 인증이나 로그아웃 시 리액트에서 쿠키에 저장된 JSESSIONID를 스프링부트로 전달 (전달과정에서 withCredentials: true를 통해 요청 시 쿠키를 읽을 수 있게 스프링부트로 전달해야 함)
3. 스프링부트에선 JSESSIONID를 읽어 세션을 식별하고 관리
spring.application.name=back
spring.output.ansi.enabled=always
#MySQL 설정
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/TEST?serverTimezone=UTC&characterEncoding=UTF-8
spring.datasource.username=test
spring.datasource.password=a123
#jpa 쿼리문 확인 가능
spring.jpa.show-sql=true
# DB의 고유 기능 사용 가능
spring.jpa.hibernate.ddl-auto=update
# SQL의 가독성 높임(JPA 구현체인 Hibernate 동작)
spring.jpa.properties.hibernate.format_sql=true
# 디버그 모드 활성화
logging.level.org.springframework.security=DEBUG
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN") // /admin/** 접근은 ADMIN 권한을 가진 사용자만 가능
.antMatchers("/user/**").authenticated() // /user/** 접근은 인증된 사용자만 가능
.anyRequest().permitAll()
.and()
.formLogin() // 로그인 설정
.loginPage("/login")
.loginProcessingUrl("/loginProc") // 실제 로그인 처리 엔드 포인트
.usernameParameter("email") // form에서 email 사용
.defaultSuccessUrl("/loginOk")
.and()
.logout() // 로그아웃 설정
.logoutUrl("/logout")
.logoutSuccessUrl("/logoutOk")
.deleteCookies("JSESSIONID");
http.addFilterBefore(corsFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("http://localhost:3000");// 리액트 서버
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
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 java.util.Collections;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import com.study.back.repository.UserRepository;
import com.study.back.model.User;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
System.out.println("넘어온 이메일: " + email);
System.out.println("loadUserByUsername 실행");
// 사용자 조회, 없으면 예외 발생
User user = userRepository.findByUserEmail(email);
if (user == null) {
throw new UsernameNotFoundException("User not found with email: " + email);
}
// 사용자가 있다면 UserDetails 객체 생성
return new org.springframework.security.core.userdetails.User(
user.getUserEmail(),
user.getPassword(),
Collections.singleton(new SimpleGrantedAuthority(user.getRole())));
}
}
import java.sql.Timestamp;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import org.hibernate.annotations.CreationTimestamp;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@Data
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id // primary key
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int no;
private String userName;
private String password;
@Column(unique = true)
private String userEmail;
private String role; // ROLE_USER, ROLE_ADMIN
@CreationTimestamp
private Timestamp createDate;
}
import org.springframework.data.jpa.repository.JpaRepository;
import com.study.back.model.User;
public interface UserRepository extends JpaRepository<User, Integer> {
User findByUserEmail(String userEmail);
}
import org.springframework.http.ResponseEntity;
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.RestController;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import com.study.back.model.User;
import com.study.back.service.UserService;
import java.util.Map;
import java.util.HashMap;
import lombok.RequiredArgsConstructor;
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping("/join")
public ResponseEntity<Void> join(@RequestBody User user) {
System.out.println("회원가입 컨트롤러 실행" + user);
userService.joinUser(user);
System.out.println("회원가입 완료");
return ResponseEntity.ok().build();
}
@GetMapping("/loginOk")
public ResponseEntity<Map<String, String>> loginOk() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String email = authentication.getName();
String authorities = authentication.getAuthorities().toString();
System.out.println("로그인한 유저 이메일:" + email);
System.out.println("유저 권한:" + authentication.getAuthorities());
Map<String, String> userInfo = new HashMap<>();
userInfo.put("email", email);
userInfo.put("authorities", authorities);
return ResponseEntity.ok(userInfo);
}
@GetMapping("/logoutOk")
public ResponseEntity<Void> logoutOk() {
System.out.println("로그아웃 성공");
return ResponseEntity.ok().build();
}
@GetMapping("/admin")
public ResponseEntity<Void> getAdminPage() {
System.out.println("어드민 인증 성공");
return ResponseEntity.ok().build();
}
@GetMapping("/user")
public ResponseEntity<User> getUserPage() {
System.out.println("일반 인증 성공");
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String email = authentication.getName();
// 유저 정보
User user = userService.getUserInfo(email);
return ResponseEntity.ok(user);
}
}
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import com.study.back.model.User;
import com.study.back.repository.UserRepository;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public void joinUser(User user) {
String rawPassword = user.getPassword();
String encPassword = bCryptPasswordEncoder.encode(rawPassword);
System.out.println("비밀번호 인코딩:" + encPassword);
user.setPassword(encPassword);
user.setRole("ROLE_USER");
userRepository.save(user);
}
public User getUserInfo(String email) {
User user = userRepository.findByUserEmail(email);
return user;
}
}
import React, { useEffect, useState } from 'react';
import axios from 'axios';
function Join() {
const [user, setUser] = useState({
userName: '',
userEmail: '',
password: '',
});
const handleChange = (e) => {
const { id, value } = e.target;
setUser({ ...user, [id]: value });
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
await axios.post('http://localhost:8080/join', user);
alert('회원가입 완료');
window.location.href = '/';
} catch (error) {
console.log('회원가입 에러: ' + error);
}
};
return (
<div>
<h3>회원가입</h3>
<form onSubmit={handleSubmit}>
<input type="text" id="userName" value={user.userName} placeholder="이름" onChange={handleChange} />
<input type="text" id="password" value={user.password} placeholder="비밀번호" onChange={handleChange} />
<input type="text" id="userEmail" value={user.userEmail} placeholder="이메일" onChange={handleChange} />
<button type="submit">회원가입</button>
</form>
</div>
);
}
export default Join;
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import axios from 'axios';
function Login() {
const navigate = useNavigate();
const [user, setUser] = useState({
email: '',
password: '',
});
const handleChange = (e) => {
const { name, value } = e.target;
setUser({ ...user, [name]: value });
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
const formData = new FormData();
formData.append('email', user.email);
formData.append('password', user.password);
const response = await axios({
url: 'http://localhost:8080/loginProc',
method: 'POST',
data: formData,
withCredentials: true,
});
if (response.status === 200) {
alert('로그인 성공! ');
console.log('유저 이메일: ' + response.data.email);
console.log('권한: ' + response.data.authorities);
navigate('/home', { state: { userData: response.data } });
}
} catch (error) {
console.log('로그인 에러: ', error);
}
};
return (
<div>
<h3>로그인</h3>
<form onSubmit={handleSubmit}>
<input type="text" name="email" placeholder="이메일" value={user.email} onChange={handleChange} />
<input type="password" name="password" placeholder="비밀번호" value={user.password} onChange={handleChange} />
<button type="submit">로그인</button>
</form>
<Link to="/join">
<button>회원가입</button>
</Link>
</div>
);
}
export default Login;
import React from 'react';
import { useLocation } from 'react-router-dom';
import axios from 'axios';
function Home() {
const location = useLocation();
const { email, authorities } = location.state.userData;
const handleLogout = async () => {
try {
await axios.post('/logout');
alert('로그아웃 완료');
window.location.href = '/';
} catch (error) {
console.error('로그아웃 에러:', error);
}
};
const goToUserPage = () => {
window.location.href = '/userInfo';
};
const goToAdminPage = () => {
window.location.href = '/admin';
};
return (
<div>
<h1>사용자 정보</h1>
<p>이메일: {email}</p>
<p>권한: {authorities}</p>
<button onClick={handleLogout}>로그아웃</button>
<button onClick={goToUserPage}>마이 페이지</button>
<button onClick={goToAdminPage}>어드민 페이지</button>
</div>
);
}
export default Home;
withCredentials: true
를 보내줘야지만 세션 쿠키를 부트에서 읽을 수 있음import React, { useEffect, useState } from 'react';
import axios from 'axios';
function UserInfo() {
const [user, setUser] = useState({
userName: '',
userEmail: '',
role: '',
});
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get('http://localhost:8080/user', {
withCredentials: true, // 자격 증명(쿠키, 인증 헤더 등)을 포함하여 HTTP 요청
});
if (response.status === 200) {
setUser(response.data);
}
} catch (error) {
console.error('Error checking user status:', error);
}
};
fetchData();
}, []);
const handleLogout = async () => {
try {
await axios.post('/logout');
alert('로그아웃 완료');
window.location.href = '/';
} catch (error) {
console.error('로그아웃 에러:', error);
}
};
return (
<div>
<h1>마이페이지</h1>
<p>이름: {user.userName}</p>
<p>이메일: {user.userEmail}</p>
<p>권한: {user.role}</p>
<button onClick={handleLogout}>로그아웃</button>
</div>
);
}
export default UserInfo;
import React, { useEffect, useState } from 'react';
import axios from 'axios';
function Admin() {
const [isAdmin, setIsAdmin] = useState(false);
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get('http://localhost:8080/admin', {
withCredentials: true, // 자격 증명(쿠키, 인증 헤더 등)을 포함하여 HTTP 요청
});
if (response.status === 200) {
setIsAdmin(true);
}
} catch (error) {
console.error('Error checking admin status:', error);
}
};
fetchData();
}, []);
const handleLogout = async () => {
try {
await axios.post('/logout');
alert('로그아웃 완료');
window.location.href = '/';
} catch (error) {
console.error('로그아웃 에러:', error);
}
};
return (
<div>
{isAdmin ? <div>어드민 페이지입니다.</div> : <div>권한이 없습니다.</div>}
<button onClick={handleLogout}>로그아웃</button>
</div>
);
}
export default Admin;
마이페이지
어드민페이지
https://github.com/fever-max/SpringBoot-React-Security-login