{"id":22735615,"url":"https://github.com/nhahan/stateless-spring-security","last_synced_at":"2025-08-27T11:03:01.867Z","repository":{"id":260395423,"uuid":"865734656","full_name":"Nhahan/stateless-spring-security","owner":"Nhahan","description":"stateless-spring-security","archived":false,"fork":false,"pushed_at":"2025-03-11T15:31:26.000Z","size":81,"stargazers_count":8,"open_issues_count":0,"forks_count":38,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-08-19T23:47:22.888Z","etag":null,"topics":["java","jwt","security","spring"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Nhahan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-10-01T03:25:33.000Z","updated_at":"2025-05-19T16:40:08.000Z","dependencies_parsed_at":"2024-10-31T04:23:07.206Z","dependency_job_id":"f12004cd-385d-4f90-adf0-ae9e9ff26e99","html_url":"https://github.com/Nhahan/stateless-spring-security","commit_stats":null,"previous_names":["nhahan/stateless-spring-security"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Nhahan/stateless-spring-security","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nhahan%2Fstateless-spring-security","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nhahan%2Fstateless-spring-security/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nhahan%2Fstateless-spring-security/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nhahan%2Fstateless-spring-security/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Nhahan","download_url":"https://codeload.github.com/Nhahan/stateless-spring-security/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nhahan%2Fstateless-spring-security/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272323667,"owners_count":24914243,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-27T02:00:09.397Z","response_time":76,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["java","jwt","security","spring"],"created_at":"2024-12-10T21:13:32.339Z","updated_at":"2025-08-27T11:03:01.858Z","avatar_url":"https://github.com/Nhahan.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# JWT를 활용한 Stateless Spring Security 예제\n\n## 프로젝트 소개\n이 프로젝트는 Spring Security와 JWT(JSON Web Token)를 활용한 Stateless 인증 구현 예제입니다.\n\n## 기술 스택\n- Java 17\n- Spring Boot 3.3.4\n\n## 프로젝트 구조\n```\nsrc\n├── main\n│   ├── java\n│   │   └── org.example.statelessspringsecurity\n│   │       ├── config\n│   │       │   ├── JwtAuthenticationFilter.java     # JWT 토큰 검증 및 인증 처리 필터\n│   │       │   ├── JwtAuthenticationToken.java      # JWT 인증 토큰 객체\n│   │       │   ├── JwtUtil.java                     # JWT 생성 및 검증 유틸\n│   │       │   └── SecurityConfig.java              # Spring Security 설정\n│   │       ├── controller                           # 컨트롤러\n│   │       ├── dto                                  # 데이터 전송 객체\n│   │       ├── entity                               # JPA 엔티티\n│   │       ├── enums                                # 열거형\n│   │       ├── repository                           # JPA 리포지토리\n│   │       ├── service                              # 비즈니스 로직\n│   │       └── StatelessSpringSecurityApplication.java\n│   └── resources\n│       └── application.yml                          # 애플리케이션 설정\n└── test                                             # 테스트 코드\n```\n\n## 주요 기능\n1. **Stateless 인증**: 서버가 사용자 인증 상태를 저장하지 않고 JWT 토큰으로 인증을 처리합니다.\n2. **Spring Security 필터 체인 커스터마이징**: 세션 관리, 폼 로그인, 기본 인증 등 불필요한 기능을 비활성화하였습니다.\n3. **JWT 토큰 기반 인증**: Authorization 헤더의 Bearer 토큰으로 인증을 처리합니다.\n4. **역할(Role) 기반 권한 부여**: 사용자 역할에 따라 API 접근 권한이 다르게 설정됩니다.\n\n## 인증 흐름\n1. 회원가입: `/auth/signup` 엔드포인트를 통해 사용자 등록\n2. 로그인: `/auth/signin` 엔드포인트를 통해 로그인 후 JWT 토큰 발급 (응답 헤더의 Authorization에 포함)\n3. 인증: 발급받은 JWT 토큰을 요청 헤더의 Authorization에 Bearer 형식으로 추가하여 API 호출\n\n## API 엔드포인트\n- `POST /auth/signup`: 회원가입\n- `POST /auth/signin`: 로그인 및 JWT 토큰 발급\n- `GET /open`: 인증 없이 접근 가능한 API\n- `GET /test`: ADMIN 권한을 가진 사용자만 접근 가능한 API\n\n## 설정 방법\n1. `application.yml` 파일에 JWT 시크릿 키 설정:\n```yaml\njwt:\n  secret:\n    key: [시크릿 키]\n```\n\n## 테스트 코드\n\n이 프로젝트에서는 Spring Security를 사용한 세 가지 서로 다른 테스트 방식을 구현했습니다.\n\n### 1. 기본 인증 토큰 주입 방식 (TestControllerTest)\n각 테스트 메서드마다 `JwtAuthenticationToken`을 생성하여 인증 객체를 직접 주입하는 방식입니다.\n\n```java\n@Test\npublic void 권한이_ADMIN일_경우_200() throws Exception {\n    AuthUser authUser = new AuthUser(1L, \"admin@example.com\", UserRole.ROLE_ADMIN);\n    JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(authUser);\n    \n    mockMvc.perform(get(\"/test\")\n                    .with(authentication(authenticationToken)))\n            .andExpect(status().isOk());\n}\n```\n\n이 방식은 각 테스트 케이스마다 인증 객체를 생성해야 하므로 코드 중복이 발생할 수 있지만, 테스트마다 다른 인증 정보가 필요한 경우 유연하게 설정할 수 있습니다.\n\n### 2. 사전 설정된 토큰 재사용 방식 (TestControllerWithSetUpTokenTest)\n`@BeforeEach`를 사용하여 테스트 실행 전에 인증 토큰을 미리 생성하고 재사용하는 방식입니다.\n\n```java\n@BeforeEach\npublic void setUp() {\n    AuthUser adminUser = new AuthUser(1L, \"admin@example.com\", UserRole.ROLE_ADMIN);\n    adminAuthenticationToken = new JwtAuthenticationToken(adminUser);\n    \n    AuthUser normalUser = new AuthUser(2L, \"user@example.com\", UserRole.ROLE_USER);\n    userAuthenticationToken = new JwtAuthenticationToken(normalUser);\n}\n\n@Test\npublic void 권한이_ADMIN일_경우_200() throws Exception {\n    mockMvc.perform(get(\"/test\")\n                    .with(authentication(adminAuthenticationToken)))\n            .andExpect(status().isOk());\n}\n```\n\n이 방식은 여러 테스트에서 동일한 인증 정보를 재사용할 수 있어 코드 중복을 줄일 수 있습니다.\n\n### 3. 커스텀 어노테이션 방식 (TestControllerWithMockAuthUserTest) - 추천\n`@WithMockAuthUser`와 같은 커스텀 어노테이션을 생성하여 테스트 메서드에 직접 인증 정보를 설정하는 방식입니다.\n\n```java\n@Test\n@WithMockAuthUser(userId = 1L, email = \"admin@example.com\", role = UserRole.ROLE_ADMIN)\npublic void 권한이_ADMIN일_경우_200() throws Exception {\n    mockMvc.perform(get(\"/test\"))\n            .andExpect(status().isOk());\n}\n```\n\n이 방식은 가장 간결하고 가독성이 높으며, 테스트 메서드에 직접 인증 정보를 명시하므로 테스트 의도를 쉽게 파악할 수 있습니다. 커스텀 어노테이션은 다음과 같이 구현됩니다:\n\n```java\n@Retention(RetentionPolicy.RUNTIME)\n@WithSecurityContext(factory = TestSecurityContextFactory.class)\npublic @interface WithMockAuthUser {\n    long userId();\n    String email();\n    UserRole role();\n}\n```\n\n그리고 `TestSecurityContextFactory`는 다음과 같이 구현됩니다:\n\n```java\npublic class TestSecurityContextFactory implements WithSecurityContextFactory\u003cWithMockAuthUser\u003e {\n    @Override\n    public SecurityContext createSecurityContext(WithMockAuthUser customUser) {\n        SecurityContext context = SecurityContextHolder.createEmptyContext();\n        AuthUser authUser = new AuthUser(customUser.userId(), customUser.email(), customUser.role());\n        JwtAuthenticationToken authentication = new JwtAuthenticationToken(authUser);\n        context.setAuthentication(authentication);\n        return context;\n    }\n}\n```\n\n### 통합 테스트 (AuthIntegrationTest)\n\n```java\n@Test\npublic void 회원가입과_로그인_후_ADMIN_인가를_통과하고_유저_정보를_확인한다() throws Exception {\n    // 1. 회원가입\n    SignupRequest signupRequest = new SignupRequest(adminEmail, UserRole.Authority.ADMIN);\n    mockMvc.perform(post(\"/auth/signup\")\n                    .contentType(MediaType.APPLICATION_JSON)\n                    .content(objectMapper.writeValueAsString(signupRequest))\n                    .with(csrf()))\n            .andExpect(status().isOk());\n\n    // 2. 로그인\n    SigninRequest signinRequest = new SigninRequest(adminEmail);\n    MvcResult mvcResult = mockMvc.perform(post(\"/auth/signin\")\n                    .contentType(MediaType.APPLICATION_JSON)\n                    .content(objectMapper.writeValueAsString(signinRequest))\n                    .with(csrf()))\n            .andExpect(status().isOk())\n            .andReturn();\n\n    String bearerToken = mvcResult.getResponse().getHeader(\"Authorization\");\n\n    // 3. 발급받은 토큰으로 API 호출\n    mockMvc.perform(get(\"/test\")\n                    .header(\"Authorization\", bearerToken))\n            .andExpect(status().isOk());\n}\n```\n\n## 라이센스\n이 프로젝트는 LICENSE 파일에 명시된 라이센스 조건에 따라 배포됩니다. \n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnhahan%2Fstateless-spring-security","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnhahan%2Fstateless-spring-security","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnhahan%2Fstateless-spring-security/lists"}