이 프로젝트는 Spring Security와 JWT(JSON Web Token)를 활용한 Stateless 인증 구현 예제입니다.
- Java 17
- Spring Boot 3.3.4
src
├── main
│ ├── java
│ │ └── org.example.statelessspringsecurity
│ │ ├── config
│ │ │ ├── JwtAuthenticationFilter.java # JWT 토큰 검증 및 인증 처리 필터
│ │ │ ├── JwtAuthenticationToken.java # JWT 인증 토큰 객체
│ │ │ ├── JwtUtil.java # JWT 생성 및 검증 유틸
│ │ │ └── SecurityConfig.java # Spring Security 설정
│ │ ├── controller # 컨트롤러
│ │ ├── dto # 데이터 전송 객체
│ │ ├── entity # JPA 엔티티
│ │ ├── enums # 열거형
│ │ ├── repository # JPA 리포지토리
│ │ ├── service # 비즈니스 로직
│ │ └── StatelessSpringSecurityApplication.java
│ └── resources
│ └── application.yml # 애플리케이션 설정
└── test # 테스트 코드
- Stateless 인증: 서버가 사용자 인증 상태를 저장하지 않고 JWT 토큰으로 인증을 처리합니다.
- Spring Security 필터 체인 커스터마이징: 세션 관리, 폼 로그인, 기본 인증 등 불필요한 기능을 비활성화하였습니다.
- JWT 토큰 기반 인증: Authorization 헤더의 Bearer 토큰으로 인증을 처리합니다.
- 역할(Role) 기반 권한 부여: 사용자 역할에 따라 API 접근 권한이 다르게 설정됩니다.
- 회원가입:
/auth/signup엔드포인트를 통해 사용자 등록 - 로그인:
/auth/signin엔드포인트를 통해 로그인 후 JWT 토큰 발급 (응답 헤더의 Authorization에 포함) - 인증: 발급받은 JWT 토큰을 요청 헤더의 Authorization에 Bearer 형식으로 추가하여 API 호출
POST /auth/signup: 회원가입POST /auth/signin: 로그인 및 JWT 토큰 발급GET /open: 인증 없이 접근 가능한 APIGET /test: ADMIN 권한을 가진 사용자만 접근 가능한 API
application.yml파일에 JWT 시크릿 키 설정:
jwt:
secret:
key: [시크릿 키]이 프로젝트에서는 Spring Security를 사용한 세 가지 서로 다른 테스트 방식을 구현했습니다.
각 테스트 메서드마다 JwtAuthenticationToken을 생성하여 인증 객체를 직접 주입하는 방식입니다.
@Test
public void 권한이_ADMIN일_경우_200() throws Exception {
AuthUser authUser = new AuthUser(1L, "admin@example.com", UserRole.ROLE_ADMIN);
JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(authUser);
mockMvc.perform(get("/test")
.with(authentication(authenticationToken)))
.andExpect(status().isOk());
}이 방식은 각 테스트 케이스마다 인증 객체를 생성해야 하므로 코드 중복이 발생할 수 있지만, 테스트마다 다른 인증 정보가 필요한 경우 유연하게 설정할 수 있습니다.
@BeforeEach를 사용하여 테스트 실행 전에 인증 토큰을 미리 생성하고 재사용하는 방식입니다.
@BeforeEach
public void setUp() {
AuthUser adminUser = new AuthUser(1L, "admin@example.com", UserRole.ROLE_ADMIN);
adminAuthenticationToken = new JwtAuthenticationToken(adminUser);
AuthUser normalUser = new AuthUser(2L, "user@example.com", UserRole.ROLE_USER);
userAuthenticationToken = new JwtAuthenticationToken(normalUser);
}
@Test
public void 권한이_ADMIN일_경우_200() throws Exception {
mockMvc.perform(get("/test")
.with(authentication(adminAuthenticationToken)))
.andExpect(status().isOk());
}이 방식은 여러 테스트에서 동일한 인증 정보를 재사용할 수 있어 코드 중복을 줄일 수 있습니다.
@WithMockAuthUser와 같은 커스텀 어노테이션을 생성하여 테스트 메서드에 직접 인증 정보를 설정하는 방식입니다.
@Test
@WithMockAuthUser(userId = 1L, email = "admin@example.com", role = UserRole.ROLE_ADMIN)
public void 권한이_ADMIN일_경우_200() throws Exception {
mockMvc.perform(get("/test"))
.andExpect(status().isOk());
}이 방식은 가장 간결하고 가독성이 높으며, 테스트 메서드에 직접 인증 정보를 명시하므로 테스트 의도를 쉽게 파악할 수 있습니다. 커스텀 어노테이션은 다음과 같이 구현됩니다:
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = TestSecurityContextFactory.class)
public @interface WithMockAuthUser {
long userId();
String email();
UserRole role();
}그리고 TestSecurityContextFactory는 다음과 같이 구현됩니다:
public class TestSecurityContextFactory implements WithSecurityContextFactory<WithMockAuthUser> {
@Override
public SecurityContext createSecurityContext(WithMockAuthUser customUser) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
AuthUser authUser = new AuthUser(customUser.userId(), customUser.email(), customUser.role());
JwtAuthenticationToken authentication = new JwtAuthenticationToken(authUser);
context.setAuthentication(authentication);
return context;
}
}@Test
public void 회원가입과_로그인_후_ADMIN_인가를_통과하고_유저_정보를_확인한다() throws Exception {
// 1. 회원가입
SignupRequest signupRequest = new SignupRequest(adminEmail, UserRole.Authority.ADMIN);
mockMvc.perform(post("/auth/signup")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(signupRequest))
.with(csrf()))
.andExpect(status().isOk());
// 2. 로그인
SigninRequest signinRequest = new SigninRequest(adminEmail);
MvcResult mvcResult = mockMvc.perform(post("/auth/signin")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(signinRequest))
.with(csrf()))
.andExpect(status().isOk())
.andReturn();
String bearerToken = mvcResult.getResponse().getHeader("Authorization");
// 3. 발급받은 토큰으로 API 호출
mockMvc.perform(get("/test")
.header("Authorization", bearerToken))
.andExpect(status().isOk());
}이 프로젝트는 LICENSE 파일에 명시된 라이센스 조건에 따라 배포됩니다.