유저관련 기본 API 기능을 구현해 보고자 한다.
구현전 사용할 Spring Security 에 대해 알 필요가 있다.
Spring Security란?
Spring Security는 인증, 권한 관리 그리고 데이터 보호 기능을 포함하여 웹 개발 과정에서 필수적인 사용자 관리 기능을 구현하는데 도움을 주는 Spring의 강력한 프레임워크이다.
Spring Security 아키텍처
1. 사용자의 요청이 서버로 들어온다.
2. Authotication Filter가 요청을 가로채고 Authotication Manger로 요청을 위임한다.
3. Authotication Manager는 등록된 Authotication Provider를 조회하며 인증을 요구한다.
4. Authotication Provider가 실제 데이터를 조회하여 UserDetails 결과를 돌려준다.
5. 결과는 SecurityContextHolder에 저장이 되어 저장된 유저정보를 Spring Controller에서 사용할 수 있게 된다.
Spring Security가 작동하는 내부 구조
1. 사용자가 자격 증명 정보를 제출하면, AbstractAuthenticationProcessingFilter가 Authentication 객체를 생성한다.
2. Authentication 객체가 AuthenticationManager에게 전달된다.
3. 인증에 실패하면, 로그인 된 유저정보가 저장된 SecurityContextHolder의 값이 지워지고 RememberMeService.joinFail()이 실행된다. 그리고 AuthenticationFailureHandler가 실행된다.
4. 인증에 성공하면, SessionAuthenticationStrategy가 새로운 로그인이 되었음을 알리고, Authentication 이 SecurityContextHolder에 저장된다. 이후에 SecurityContextPersistenceFilter가 SecurityContext를 HttpSession에 저장하면서 로그인 세션 정보가 저장된다. 그 뒤로 RememberMeServices.loginSuccess()가 실행된다. ApplicationEventPublisher가 InteractiveAuthenticationSuccessEvent를 발생시키고 AuthenticationSuccessHandler 가 실행된다.
블로그 참조
https://www.elancer.co.kr/blog/detail/235
Spring Security란? 사용하는 이유부터 설정 방법까지 알려드립니다! I 이랜서 블로그
홈페이지에 인증 및 권한 기능을 빠르게 부여해 인증 및 권한 보호 기능을 손쉽게 추가할 수 있는 Spring의 프레임워크 중 하나인 ‘Spring Security’에 대해 이랜서에서 자세히 알려드립니다. I spring
www.elancer.co.kr
요약하면,
Spring Security는 애플리케이션의 인증(로그인)과 권한(Authorization) 관리를 처리하는데 사용자의 요청은 필터 체인에서 가로채어 인증 필터(Authentication Filter)가 처리하며, 인증 매니저(Authentication Manager)와 프로바이더(Authentication Provider)가 사용자의 정보를 검증한다. 검증된 사용자 정보는 SecurityContextHolder에 저장되어 컨트롤러에서 쉽게 접근할 수 있다. 실패 시 오류 처리가 수행되고, 성공 시 세션 정보가 관리된다.
Spring Security는 이러한 구조로 안전하고 효율적인 사용자 관리를 제공한다.
스프링 시큐리티를 사용하는 기본 방법은
implementation 'org.springframework.boot:spring-boot-starter-security'
해당 Dependency를 추가하면 된다.
해당 Dependency만 추가하게되면 어플리케이션을 키게되면 다음과 같은 화면에 강제로 라우팅되는 것을 볼 수 있다.
해당 부분은 스프링 시큐리티 설정을 진행하여 접근을 허용하는 부분, 허용하지 않는 부분을 정해줘야한다.
package com.gathering.gathering_backend.config;
import com.gathering.gathering_backend.util.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Configuration
public class SecurityConfig {
// PasswordEncoder Bean
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// SecurityFilterChain Bean
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // CSRF 비활성화
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll() // 모든 요청 허용 (테스트용)
)
.formLogin(form -> form.disable()) // 기본 로그인 폼 비활성화
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); // JWT 필터 추가
return http.build();
}
// JWT 인증 필터 클래스
class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
String token = authorizationHeader.substring(7); // "Bearer " 제거
if (JwtUtil.validateToken(token)) {
String userId = JwtUtil.extractUserId(token);
request.setAttribute("userId", userId); // 사용자 정보를 요청에 저장
}
}
filterChain.doFilter(request, response);
}
}
}
설정한 SecurityConfig은 다음과 같다.
기본적으로 인증 방식은 JWT는 헤더에 간단히 포함시켜 요청을 보내기만 하면 되고, 구현이 쉽고 간편한 JWT를 사용했다.
해당 코드들에 대해 간단히만 넘어가겠다.
1. @Configuration
이 클래스가 Spring 설정 클래스임을 나타내는 어노테이션이다. Bean 정의를 포함하는 클래스로, Spring 컨테이너에서 이 설정을 읽어 애플리케이션에 반영한다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // CSRF 비활성화
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll() // 모든 요청 허용 (테스트용)
)
.formLogin(form -> form.disable()) // 기본 로그인 폼 비활성화
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); // JWT 필터 추가
return http.build();
}
2. CSRF
REST API의 경우, CSRF가 필요하지 않으므로 비활성화하는 것이 일반적이기 때문에 CSRF(사이트 간 요청 위조) 보호 기능을 비활성화하였다.
3. authorizeHttpRequests
모든 요청을 허용한다. 테스트 용도로 사용하기 위해 우선 설정해 두었다. 실제 서비스에서는 permitAll() 대신 인증이 필요한 경로를 설정해야 한다. -> 해당 부분 설정으로 로그인 페이지를 넘기고 접근할 수 있다.
4. formLogin
JWT 인증 방식에서는 로그인 폼 대신 API 요청으로 로그인하기 때문에 Spring Security가 제공하는 기본 로그인 페이지를 비활성화한다.
5. addFilterBefore
UsernamePasswordAuthenticationFilter 앞에 실행되도록 설정해 JWT를 먼저 검증하기 위해 커스텀 필터인 JwtAuthenticationFilter를 Spring Security 필터 체인에 추가한다.
class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
String token = authorizationHeader.substring(7); // "Bearer " 제거
if (JwtUtil.validateToken(token)) {
String userId = JwtUtil.extractUserId(token);
request.setAttribute("userId", userId); // 사용자 정보를 요청에 저장
}
}
filterChain.doFilter(request, response);
}
}
이 필터는 모든 HTTP 요청에서 JWT 토큰을 검사하고, 유효한 경우 사용자 정보를 추출하여 요청에 저장하는 역할을 한다.
주요 동작:
1. Authorization 헤더에서 JWT 토큰을 가져온다.
2. Bearer로 시작하는 경우, 앞의 "Bearer " 문자열을 제거하고 순수 토큰만 추출한다.
3. 토큰을 검증(JwtUtil.validateToken)하고, 유효한 경우 사용자 ID를 추출(JwtUtil.extractUserId)한다.
4. 추출된 사용자 ID를 요청 속성(request.setAttribute)에 저장하여 컨트롤러나 서비스에서 활용 가능하게 만든다.
5. 다음 필터로 요청을 전달(filterChain.doFilter)한다.
해당 부분 설정해준 후, JwtUtil 클래스 생성후 JWT 생성, 유효성 검증, 사용자 정보 추출 기능을 구현하였다.
그후, Repository, Service, Controller, DTO를 작성하였다.
USER 로그인에 대해서는 일단, 직접 회원가입한 유저 기준으로 작성을 하였다. 추후 소셜 로그인 기능을 추가할 예정이다.
Service경우 추후 확장성을 위해 추상화하여 interface를 작성하고 ServiceImpl를 생성했다.
DTO의 경우 API request값을 명시하기 위해 사용하였다.
Test
// Swagger/OpenAPI 사용 (Springdoc)
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'
해당 Dependency를 사용하면, 다른 프로그램없이 API를 test 할 수 있다.
http://localhost:8080/swagger-ui/index.html -> 다음과 같이 작성하게 되면 만들어둔 API를 확인 할 수 있고, 테스트까지 가능하다.
회원 가입 test
로그인 test
로그인을 하게 되면 다음과 같이 JWT 토큰을 얻을 수 있다.
이를 활용하여
회원정보 수정 test
회원 정보를 변경할 수 있다.
추가적으로, 해당 코드를 통해 다음과 같이 인증 설정 같은 swagger에 설정을 진행 할 수 있다.
package com.gathering.gathering_backend.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI customOpenAPI() {
String securitySchemeName = "bearerAuth";
return new OpenAPI()
.addSecurityItem(new SecurityRequirement().addList(securitySchemeName)) // SecurityRequirement 생성
.components(new Components().addSecuritySchemes(securitySchemeName,
new SecurityScheme()
.name(securitySchemeName)
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")));
}
}
해당 코드들은
https://github.com/software-gathering/gather-be1
GitHub - software-gathering/gather-be1
Contribute to software-gathering/gather-be1 development by creating an account on GitHub.
github.com
여기서 확인 가능하다.
'Project > Gathering' 카테고리의 다른 글
Crawling 버그 수정 및 개선 (0) | 2024.12.18 |
---|---|
Gathering Backend 구현 1 (프로젝트 생성 및 엔티티 설계) (0) | 2024.11.26 |
Gathering 기획 3 (1) | 2024.11.21 |
Gathering 기획 2 (0) | 2024.11.16 |
Crawling 서버 구현 (1) | 2024.11.12 |