이제부터 이론도 이론이지만 실습형에 비중을 더 두고 학습을 이어나가려 한다. 프로젝트도 있고 해커톤도 있고 앞으로 쓸 거리가 넘쳐나서 좋다.
이 프로젝트는 https://www.youtube.com/watch?v=NPRh2v7PTZg&list=PLJkjrxxiBSFCcOjy0AAVGNtIa08VLk1EJ
이 강의를 참고하여 만든 실습 프로젝트이다.
바로 jwt로 넘어가진 않고 회원가입 로직부터 로그인 필터 구현, DB기반 로그인 검증, JWT 발급 및 검증,세션, CORS 설정까지 순서대로 하는 것이 목표이다.
앞의 초기 내용은 생략하고 오늘은 DB 연결과 Entity 작성, 회원가입 로직 구현까지 할 것이다.
시작 전 설정
package com.example.Springjwt.config;
// 시큐리티 설정에 필요한 클래스들을 import
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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration // 이 클래스가 설정 클래스라는 걸 Spring에게 알림
@EnableWebSecurity // Spring Security 활성화 어노테이션
public class SecurityConfig {
// 비밀번호를 암호화하는 데 사용하는 BCryptPasswordEncoder를 Bean으로 등록
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
// 시큐리티 필터 체인을 설정하는 Bean
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 1. CSRF 보안 기능 비활성화
// JWT를 사용하는 경우, 세션/쿠키 기반이 아니라서 CSRF 보호가 필요 없음
http.csrf((auth) -> auth.disable());
// 2. 기본 제공 로그인 폼 기능 비활성화
// 우리는 REST API 방식으로 로그인할 예정이므로 formLogin은 꺼준다
http.formLogin((auth) -> auth.disable());
// 3. HTTP Basic 인증 방식 비활성화
// 브라우저 팝업창 로그인 방식도 사용하지 않을 예정이므로 꺼준다
http.httpBasic((auth) -> auth.disable());
// 4. URL 접근 권한 설정
http.authorizeHttpRequests((auth) -> auth
// 로그인, 회원가입, 메인 페이지는 인증 없이 접근 가능
.requestMatchers("/login", "/", "/join").permitAll()
// /admin 경로는 ADMIN 권한이 있어야 접근 가능
.requestMatchers("/admin").hasRole("ADMIN")
// 위에 명시되지 않은 모든 요청은 인증이 필요함
.anyRequest().authenticated()
);
// 5. 세션 관리 정책 설정
// JWT를 사용할 경우 서버에 세션 정보를 저장하지 않으므로 STATELESS로 설정
http.sessionManagement((session) ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
// 모든 설정을 적용한 SecurityFilterChain 객체를 반환
return http.build();
}
}
DB 연결과 Entity 작성
DB 연결
먼저 워크벤치를 사용해 jwt_prac이라는 이름의 데이터베이스를 생성한다.
해당 데이터베이스에 접근할 관리자 계정 이름은 smin_jwt로 설정하였고
Spring Boot 애플리케이션에서는 이 계정을 통해 데이터베이스에 연결하게 된다.
처음엔 DB를 생성하는 단계 이므로 ddl-auto는 create로 해준다.
이후에도 create를 하면 데이터가 날아가고 다시 생성되기 때문에 주의하자.
Entity 작성
이제 유저 정보를 저장할 테이블이 필요하다.
회원 정보를 담는 엔티티 클래스인 UserEntity를 정의하자.
우선 entity라는 이름의 패키지를 생성하고 그 안에 UserEntity라는 이름의 자바 클래스를 만들어준다.
이 클래스는 회원 가입 시 입력받는 정보를 담기 위해
- id: 회원 고유 ID (Primary Key)
- username: 사용자 ID 또는 이름
- password: 비밀번호
- role: 사용자의 권한(예: 일반 사용자, 관리자 등)
이 필드를 추가해줬다.
Repository 작성
이제 우리가 만든 UserEntity 클래스를 기반으로 실제 데이터베이스에 접근하고
해당 테이블에서 데이터를 조회하거나 저장할 수 있는 Repository를 생성하자.
repository 패키지를 만들고 그 안에 인터페이스 형태의 클래스를 정의한다.
이 Repository는 JpaRepository를 상속받아 만들며 두 개의 제네릭 타입 인자를 지정해줘야 한다.
- 첫 번째 인자는 관리 대상 엔티티 클래스인 UserEntity
- 두 번째 인자는 해당 엔티티의 기본 키 타입인 Integer이다 (참고: int가 아닌 래퍼 타입)
Repository를 인터페이스로 만드는 이유는 직접 구현체를 작성하지 않아도 Spring Data JPA가 자동으로 구현체를 생성하고 관리해주기 때문이다.
훨씬 간결하고 효율적으로 데이터 접근 로직을 처리할 수 있다.
회원가입 로직 구현
이 로직대로 회원가입 로직을 구현해볼 것이다.
전체적인 흐름을 알아보자.
1. 클라이언트(프론트엔드)에서 POST 방식으로 /join 경로에 username과 password를 포함한 회원가입 요청을 보낸다.
2. 이 요청은 Spring 애플리케이션의 JoinController에서 받는다.
- 요청에 포함된 회원 정보를 DTO 객체로 매핑하여 받도록 한다.
3. 컨트롤러는 받은 DTO를 JoinService로 넘기고 서비스에서는 이 DTO를 기반으로 UserEntity 객체를 생성한다.
4.변환된 UserEntity를 UserRepository를 통해 데이터베이스에 저장하여 최종적으로 회원가입 처리가 완료된다.
아직 만들지 않은 DTO, Controller, Service만 작성해서 회원가입이 되는지 확인해보자.
우선 아까 만든 테이블을 확인해보면 잘 생성된 것을 확인할 수 있다.
아직 내부에 데이터가 없으니까 null 상태인데 회원가입을 해서 데이터가 차는지 확인해보자.
DTO 생성
회원가입 시 클라이언트가 전송한 데이터를 서버가 안전하게 전달받기 위해 dto 패키지 하위에 JoinDTO라는 클래스를 생성한다.
이 클래스는 회원가입에 필요한 최소한의 정보인 username과 password를 담는 역할을 한다.
- username: 사용자가 회원가입 시 입력한 아이디나 이름
- password: 사용자가 설정한 비밀번호
이 두 필드는 사용자로부터 받아야 할 핵심 정보이다.
회원 가입 시 불필요한 정보 노출을 방지하기 위해 필요한 항목만 받도록 제한하는 것이다.
id, role 등의 필드는 서버에서 자동으로 설정되거나 관리되기 때문에 DTO에 포함하지 않는다.
Controller와 Service 생성
Controller
DTO를 통해서 특정한 경로에서 데이터를 받을 JoinController를 만들자.
외부에서 JSON 형식으로 요청 받는 API 서버 형태이므로 @RestController 어노테이션을 붙여줘야 한다.
클라이언트로부터 전달받은 JoinDTO 객체를 joinProcess() 메서드의 파라미터로 받고 해당 데이터를 내부 JoinService로 넘겨서 회원가입 로직을 처리하도록 한다.
JoinService를 생성자 주입 방식으로 의존성 주입을 받게 된다.
HTTP 요청이 POST 방식으로 /join 경로에 들어올 경우
public String joinProcess(JoinDTO joinDTO)
이 메서드가 실행된다.
요청으로 들어온 데이터를 JoinDTO라는 객체로 받아야 한다.
받은 JoinDTO를 JoinService로 넘겨 실제 저장 로직을 처리하게 된다.
ok를 리턴받으면 끝.
컨트롤러는 요청 받고 응답만 리턴하는 역할에 충실해야 한다.
Service
회원가입 로직의 핵심 처리 역할을 JoinService에서 담당한다.
클라이언트에서 전달받은 JoinDTO를 바탕으로 실제 DB에 사용자 정보를 저장하고 중복 사용자 검사도 처리한다.
여기서도 DB에 데이터를 저장하거나 중복 유저 검사를 위해 UserRepository를 주입받는다.
private final UserRepository userRepository; //@RequiredArgsConstructor로 자동 생성자 주입
이제 실제 회원가입을 처리하는 joinProcess() 메서드를 작성한다.
public void joinProcess(JoinDTO joinDTO) {}
이 메서드는 Controller에서 받은 JoinDTO를 인자로 받아서 처리한다.
회원 가입 진행 전에 이미 존재하는 회원인지를 검사해야 하는데 여기서 UserRepository도 변화를 줘야 한다.
Boolean existsByUsername(String username);
existsByUsername를 통해 Spring Data JPA의 쿼리 메서드 기능을 이용해준다.
메서드 이름만 보고도 Spring이 알아서 SQL을 만들어줄 수 있게 한다.
즉, DB에 해당 username이 존재하는지를 검사해서 중복 회원가입을 방지하기 위해 JoinService에서 사용할 수 있다.
어쨋든 다시 돌아와서 이미 존재하는 회원인지 검사하기 위해 userRepository.existsByUsername() 메서드를 활용한다.
boolean isUserExist = userRepository.existsByUsername(joinDTO.getUsername());
해당 username을 가진 유저가 DB에 존재하는지 확인해주고 값이 true로 나온다면 중복 사용자인 것이다.
UserEntity user = new UserEntity();
user.setUsername(joinDTO.getUsername());
user.setPassword(joinDTO.getPassword());
user.setRole("ROLE_USER"); // 기본 권한 부여
이제 전달받은 DTO 데이터를 기반으로 UserEntity 객체를 만들어주면 된다.
마지막으로 userRepository.save(user)를 통해 객체를 db에 전달해준다.
package com.example.Springjwt.service;
import com.example.Springjwt.dto.JoinDTO;
import com.example.Springjwt.entity.UserEntity;
import com.example.Springjwt.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor // 생성자 주입을 자동으로 만들어주는 Lombok 어노테이션
public class JoinService {
private final UserRepository userRepository; // 사용자 정보를 다루는 JPA Repository
private final BCryptPasswordEncoder bCryptPasswordEncoder; // 비밀번호 암호화를 위한 Bean
/**
* 회원가입 처리를 위한 메서드
* @param joinDTO 클라이언트로부터 전달받은 회원가입 정보(username, password)
*/
public void joinProcess(JoinDTO joinDTO) {
// DTO에서 username과 password 추출
String username = joinDTO.getUsername();
String password = joinDTO.getPassword();
// 이미 존재하는 사용자명인지 확인
Boolean isExists = userRepository.existsByUsername(username);
if (isExists) {
// username이 이미 존재하면 회원가입 중단
return;
}
// 새로운 사용자 엔티티 생성
UserEntity data = new UserEntity();
// 사용자명 설정
data.setUsername(username);
// 비밀번호를 암호화하여 설정
data.setPassword(bCryptPasswordEncoder.encode(password));
// 기본 권한 설정 (ROLE_ADMIN) 하드코딩 했음
data.setRole("ROLE_ADMIN");
// 회원 정보(3개 필드: username, password, role)를 DB에 저장
userRepository.save(data);
}
}
// 코드가 길어서 대체
이제 실제로 잘 동작하는지 확인해보자.
동작 확인
이전에 적은 코드 중에
http.authorizeHttpRequests((auth) -> auth
.requestMatchers("/login", "/", "/join").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated()
);
/join 경로로 들어오는 요청은 인증 없이 접근 가능하도록 허용했으므로 로그인하지 않은 사용자도 Postman 같은 클라이언트 도구로 POST /join 요청을 보내면 Spring Security가 막지 않고 요청을 컨트롤러까지 통과시킨다.
테스트를 해보자.
성공이 떴고 이제 db를 확인해보면
회원가입이 완료된 것을 확인할 수 있다.
다음 시간엔 로그인 필터와 DB 기반 로그인 검증 로직 구현을 해보겠다.
'Backend > Security' 카테고리의 다른 글
OAuth2.0을 활용한 소셜 로그인 구현하기 - 1 (2) | 2025.06.07 |
---|---|
Spring Security JWT -5 (0) | 2025.05.06 |
Spring Security JWT -4 (1) | 2025.05.04 |
Spring Security JWT -3 (0) | 2025.05.03 |
Spring Security JWT -2 (0) | 2025.05.02 |