📌 프로젝트 개요
JWT를 활용해 로그인을 구현하고, Spring Security로 인가 기능을 구현. 비밀번호는 BCrypt로 암호화.
- 프로젝트 명: JWT 기반 인증/인가 구현 (Spring Boot)
- 개발 기간: 25.06.23 ~ 25.06.27
- 목적:
- JWT를 활용한 로그인 및 인가 기능을 직접 구현하여 인증 방식의 원리를 이해하고 실습
- Spring Boot로 기능별 API 설계 연습
- 사용 기술: Java 17, Spring Boot 3.5.4, JPA & H2, VS Code, Git & GitHub
🧩 구현 기능
기능명 | 상세 설명 |
회원가입 | 비밀번호는 BCrypt로 암호화하여 저장 |
로그인 | 로그인 시 JWT 토큰이 발급, HttpOnly 쿠키에 저장 |
로그아웃 | 로그아웃 시 HttpOnly 쿠키의 JWT 토큰 삭제 |
인가 | 페이지 접근 시 JWT 토큰을 검증해 권한 확인 |
💜 GitHub 저장소 & 데모
📁 디렉토리 구조
📂 src/main/java/com/jelee/toyauthjwtjpa
├── ToyauthjwtjpaApplication.java // 애플리케이션 진입점
├── config/
│ └── SecurityConfig.java // 스프링 시큐리티 설정
├── security/
│ ├── JwtAuthenticationFilter.java // JWT 인증 필터 (요청 시 토큰 검사)
│ └── JwtProvider.java // JWT 생성 및 검증, 사용자 조회
├── entity/
│ ├── Role.java // 권한 정보 엔티티
│ └── User.java // 사용자 엔티티 (UserDetails 구현)
├── repository/
│ └── UserRepository.java // DB와 접근 처리 레포지토리
├── controller/
│ └── AuthController.java // API 엔드포인트 정의
├── service/
│ └── AuthService.java // 인증 관련 비즈니스 로직 처리
├── dto/
│ ├── JoinRequest.java // 회원가입 데이터 전송 객체
│ └── LoginRequest.java // 로그인 데이터 전송 객체
└── README.md
🧱 클래스/설계 구조 설명
- SecurityConfig:
- SecurityFilterChain 빈 등록
- .authorizeHttpRequests 설정
-> index.html파일과 /css, /js, /h2-console, /auth 해당 경로로 시작하는 건 모두 접근 가능하게 permitAll() 지정 - JWT 인증 필터 (JwtAuthenticationFilter)를
UsernamePasswordAuthenticationFilter 이전에 실행되도록 등록 - H2 콘솔에서 iframe을 사용 허용 설정 추가
- .authorizeHttpRequests 설정
- PasswordEncoder 빈 등록
- BCryptPassowrdEncoder 객체 생성 후 리턴.
- SecurityFilterChain 빈 등록
- JwtAuthenticationFilter:
- doFilterInternal 재정의
- 파라미터로 사용자한테 요청받은 HttpServletRequest, 응답용인 빈 객체 HttpServletResponse, 그리고 요청과 응답을 다음 필터로 보내기 위해 FilterChain을 받는다.
- token에 사용자한테 요청받은 request에서 Cookies가 있는지 확인하고 token 변수에 요청받은 데이터에서 쿠키에 JWT 글자가 있는지 확인하고 쿠키에 JWT가 있으면 쿠키의 값을 꺼낸다. 만약에 없으면 null을 반환한다.
- token에 담은 JWT가 유효한지 검사하기
- JwtProvider.validateToken()을 사용해 token이 유효한지 먼저 검사한다.
- 유효하다면 user 객체에 토큰에서 추출한 username을 User 객체를 생성하여 담는다.
- UsernamePasswordAuthenticationToken을 사용해 생성한 User 객체와 credentials는 null, 그리고 User 객체에서 권한을 담아 인증 객체를 생성한다.
- 생성한 인증 객체는 SecurityContext에 저장한다.
- 마지막으로 filterChain.doFilter()를 사용하여 클라이언트한테 요청받은 request와 빈 응답 객체인 response를 다음 체인으로 보낸다.
- doFilterInternal 재정의
- JwtProvider:
- 토큰을 생성하기 전에 토큰에 사용할 시크릿 키를 생성하기 위해 Keys.secretKeyFor(SignatureAlgorithm.HS256); 을 key에 상수로 담는다.
- 그리고 EXPIRATION 변수를 상수로 선언하여 1000 * 60 * 60 * 24 의 값을 초기화한다. (24시간을 밀리초 단위로 환산한 값)
- 토큰 생성하기
- username과 role을 파라미터로 받는다.
- Date를 사용하여 생성된 현재 시간을 now 변수에 담고, expiry 변수에는 생성된 시간에 1일을 더한 값을 저장한다.
- 그리고 Jwts를 사용해 username, 권한, 생성 시간, 유효 시간, key(시크릿 키)을 .compact() 하나의 JWT 토큰 문자열로 만들어서 빌더 객체를 반환한다.
- 토큰 검증하기
- 클라이언트로 받은 token을 파라미터로 받아서 Jwts를 사용해 .parserBuilder()를 한다. 이때 클라이언트한테 받은 token을 parseClaimsJws()를 사용해 분해하여 palyload 정보를 읽어 처음에 token 생성할 때 서명된 key인지 확인한다.
- JWT 검증 실패하거나 잘못된 인자가 전달된 경우 JwtException과 IllegalArgumentException 예외를 발생시킨다.
- 토큰으로 사용자 정보 얻기
- 클라이언트로 요청받은 데이터에서 token을 Jwts를 사용해 파싱(빌더)한다. 이 과정에서 서명 검증을 한 다음 서명이 유효하면 token을 분해하여 토큰의 body를 가져온다. 그리고나서 Subject를 꺼내어 리턴한다.
- Role/User:
- Role 엔티티는 사용자 권한을 enum으로 관리. ROLE_USER, RULE_MANAGER, ROLE_ADMIN 으로 구성
- User 엔티티는 UserDetails를 구현한다.
- 필드는 id, username(사용자 아이디), password, role로 구성되어 있다.
- 메서드는 UserDetails를 구현하기 때문에 반드시 오버라이드해야하는 메서드들을 재정의 해준다.
- getAuthorities, getUsername, getPassword, isAccountNonExpired, isAccountNonLocked, isCredentialsNonExpired, isEnabled
- UserRepository:
- JpaRepository를 상속 받는다.
- 로그인 시 아이디로 사용자를 찾을 때 사용할 메서드 findByUsername은 Optional 제너릭을 사용해 username으로 파라미터를 받아서 DB에서 찾아서 User객체를 반환한다.
- 회원가입 시 아이디가 중복되었는지를 확인하기 위해 existsByUsername()을 만들어 username을 파라미터로 받아서 중복되는지 체크하여 boolean값을 반환한다.
- AuthController:
- /auth를 기본 mapping으로 두기
- 회원가입은 /register, 로그인은 /login, 사용자 인증은 /me, 로그아웃은 /logout 각 API 엔드포인트를 갖는다.
- 회원가입은
- RequestBody로 요청값을 받아 AuthService의 register()를 통해 회원가입을 처리한다.
- 로그인은
- RequestBody로 클라이언트 요청과 HttpServletResponse 빈 객체를 파라미터로 받는다.
- 클라이언트 요청은 AuthService의 logn()을 통해 로그인을 하는데 이때 생성되는 token을 token 변수에 담는다.
- 그리고 ResponseCookie 인스턴스 cookie를 생성해 JWT이름과 값은 token을 담고 httpOnly에 저장, secure(), path()는 기본 /, maxAge()는 24시간, samSite()는 Strict로 설정해주고 .build()를 해준다.
- 이렇게 생성한 cookie는 Set-Cookie로 응답해주는데 JWT 토큰을 로그인한 브라우저에 보내어 저장해준다.
- 로그아웃은
- HttpServletResponse 빈 객체 response를 파라미터로 받는다.
- ResponseCookie 인스턴스 deleteCookie를 생성하는데 로그인과 다르게 JWT 속성에 값은 "" 빈 문자열로 설정한다. 그리고 maxAge를 0으로 하여 쿠키를 즉시 만료시킨다.
- response 헤더에 deleteCookie를 담아 보내어 로그아웃 시 쿠키에 JWT가 삭제되도록 한다.
- 사용자 인증은
- 페이지마다 이동할 때 유효한지 검증을 해야한다.
- Authentication 인스턴스 authentication을 파라미터로 받아서 User 인스턴스인 user에 요청 데이터인 authentication에서 getPrincipal()하여 담는다.
- user.getUsername()을 반환하여 로그인한 사용자를 확인할 수 있다.
- AuthService:
- 회원가입과 로그인 로직이 작성되어 있다.
- 회원가입은
- Controller로부터 요청받은 request를 먼저 아이디 중복 체크를 한 다음에 중복이 아니라면 User 인스턴스 user를 생성하여 builder()를 한다. 이때 username은 request.getUsername(), password는 passwordEncoder를 사용해 암호화하여 비밀번호를 저장, role은 기본 ROLE_USER로 한다.
- 마지막에 UserRepository.save()를 통해 user를 저장한다.
- 로그인은
- Controller로부터 요청받은 request를 User 인스턴스 user에 DB에 해당 username이 있는지 찾아서 데이터를 저장한다.
- 만약에 request.getPassword()와 user.getPassword()가 일치하지 않으면 IllegalArugementException을 throw하고, 일치하면 jwtProvider를 통해 token을 생성해 반환한다.
- JoinRequest/LoginRequest
- 두 객체는 username과 password 필드를 갖고 있다.
- 두개로 분류한 이유는 DTO는 하나의 요청에만 사용되도록 설계되어야 역할이 명확해져 코드를 이해하기 쉽고 재사용도 용이하다는 내용을 봤다. 그리고 유지보수에도 용이하기에 각각 나누었다.
🔢 개발 순서
순서 | 내용 | 상세 설명 |
1 | 요구사항 분석 | - 기능 요약, 기술 명세, 사용자 역할 정리 |
2 | ERD 설계 | - User 테이블 - 권한 Enum으로 관리 |
3 | 프로젝트 구조 설계 | - config, security, dto, entity, controller, repository, service 로 구성 |
4 | 개발 환경 세팅 | - build.gradle 의존성 추가 - application.properties 설정 |
5 | Entity & Enum 구현 | - User, Role Enum 클래스 만들기 |
6 | SecurityConfig 기본 설정 | - H2 콘솔 접근 허용, H2에서 iframe 허용, H2 콘솔은 CSRF 무시 설정 - 기본 로그인 폼 사용 설정 |
7 | UserRepository 생성 (JpaRepository 상속) |
- 회원가입 시 중복 아이디 체크 기능 - 로그인 시 아이디로 사용자 찾기 기능 |
8 | 회원가입 구현 | - AuthService 생성 (중복 아이디 체크 후 회원가입) - JoinRequest 생성 (회원가입에 필요한 DTO 생성) - SecurityConfig에 PasswordEncoder 빈으로 등록 - AuthServiceTest 생성 (회원가입 단위 테스트를 위해 생성) - SecurityConfig에서 테스트를 위해 .csrf() 잠시 꺼두기 - AuthController 생성 (회원가입 API 작성) |
9 | 로그인 구현 | - LoginRequestDTO 생성 (로그인에 사용할 DTO 생성) - JWT 토큰 생성, 검증, 정보 추출을 위해 JwtProvider 생성 - AuthService에 로그인에 사용될 비즈니스 로직 구현 - AuthController에 로그인 API 설계 (JWT를 HttpOnly 쿠키에 저장) |
10 | JWT 인증 필터 생성 | - JwtAuthenticationFilter 만들기 (쿠키에서 JWT 찾고 유효성 검사) - SecurityConfig에 .addFilterBefore() 추가 (UsernamePasswordAuthenticationFilter 앞에 JWT 두기) - AuthController에 인증을 필요로하는 API 설계 |
11 | 로그아웃 구현 | - AuthController에 로그아웃 API 설계 (쿠키에서 JWT 삭제) |
🐞 문제 해결 & 트러블 슈팅
✅ 이슈 1 - 엔티티 생성 후 서버 실행 시 403 에러코드 발생
- 문제: H2 Console에서 403에러 발생
- 원인: Spring Security 설정에서 /h2-console 추가 안함
- 해결: SecurityFilterChain에서 .authorizeHttpRequests에 /h2-console 추가
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/h2-console/**").permitAll()
.anyRequest().authenticated()
)
✅ 이슈 2 - 엔티티 생성 후 서버 실행 시 403 에러코드 발생
- 문제: h2-console에 로그인 안됨
- 원인: application.properties에 spring.datasource.username과 password를 임의로 설정함
- 해결: 기본값으로 설정함
spring.datasource.username=sa
spring.datasource.password=
***
Spring Boot는 기본적으로
H2 in-memory DB를 사용하는 경우
DB를 서버 실행 시점에 메모리에 생성하는데
이때 DB에 아무런 데이터가 없다.
그런데 username과 password를 설정하면
DB에서 username과 password를 찾아서 비교해야하는데
DB에는 데이터가 없기 때문에 접근할 수 없게 되는 거다.
✅ 이슈 3 - h2 콘솔에 db가 보이지 않는 현상
- 문제: Postman에서 회원가입 API를 테스트하는데, 200OK가 뜨는데도 h2에 데이터가 생성이 안됨
- 원인: 애플리케이션과 H2 콘솔이 서로 다른 커넥션을 사용
- 해결: application.properties에서 datasource.url 수정 및 h2 콘솔 접속 시 같은 DB URL로 접속
// application.properties에서 설정하기
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
✍ 느낀 점 / 회고
- JWT의 사용 방법과 흐름을 알게 되었다. 그렇다고 전체다 이해하는 건 아니지만 전체적인 흐름은 이해하는데 도움이 되었다.
- 처음부터 끝까지 코드를 작성한다면 못할 거 같다. 여러번 반복하며 연습해야할 거 같다.
- dto, entity의 차이를 알게 되었고 repository와 service의 차이점도 알게 되었다. dto는 계층 간 데이터 전달용 객체이고 entity는 db와 연결된 객체로 저장, 수정, 삭제 등을 할 수 있다. service는 controller로부터 요청을 받아 필요한 작업을 하고 repository는 db에 접근하는 클래스다. Mybatis에서 Mapper와 비슷한 역할을 하는 거 같다.
'개발 기록 > 개발 요약' 카테고리의 다른 글
[mini-project] To-do List (React) (0) | 2025.07.07 |
---|---|
[mini-project] Country Flag Guessing Game (JS) (0) | 2025.07.05 |
[toy-project] Library Book Manager (CLI, Java) (0) | 2025.07.03 |
[team-project] Hotel PMS (0) | 2025.04.29 |
JavaScript를 사용하여 목록을 추가하는 간단한 예제 (0) | 2024.08.24 |