REST API용 JWT를 사용한 스프링 보안
게시 됨: 2022-03-11Spring은 Java 생태계에서 신뢰할 수 있는 프레임워크로 간주되며 널리 사용됩니다. Spring을 프레임워크로 지칭하는 것은 더 이상 유효하지 않습니다. Spring은 다양한 프레임워크를 포괄하는 포괄적인 용어이기 때문입니다. 이러한 프레임워크 중 하나는 강력하고 사용자 정의 가능한 인증 및 권한 부여 프레임워크인 Spring Security입니다. 이것은 Spring 기반 애플리케이션을 보호하기 위한 사실상의 표준으로 간주됩니다.
그 인기에도 불구하고 단일 페이지 응용 프로그램의 경우 구성하기가 간단하고 간단하지 않다는 것을 인정해야 합니다. 그 이유는 웹 페이지 렌더링이 서버 측에서 발생하고 통신이 세션 기반인 MVC 애플리케이션 지향 프레임워크로 시작했기 때문이라고 생각합니다.
백엔드가 Java 및 Spring을 기반으로 하는 경우 인증/권한 부여를 위해 Spring Security를 사용하고 상태 비저장 통신을 위해 구성하는 것이 합리적입니다. 이렇게 하는 방법을 설명하는 기사가 많이 있지만, 저에게는 처음 설정하는 것이 여전히 답답하고 여러 출처에서 정보를 읽고 요약해야 했습니다. 이것이 내가 이 기사를 작성하기로 결정한 이유입니다. 여기에서 구성 프로세스 중에 발생할 수 있는 모든 필수 세부 사항과 결점을 요약하고 다루려고 합니다.
용어 정의
기술적인 세부 사항에 대해 알아보기 전에 우리 모두가 동일한 언어를 사용하는지 확인하기 위해 Spring Security 컨텍스트에서 사용되는 용어를 명시적으로 정의하고 싶습니다.
해결해야 할 용어는 다음과 같습니다.
- 인증 은 제공된 자격 증명을 기반으로 사용자의 신원을 확인하는 프로세스를 말합니다. 일반적인 예는 웹 사이트에 로그인할 때 사용자 이름과 암호를 입력하는 것입니다. 당신은 누구입니까? 라는 질문에 대한 대답으로 생각할 수 있습니다. .
- 권한 부여 는 사용자가 성공적으로 인증되었다는 가정 하에 특정 작업을 수행하거나 특정 데이터를 읽을 수 있는 적절한 권한이 사용자에게 있는지 확인하는 프로세스를 나타냅니다. 사용자가 이것을 읽을 수 있습니까? 라는 질문에 대한 답변으로 생각할 수 있습니다. .
- 원칙 은 현재 인증된 사용자를 나타냅니다.
- 부여된 권한 은 인증된 사용자의 권한을 의미합니다.
- 역할 은 인증된 사용자의 권한 그룹을 나타냅니다.
기본 스프링 애플리케이션 생성
Spring Security 프레임워크의 설정으로 넘어가기 전에 기본적인 Spring 웹 애플리케이션을 생성해보자. 이를 위해 Spring Initializr를 사용하고 템플릿 프로젝트를 생성할 수 있습니다. 간단한 웹 애플리케이션의 경우 Spring 웹 프레임워크 종속성만 있으면 충분합니다.
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
프로젝트를 생성하고 나면 다음과 같이 간단한 REST 컨트롤러를 추가할 수 있습니다.
@RestController @RequestMapping("hello") public class HelloRestController { @GetMapping("user") public String helloUser() { return "Hello User"; } @GetMapping("admin") public String helloAdmin() { return "Hello Admin"; } }
그런 다음 프로젝트를 빌드하고 실행하면 웹 브라우저에서 다음 URL에 액세스할 수 있습니다.
-
http://localhost:8080/hello/user
는Hello User
문자열을 반환합니다. -
http://localhost:8080/hello/admin
은Hello Admin
문자열을 반환합니다.
이제 프로젝트에 Spring Security 프레임워크를 추가할 수 있으며 pom.xml
파일에 다음 종속성을 추가하여 이를 수행할 수 있습니다.
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>
다른 Spring 프레임워크 종속성을 추가하는 것은 일반적으로 해당 구성을 제공할 때까지 애플리케이션에 즉각적인 영향을 미치지 않지만 Spring Security는 즉각적인 영향을 미친다는 점에서 다르며 이는 일반적으로 새로운 사용자를 혼란스럽게 합니다. 추가한 후 프로젝트를 다시 빌드하고 실행한 다음 결과를 보는 대신 앞서 언급한 URL 중 하나에 액세스하려고 하면 http://localhost:8080/login
으로 리디렉션됩니다. Spring Security 프레임워크는 모든 URL에 대해 기본적으로 인증을 요구하기 때문에 이것이 기본 동작입니다.
인증을 통과하기 위해 기본 사용자 이름 user
를 사용하고 콘솔에서 자동 생성된 비밀번호를 찾을 수 있습니다.
Using generated security password: 1fc15145-dfee-4bec-a009-e32ca21c77ce
응용 프로그램을 다시 실행할 때마다 암호가 변경된다는 점을 기억하십시오. 이 동작을 변경하고 비밀번호를 정적으로 만들려면 다음 구성을 application.properties
파일에 추가할 수 있습니다.
spring.security.user.password=Test12345_
이제 로그인 양식에 자격 증명을 입력하면 URL로 다시 리디렉션되고 올바른 결과가 표시됩니다. 기본 인증 프로세스는 세션 기반이며 로그아웃하려는 경우 다음 URL에 액세스할 수 있습니다. http://localhost:8080/logout
이 즉시 사용 가능한 동작은 세션 기반 인증이 있는 클래식 MVC 웹 애플리케이션에 유용할 수 있지만 단일 페이지 애플리케이션의 경우 대부분의 사용 사례에서 클라이언트 측 인증이 있기 때문에 일반적으로 유용하지 않습니다. 렌더링 및 JWT 기반 상태 비저장 인증. 이 경우 우리는 Spring Security 프레임워크를 크게 커스터마이징해야 하며, 이는 기사의 나머지 부분에서 할 것입니다.
예를 들어, 우리는 고전적인 서점 웹 애플리케이션을 구현하고 CRUD API를 제공하여 저자와 책과 사용자 관리 및 인증을 위한 API를 제공하는 백엔드를 만들 것입니다.
스프링 보안 아키텍처 개요
구성 사용자 정의를 시작하기 전에 먼저 Spring Security 인증이 배후에서 작동하는 방식에 대해 논의하겠습니다.
다음 다이어그램은 흐름을 나타내고 인증 요청이 처리되는 방식을 보여줍니다.
스프링 보안 아키텍처

이제 이 다이어그램을 구성 요소로 분해하고 각 구성 요소에 대해 개별적으로 논의하겠습니다.
스프링 보안 필터 체인
Spring Security 프레임워크를 애플리케이션에 추가하면 들어오는 모든 요청을 가로채는 필터 체인을 자동으로 등록합니다. 이 체인은 다양한 필터로 구성되며 각 필터는 특정 사용 사례를 처리합니다.
예를 들어:
- 구성에 따라 요청된 URL이 공개적으로 액세스 가능한지 확인합니다.
- 세션 기반 인증의 경우 현재 세션에서 사용자가 이미 인증되었는지 확인합니다.
- 사용자가 요청한 작업 등을 수행할 수 있는 권한이 있는지 확인합니다.
내가 언급하고 싶은 한 가지 중요한 세부 사항은 Spring Security 필터가 가장 낮은 순서로 등록되고 가장 먼저 호출되는 필터라는 것입니다. 일부 사용 사례의 경우 사용자 정의 필터를 앞에 두고 싶다면 순서에 패딩을 추가해야 합니다. 이것은 다음 구성으로 수행할 수 있습니다.
spring.security.filter.order=10
이 구성을 application.properties
파일에 추가하면 Spring Security 필터 앞에 10개의 사용자 정의 필터를 위한 공간이 생깁니다.
인증 관리자
AuthenticationManager
를 여러 공급자를 등록할 수 있는 조정자로 생각할 수 있으며 요청 유형에 따라 인증 요청을 올바른 공급자에게 전달합니다.
인증 제공자
AuthenticationProvider
는 특정 유형의 인증을 처리합니다. 인터페이스는 다음 두 가지 기능만 노출합니다.
-
authenticate
은 요청으로 인증을 수행합니다. - 이 공급자가 표시된 인증 유형을
supports
하는지 확인을 지원합니다.
샘플 프로젝트에서 사용하고 있는 인터페이스의 중요한 구현 중 하나는 DaoAuthenticationProvider
이며, 이는 UserDetailsService
에서 사용자 세부 정보를 검색합니다.
사용자 세부 정보 서비스
UserDetailsService
는 Spring 문서에서 사용자별 데이터를 로드하는 핵심 인터페이스로 설명됩니다.
대부분의 사용 사례에서 인증 공급자는 데이터베이스에서 자격 증명을 기반으로 사용자 ID 정보를 추출한 다음 유효성 검사를 수행합니다. 이 사용 사례가 너무 일반적이기 때문에 Spring 개발자는 단일 기능을 노출하는 별도의 인터페이스로 추출하기로 결정했습니다.
-
loadUserByUsername
은 사용자 이름을 매개변수로 받아들이고 사용자 ID 개체를 반환합니다.
Spring Security와 함께 JWT를 사용한 인증
Spring Security 프레임워크의 내부에 대해 논의한 후 JWT 토큰을 사용하여 상태 비저장 인증을 위해 구성해 보겠습니다.
Spring Security를 사용자 정의하려면 클래스 경로에 @EnableWebSecurity
주석으로 주석이 달린 구성 클래스가 필요합니다. 또한 사용자 지정 프로세스를 단순화하기 위해 프레임워크는 WebSecurityConfigurerAdapter
클래스를 노출합니다. 이 어댑터를 확장하고 두 기능을 모두 재정의하여 다음과 같이 할 것입니다.
- 올바른 공급자로 인증 관리자 구성
- 웹 보안 설정(공개 URL, 비공개 URL, 권한 부여 등)
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // TODO configure authentication manager } @Override protected void configure(HttpSecurity http) throws Exception { // TODO configure web security } }
샘플 애플리케이션에서는 users
컬렉션의 MongoDB 데이터베이스에 사용자 ID를 저장합니다. 이러한 ID는 User
엔터티에 의해 매핑되고 CRUD 작업은 UserRepo
Spring Data 저장소에 의해 정의됩니다.
이제 인증 요청을 수락할 때 제공된 자격 증명을 사용하여 데이터베이스에서 올바른 ID를 검색한 다음 확인해야 합니다. 이를 위해 다음과 같이 정의된 UserDetailsService
인터페이스의 구현이 필요합니다.
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
여기에서 UserDetails
인터페이스를 구현하는 객체를 반환해야 하고 User
엔터티가 이를 구현한다는 것을 알 수 있습니다(구현 세부 정보는 샘플 프로젝트의 저장소 참조). 단일 기능 프로토타입만 노출한다는 사실을 고려하면 이를 기능적 인터페이스로 취급하고 람다 식으로 구현을 제공할 수 있습니다.
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; public SecurityConfig(UserRepo userRepo) { this.userRepo = userRepo; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(username -> userRepo .findByUsername(username) .orElseThrow( () -> new UsernameNotFoundException( format("User: %s, not found", username) ) )); } // Details omitted for brevity }
여기에서 auth.userDetailsService
함수 호출은 UserDetailsService
인터페이스 구현을 사용하여 DaoAuthenticationProvider
인스턴스를 시작하고 인증 관리자에 등록합니다.
인증 공급자와 함께 자격 증명 확인에 사용할 올바른 암호 인코딩 스키마로 인증 관리자를 구성해야 합니다. 이를 위해 우리는 PasswordEncoder
인터페이스의 선호하는 구현을 빈으로 노출해야 합니다.
샘플 프로젝트에서는 bcrypt 암호 해싱 알고리즘을 사용합니다.
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; public SecurityConfig(UserRepo userRepo) { this.userRepo = userRepo; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(username -> userRepo .findByUsername(username) .orElseThrow( () -> new UsernameNotFoundException( format("User: %s, not found", username) ) )); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // Details omitted for brevity }
인증 관리자를 구성했으면 이제 웹 보안을 구성해야 합니다. REST API를 구현 중이며 JWT 토큰을 사용한 상태 비저장 인증이 필요합니다. 따라서 다음 옵션을 설정해야 합니다.
- CORS를 활성화하고 CSRF를 비활성화합니다.
- 세션 관리를 상태 비저장으로 설정하십시오.
- 승인되지 않은 요청 예외 처리기를 설정합니다.
- 끝점에 대한 권한을 설정합니다.
- JWT 토큰 필터를 추가합니다.
이 구성은 다음과 같이 구현됩니다.
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; private final JwtTokenFilter jwtTokenFilter; public SecurityConfig(UserRepo userRepo, JwtTokenFilter jwtTokenFilter) { this.userRepo = userRepo; this.jwtTokenFilter = jwtTokenFilter; } // Details omitted for brevity @Override protected void configure(HttpSecurity http) throws Exception { // Enable CORS and disable CSRF http = http.cors().and().csrf().disable(); // Set session management to stateless http = http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and(); // Set unauthorized requests exception handler http = http .exceptionHandling() .authenticationEntryPoint( (request, response, ex) -> { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage() ); } ) .and(); // Set permissions on endpoints http.authorizeRequests() // Our public endpoints .antMatchers("/api/public/**").permitAll() .antMatchers(HttpMethod.GET, "/api/author/**").permitAll() .antMatchers(HttpMethod.POST, "/api/author/search").permitAll() .antMatchers(HttpMethod.GET, "/api/book/**").permitAll() .antMatchers(HttpMethod.POST, "/api/book/search").permitAll() // Our private endpoints .anyRequest().authenticated(); // Add JWT token filter http.addFilterBefore( jwtTokenFilter, UsernamePasswordAuthenticationFilter.class ); } // Used by spring security if CORS is enabled. @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }
Spring Security 내부 UsernamePasswordAuthenticationFilter
전에 JwtTokenFilter
를 추가했음을 유의하십시오. 인증/권한 부여를 수행하기 위해 이 시점에서 사용자 ID에 대한 액세스가 필요하고 제공된 JWT 토큰을 기반으로 JWT 토큰 필터 내에서 추출이 발생하기 때문에 이 작업을 수행합니다. 이것은 다음과 같이 구현됩니다.
@Component public class JwtTokenFilter extends OncePerRequestFilter { private final JwtTokenUtil jwtTokenUtil; private final UserRepo userRepo; public JwtTokenFilter(JwtTokenUtil jwtTokenUtil, UserRepo userRepo) { this.jwtTokenUtil = jwtTokenUtil; this.userRepo = userRepo; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // Get authorization header and validate final String header = request.getHeader(HttpHeaders.AUTHORIZATION); if (isEmpty(header) || !header.startsWith("Bearer ")) { chain.doFilter(request, response); return; } // Get jwt token and validate final String token = header.split(" ")[1].trim(); if (!jwtTokenUtil.validate(token)) { chain.doFilter(request, response); return; } // Get user identity and set it on the spring security context UserDetails userDetails = userRepo .findByUsername(jwtTokenUtil.getUsername(token)) .orElse(null); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails == null ? List.of() : userDetails.getAuthorities() ); authentication.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(request, response); } }
로그인 API 기능을 구현하기 전에 한 단계 더 처리해야 합니다. 인증 관리자에 대한 액세스 권한이 필요합니다. 기본적으로 공개적으로 액세스할 수 없으며 구성 클래스에서 이를 Bean으로 명시적으로 노출해야 합니다.

이것은 다음과 같이 할 수 있습니다:
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
이제 로그인 API 기능을 구현할 준비가 되었습니다.
@Api(tags = "Authentication") @RestController @RequestMapping(path = "api/public") public class AuthApi { private final AuthenticationManager authenticationManager; private final JwtTokenUtil jwtTokenUtil; private final UserViewMapper userViewMapper; public AuthApi(AuthenticationManager authenticationManager, JwtTokenUtil jwtTokenUtil, UserViewMapper userViewMapper) { this.authenticationManager = authenticationManager; this.jwtTokenUtil = jwtTokenUtil; this.userViewMapper = userViewMapper; } @PostMapping("login") public ResponseEntity<UserView> login(@RequestBody @Valid AuthRequest request) { try { Authentication authenticate = authenticationManager .authenticate( new UsernamePasswordAuthenticationToken( request.getUsername(), request.getPassword() ) ); User user = (User) authenticate.getPrincipal(); return ResponseEntity.ok() .header( HttpHeaders.AUTHORIZATION, jwtTokenUtil.generateAccessToken(user) ) .body(userViewMapper.toUserView(user)); } catch (BadCredentialsException ex) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } } }
여기에서 인증 관리자를 사용하여 제공된 자격 증명을 확인하고 성공할 경우 JWT 토큰을 생성하여 응답 본문의 사용자 ID 정보와 함께 응답 헤더로 반환합니다.
스프링 시큐리티로 권한 부여
이전 섹션에서는 인증 프로세스를 설정하고 공개/비공개 URL을 구성했습니다. 단순한 애플리케이션에는 이것으로 충분할 수 있지만 대부분의 실제 사용 사례에서는 항상 사용자를 위한 역할 기반 액세스 정책이 필요합니다. 이 장에서는 이 문제를 해결하고 Spring Security 프레임워크를 사용하여 역할 기반 인증 스키마를 설정합니다.
샘플 애플리케이션에서 다음 세 가지 역할을 정의했습니다.
-
USER_ADMIN
을 통해 애플리케이션 사용자를 관리할 수 있습니다. -
AUTHOR_ADMIN
을 통해 작성자를 관리할 수 있습니다. -
BOOK_ADMIN
을 사용하면 책을 관리할 수 있습니다.
이제 해당 URL에 적용해야 합니다.
-
api/public
은 공개적으로 액세스할 수 있습니다. -
api/admin/user
는USER_ADMIN
역할을 가진 사용자에 액세스할 수 있습니다. -
api/author
는AUTHOR_ADMIN
역할을 가진 사용자에 액세스할 수 있습니다. -
api/book
은BOOK_ADMIN
역할을 가진 사용자에 액세스할 수 있습니다.
Spring Security 프레임워크는 인증 스키마를 설정하는 두 가지 옵션을 제공합니다.
- URL 기반 구성
- 주석 기반 구성
먼저 URL 기반 구성이 작동하는 방식을 살펴보겠습니다. 다음과 같이 웹 보안 구성에 적용할 수 있습니다.
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Override protected void configure(HttpSecurity http) throws Exception { // Enable CORS and disable CSRF http = http.cors().and().csrf().disable(); // Set session management to stateless http = http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and(); // Set unauthorized requests exception handler http = http .exceptionHandling() .authenticationEntryPoint( (request, response, ex) -> { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage() ); } ) .and(); // Set permissions on endpoints http.authorizeRequests() // Our public endpoints .antMatchers("/api/public/**").permitAll() .antMatchers(HttpMethod.GET, "/api/author/**").permitAll() .antMatchers(HttpMethod.POST, "/api/author/search").permitAll() .antMatchers(HttpMethod.GET, "/api/book/**").permitAll() .antMatchers(HttpMethod.POST, "/api/book/search").permitAll() // Our private endpoints .antMatchers("/api/admin/user/**").hasRole(Role.USER_ADMIN) .antMatchers("/api/author/**").hasRole(Role.AUTHOR_ADMIN) .antMatchers("/api/book/**").hasRole(Role.BOOK_ADMIN) .anyRequest().authenticated(); // Add JWT token filter http.addFilterBefore( jwtTokenFilter, UsernamePasswordAuthenticationFilter.class ); } // Details omitted for brevity }
보시다시피 이 접근 방식은 간단하고 간단하지만 한 가지 단점이 있습니다. 우리 애플리케이션의 권한 부여 스키마는 복잡할 수 있으며 모든 규칙을 한 곳에서 정의하면 매우 크고 복잡하며 읽기 어려울 것입니다. 이 때문에 일반적으로 주석 기반 구성을 사용하는 것을 선호합니다.
Spring Security 프레임워크는 웹 보안을 위해 다음 주석을 정의합니다.
-
@PreAuthorize
는 Spring Expression Language를 지원하며 메소드를 실행 하기 전에 표현식 기반 접근 제어를 제공하는 데 사용됩니다. -
@PostAuthorize
는 Spring Expression Language를 지원하며 메소드 실행 후 표현식 기반 접근 제어를 제공하기 위해 사용된다(메소드 결과에 접근할 수 있는 기능 제공). -
@PreFilter
는 Spring Expression Language를 지원하며 우리가 정의한 사용자 정의 보안 규칙을 기반으로 메소드를 실행 하기 전에 컬렉션 또는 배열을 필터링하는 데 사용됩니다. -
@PostFilter
는 Spring Expression Language를 지원하며 우리가 정의한 사용자 정의 보안 규칙을 기반으로 메소드를 실행한 후 반환된 컬렉션 또는 배열을 필터링하는 데 사용됩니다(메소드 결과에 액세스할 수 있는 기능 제공). -
@Secured
는 Spring Expression Language를 지원하지 않으며 메소드의 역할 목록을 지정하는 데 사용됩니다. -
@RolesAllowed
는 Spring Expression Language를 지원하지 않으며@Secured
주석의 JSR 250에 해당하는 주석입니다.
이러한 주석은 기본적으로 비활성화되어 있으며 다음과 같이 애플리케이션에서 활성화할 수 있습니다.
@EnableWebSecurity @EnableGlobalMethodSecurity( securedEnabled = true, jsr250Enabled = true, prePostEnabled = true ) public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity }
securedEnabled = true
는 @Secured
주석을 활성화합니다.
jsr250Enabled = true
는 @RolesAllowed
주석을 활성화합니다.
@PreAuthorize
prePostEnabled = true
는 @PreAuthorize , @PostAuthorize
, @PreFilter
, @PostFilter
주석을 활성화합니다.
활성화한 후에는 다음과 같이 API 엔드포인트에서 역할 기반 액세스 정책을 시행할 수 있습니다.
@Api(tags = "UserAdmin") @RestController @RequestMapping(path = "api/admin/user") @RolesAllowed(Role.USER_ADMIN) public class UserAdminApi { // Details omitted for brevity } @Api(tags = "Author") @RestController @RequestMapping(path = "api/author") public class AuthorApi { // Details omitted for brevity @RolesAllowed(Role.AUTHOR_ADMIN) @PostMapping public void create() { } @RolesAllowed(Role.AUTHOR_ADMIN) @PutMapping("{id}") public void edit() { } @RolesAllowed(Role.AUTHOR_ADMIN) @DeleteMapping("{id}") public void delete() { } @GetMapping("{id}") public void get() { } @GetMapping("{id}/book") public void getBooks() { } @PostMapping("search") public void search() { } } @Api(tags = "Book") @RestController @RequestMapping(path = "api/book") public class BookApi { // Details omitted for brevity @RolesAllowed(Role.BOOK_ADMIN) @PostMapping public BookView create() { } @RolesAllowed(Role.BOOK_ADMIN) @PutMapping("{id}") public void edit() { } @RolesAllowed(Role.BOOK_ADMIN) @DeleteMapping("{id}") public void delete() { } @GetMapping("{id}") public void get() { } @GetMapping("{id}/author") public void getAuthors() { } @PostMapping("search") public void search() { } }
보안 주석은 클래스 수준과 메서드 수준 모두에서 제공될 수 있습니다.
시연된 예제는 단순하고 실제 시나리오를 나타내지 않지만 Spring Security는 풍부한 주석 세트를 제공하며 이를 사용하도록 선택하면 복잡한 권한 부여 스키마를 처리할 수 있습니다.
역할 이름 기본 접두사
이 별도의 하위 섹션에서는 많은 신규 사용자를 혼란스럽게 하는 미묘한 세부 사항을 한 가지 더 강조하고 싶습니다.
Spring Security 프레임워크는 두 가지 용어를 구별합니다.
-
Authority
은 개별 권한을 나타냅니다. -
Role
은 권한 그룹을 나타냅니다.
둘 다 GrantedAuthority
라는 단일 인터페이스로 표시될 수 있으며 나중에 다음과 같이 Spring Security 주석 내부에서 Spring Expression Language로 확인할 수 있습니다.
-
Authority
: @PreAuthorize(“hasAuthority('EDIT_BOOK')”) -
Role
: @PreAuthorize(“hasRole('BOOK_ADMIN')”)
이 두 용어의 차이를 보다 명확하게 하기 위해 Spring Security 프레임워크는 기본적으로 역할 이름에 ROLE_
접두사를 추가합니다. 따라서 BOOK_ADMIN
ROLE_BOOK_ADMIN
확인합니다.
개인적으로 나는 이 동작이 혼란스럽다고 생각하고 내 애플리케이션에서 비활성화하는 것을 선호합니다. 다음과 같이 Spring Security 구성 내에서 비활성화할 수 있습니다.
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Bean GrantedAuthorityDefaults grantedAuthorityDefaults() { return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix } }
스프링 시큐리티로 테스트하기
Spring Security 프레임워크를 사용할 때 단위 또는 통합 테스트로 엔드포인트를 테스트하려면 spring-boot-starter-test
와 함께 spring-security-test
종속성을 추가해야 합니다. pom.xml
빌드 파일은 다음과 같습니다.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency>
이 종속성은 테스트 기능에 보안 컨텍스트를 추가하는 데 사용할 수 있는 일부 주석에 대한 액세스를 제공합니다.
이러한 주석은 다음과 같습니다.
-
@WithMockUser
를 테스트 메서드에 추가하여 모의 사용자와 함께 실행하는 것을 에뮬레이트할 수 있습니다. -
@WithUserDetails
를 테스트 메서드에 추가하여UserDetailsService
에서 반환된UserDetails
로 실행을 에뮬레이트할 수 있습니다. -
@WithAnonymousUser
를 테스트 메서드에 추가하여 익명 사용자와 함께 실행하는 것을 에뮬레이트할 수 있습니다. 이것은 사용자가 특정 사용자로 대부분의 테스트를 실행하고 몇 가지 메서드를 익명으로 재정의하려는 경우에 유용합니다. -
@WithSecurityContext
는 사용할SecurityContext
를 결정하며 위에서 설명한 세 가지 주석은 모두 이를 기반으로 합니다. 특정 사용 사례가 있는 경우@WithSecurityContext
를 사용하여 원하는SecurityContext
를 만드는 자체 주석을 만들 수 있습니다. 이에 대한 논의는 우리 기사의 범위를 벗어나며 자세한 내용은 Spring Security 문서를 참조하십시오.
특정 사용자로 테스트를 실행하는 가장 쉬운 방법은 @WithMockUser
주석을 사용하는 것입니다. 이를 사용하여 모의 사용자를 만들고 다음과 같이 테스트를 실행할 수 있습니다.
@Test @WithMockUser(username="[email protected]", roles={"USER_ADMIN"}) public void test() { // Details omitted for brevity }
그러나 이 접근 방식에는 몇 가지 단점이 있습니다. 첫째, 모의 사용자가 존재하지 않으며, 나중에 데이터베이스에서 사용자 정보를 조회하는 통합 테스트를 실행하면 테스트가 실패합니다. 둘째, 모의 사용자는 org.springframework.security.core.userdetails.User
클래스의 인스턴스이며, 이는 Spring 프레임워크의 UserDetails
인터페이스의 내부 구현이며 자체 구현이 있는 경우 나중에 충돌이 발생할 수 있습니다. 테스트 실행.
이전 단점이 우리 애플리케이션의 차단기 @WithUserDetails
주석이 갈 길입니다. 사용자 정의 UserDetails
및 UserDetailsService
구현이 있을 때 사용됩니다. 사용자가 존재한다고 가정하므로 테스트를 실행하기 전에 데이터베이스에 실제 행을 생성하거나 UserDetailsService
모의 인스턴스를 제공해야 합니다.
이 주석을 사용하는 방법은 다음과 같습니다.
@Test @WithUserDetails("[email protected]") public void test() { // Details omitted for brevity }
앞서 언급한 인터페이스의 사용자 정의 구현이 있기 때문에 이것은 샘플 프로젝트의 통합 테스트에서 선호되는 주석입니다.
@WithAnonymousUser
를 사용하면 익명 사용자로 실행할 수 있습니다. 이것은 특정 사용자로 대부분의 테스트를 실행하고 익명 사용자로 몇 가지 테스트를 실행하려는 경우에 특히 편리합니다. 예를 들어 다음은 모의 사용자로 test1 및 test2 테스트 케이스를 실행하고 익명 사용자로 test3 을 실행합니다.
@SpringBootTest @AutoConfigureMockMvc @WithMockUser public class WithUserClassLevelAuthenticationTests { @Test public void test1() { // Details omitted for brevity } @Test public void test2() { // Details omitted for brevity } @Test @WithAnonymousUser public void test3() throws Exception { // Details omitted for brevity } }
마무리
결국 Spring Security 프레임워크는 미인 대회에서 우승하지 못할 것이며 확실히 학습 곡선이 가파르다는 것을 언급하고 싶습니다. 초기 구성 복잡성으로 인해 자체 개발한 솔루션으로 대체된 많은 상황이 발생했습니다. 그러나 일단 개발자가 내부를 이해하고 초기 구성을 설정하면 사용하기가 비교적 간단해집니다.
이 기사에서는 구성의 모든 미묘한 세부 사항을 보여 주려고 노력했으며 예제가 유용하기를 바랍니다. 전체 코드 예제는 내 샘플 Spring Security 프로젝트의 Git 리포지토리를 참조하십시오.