반응형

 

 

이전 포스팅에서 Spring Boot에서 JWT를 이용한 로그인 인증을 구현하였다.

이번에는 로그인한 사용자에 한해서 API 사용을 승인하는 API 인가(Authorization), 더불어 권한별로 인가를 구현하는 방법을 알아보도록 하겠다.

 

JWT 토큰 인증 방식에 대해서는 아래의 포스팅을 참고하면 좋을 것 같다.

 

https://sjh9708.tistory.com/46

 

[Web] 인증과 인가 - JWT 토큰 인증

앞 포스팅에서 세션 방식의 인증과, 성능 개선을 위한 방법들에 대해서 다루어 보았었는데 이번에는 언급했던 토큰 인증 방식에 대해서 알아보려고 한다. 토큰 인증 세션 인증 방식과 달리 인증

sjh9708.tistory.com

 

 

 

<이전 포스팅> Spring Security JWT 인증과 인가 - (1) 회원 가입

https://sjh9708.tistory.com/83

 

[SpringBoot] Spring Security JWT 인증과 인가 - (1) 회원 가입

이번 포스팅에서는 Spring Boot에서 JWT를 이용한 인증과 인가를 다루기 이전, 사용자를 회원가입시키는 API 로직을 작성하고, 비밀번호를 해시처리하는 작업을 우선적으로 진행해보도록 하겠다. JWT

sjh9708.tistory.com

 

<이전 포스팅> Spring Security JWT 인증과 인가 - (2) 로그인 인증 (Authentication)

https://sjh9708.tistory.com/84

 

[SpringBoot] Spring Security JWT 인증과 인가 - (2) 로그인 인증 (Authentication)

이번 포스팅에서는 Spring Boot에서 JWT를 이용한 로그인 인증을 구현해보도록 하겠다. JWT 토큰 인증 방식에 대해서는 아래의 포스팅을 참고하면 좋을 것 같다. https://sjh9708.tistory.com/46 [Web] 인증과

sjh9708.tistory.com

 


2024-01-14 : 유의사항

해당 포스팅은 Spring 2점 버전대를 기준으로 작성하였다.

2023년 11월 경, Spring 2점대 버전의 지원이 공식 중단되면서, 이제 웬만하면 Spring 3 버전대를 사용할 것을 Spring 진영에서 권장하고 있다.

Spring Security의 경우 사용 방식이 변화한 것이 있어, Spring 3에서의 JWT 인증과 인가에 대해서 따로 포스팅을 작성하였다. 만약 Spring 3점대 버전을 사용한다면 다음 포스팅을 참고하면 좋을 것 같다.

 

https://sjh9708.tistory.com/170

 

[Spring Boot] JWT Auth (SpringBoot 3 버전 Spring Security)

작년 말, Spring 2점대 버전의 지원이 공식 중단되면서, 이제 웬만하면 Spring 3 버전대를 사용할 것을 Spring 진영에서 권장하고 있다. 그 중 Spring Security의 경우 변화한 내용이 조금 있는 편이라 이

sjh9708.tistory.com

 


JWT 인증 필터 클래스 구현

 

인가를 적용시키기 위해서, API에 접근하기 전에, Request Header에 있는 JWT 토큰을 추출하여, 토큰의 내용을 검증, 즉 인증 내용을 검증하는 필터를 작성해보겠다.

 

 

JwtAuthenticationFilter.java

/common/auth/JwtAuthenticationFilter.java

/**
 * [JWT 인증 커스텀 필터 클래스]
 */
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {

    private final JwtTokenProvider jwtTokenProvider;


    /**
     * [요청 시 거치는 필터 로직]
     * Request는 다음 로직을 통과함.
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        // 1. Request Header 에서 JWT 토큰 추출
        String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);

        // 2. validateToken 으로 토큰 유효성 검사
        if (token != null && jwtTokenProvider.validateToken(token)) {
            // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext 에 저장
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }


}

GenericFilterBean은 Spring Framework에서 제공하는 추상 클래스로서, 일반적인 필터 구현을 간편하게 만들 수 있도록 도와준다.

JwtTokenProvider는 이전 포스팅에서 작성했던 JWT를 다루는 클래스이다.

 

doFilter(ServletRequest request, ServletResponse response, FilterChain chain)

  • 필터가 요청을 처리하는 핵심 로직이 담긴 메서드. 
  • JwtTokenProvider의 resolveToken 메서드를 사용하여 HTTP 요청의 헤더에서 JWT 토큰을 추출한다.
  • JwtTokenProvider의 validateToken 메서드를 사용하여 토큰의 유효성을 검사한다.

  • Authentication authentication = jwtTokenProvider.getAuthentication(token)
    • 검증된 토큰을 기반으로 JwtTokenProvider의 getAuthentication 메서드를 호출하여 Authentication 객체를 가져온다.

  • SecurityContextHolder.getContext().setAuthentication(authentication)
    • Authentication 객체를 Spring Security의 SecurityContext에 설정.
    • 해당 요청에 대한 인증 성공 / 실패 및 사용자 정보를 포함한 인증 정보가 컨텍스트에 저장된다. 

  • chain.doFilter(request, response)
    • 또다른 필터가 있다면 다음 필터로 요청을 전달한다.

 

 

 


Security Config 수정

 

이제 요청에 필터를 적용시키기 위해서 작성했던 SecurityConfig를 수정한다.

 

 

 SecurityConfig.java

/common/auth/SecurityConfig.java

/**
 * [Spring Security Config 클래스]
 */
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final JwtTokenProvider jwtTokenProvider;
    private final CustomUserDetailsService customUserDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(customUserDetailsService)
                .passwordEncoder(passwordEncoder()); // Set the BCryptPasswordEncoder for password encoding
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
                .authorizeRequests()
                .antMatchers("/swagger-resources/**").permitAll()
                .antMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
                .anyRequest().permitAll();

        http
                .headers()
                .frameOptions()
                .sameOrigin();

        http
                .httpBasic().disable()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .cors()
                .and()
                .formLogin().disable()
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
    }


    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

 

앞에서 작성한 필터를 addFilterBefore()를 작성해서 적용시켜주었다. 

 

Spring Security는 요청 처리 시 여러개의 필터를 거치게 되는 필터 체인 형식으로 구성되어 있다., 해당 메서드는 특정 필터를 필터 체인의 특정 위치에 끼워넣어주는 역할을 한다.

 

UsernamePasswordAuthenticationFilter는 로그인 인증을 담당하는 필터로, 사용자가 로그인을 시도할 때 이 필터가 실행된다. JwtAuthenticationFilter는 JWT 토큰을 검증하고 인증 정보를 설정하는 역할을 하는데, 이 필터가 먼저 실행되도록 설정하여 우선적으로 JWT 토큰 검증과 관련된 작업을 수행하도록 하였다.

 

 

 

@EnableGlobalMethodSecurity 어노테이션은 Spring Security의 메서드 수준 보안 설정을 활성화한다.

즉 Annotation을 통해서 Controller의 API들의 보안 수준을 설정할 수 있게 되는 것이다.

 

 

 


인증 정보 얻어오기

 

 

 SecurityUtil.java

/common/auth/SecurityUtil.java

public class SecurityUtil {

    /**
     * [인증 유저의 이메일]
     * 현재 로그인한 유저의 이메일 정보를 얻어옴
     * @return [String] Email
     */
    public static String getCurrentMemberEmail() {
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || authentication.getName() == null) {
            throw new RuntimeException("No authentication information.");
        }
        return authentication.getName();
    }


    /**
     * [인증 유저의 PK]
     * 현재 로그인한 유저의 PK를 얻어옴
     * @return [String] PK
     */
    public static Long getCurrentMemberPk() {
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || authentication.getName() == null) {
            throw new RuntimeException("No authentication information.");
        }
        CustomUserDetail userDetails = (CustomUserDetail) authentication.getPrincipal();
        Long userPk = userDetails.getUserId();
        return userPk;
    }
}

요청 시 포함되는 인증 정보를 추출하는 Util 클래스를 다음과 같이 작성할 수 있다.

아래의 코드는 API 인가가 성공했을 때, Authentication 객체의 Name과, Principle을 추출하여 이메일과 회원 PK를 얻어오는 메서드를 정의한 것이다.

 

 


API 인가와 보안 수준 설정하기

 

public class AuthConstant {

    /**
     * API Auth Role : 공통
     */
    public static final String AUTH_ROLE_COMMON_USER = "hasRole('ROLE_CONSUMER') or hasRole('ROLE_SELLER')";

    /**
     * API Auth Role : 손님
     */
    public static final String AUTH_ROLE_CONSUMER = "hasRole('ROLE_CONSUMER')";

    /**
     * API Auth Role : 매장
     */
    public static final String AUTH_ROLE_SELLER = "hasRole('ROLE_SELLER')";



}
    @ApiOperation(value = "샘플 API - 현재 로그인한 Member의 PK와 이메일 가져오기")
    @PreAuthorize(AuthConstant.AUTH_ROLE_COMMON_USER)
    @GetMapping("/user")
    public ResponseEntity<String> authSample(
    ){
        Long pk = SecurityUtil.getCurrentMemberPk();
        String email = SecurityUtil.getCurrentMemberEmail();
        String result = String.format("%d-%s", pk, email);
        return ResponseEntity.status(HttpStatus.OK).body(result);
    }

SecurityConfig에서 @EnableGlobalMethodSecurity을 설정함에 따라서 어노테이션을 통한 Authentication 보안 수준 설정이 가능해졌다.

 

@PreAuthorize 어노테이션을 통해 Security를 통한 보안 수준을 통과해야 해당 API를 사용할 수 있도록 설정해주었다.

안의 인자로는 Role, 권한을 넣어주었으며 해당 API의 경우 로그인 사용자의 Role이 SELLER 혹은 CONSUMER이어야 해당 API를 사용할 수 있도록 지정해주었다.

 

SecurityUtil을 통해서 인증된 사용자의 이메일을 얻어오는 로직을 API 내부에 담아보았다.

 

 

 

 


Swagger에서 API 인가 설정

 

 

@Configuration
public class SwaggerConfiguration {

    @Bean
    public Docket api(){
        return new Docket(DocumentationType.OAS_30)
                .useDefaultResponseMessages(false)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.sklookiesmu.wisefee.api"))
                .paths(PathSelectors.any())
                .build()
                .apiInfo(apiInfo())
                .securityContexts(Arrays.asList(securityContext()))
                .securitySchemes(Arrays.asList(bearerAuthSecurityScheme()));
    }

    private ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("Wisefee API Document")
                .description("This is Wisefee API Document")
                .version("1.0")
                .build();
    }

    private SecurityContext securityContext() {
        return springfox
                .documentation
                .spi.service
                .contexts
                .SecurityContext
                .builder()
                .securityReferences(defaultAuth())
                .operationSelector(operationContext -> true)
                .build();
    }

    private List<SecurityReference> defaultAuth() {
        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
        authorizationScopes[0] = authorizationScope;
        return Arrays.asList(new SecurityReference("JWT", authorizationScopes));
    }

    private HttpAuthenticationScheme bearerAuthSecurityScheme(){
        return HttpAuthenticationScheme.JWT_BEARER_BUILDER
                .name("JWT").build();
    }

}

 

Swagger를 사용한다면 인증과 API 인가 절차를 UI상에서 진행할 수 있도록 설정할 수 있다.

  1. securityContext() : 보안 컨텍스트를 설정
  2. defaultAuth() : 기본 보안 참조를 설정
  3. bearerAuthSecurityScheme() : Bearer 토큰 기반의 보안 스키마를 설정

 

 

 


API 인가 테스트

 

인증하지 않고 API 요청을 했을 경우

 

로그인 인증

 

 

인증 성공 후 발급받은 Token을 Request Header에 Bearer Token으로 설정

 

 

API 인가 승인 이후, SecurityUtil을 통해 사용자 인증 정보의 이메일을 추출한 데이터를 응답으로 받았음

반응형

BELATED ARTICLES

more