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

2024. 1. 15. 10:19
반응형

 

 

 

 

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

그 중 Spring Security의 경우 변화한 내용이 조금 있는 편이라 이 참에 Spring Security를 이용한 JWT 인증과 인가를 해당 버전대에 맞추고, 정리하여 다시 작성해보려고 한다.

 

 

아래는 Spring 2점대 버전에서의 JWT Security를 설정했던 포스팅들인데, 3점대 버전에서도 근본적인 과정들이 바뀐 것은 아니다. 만약 2점대 버전에서의 설정이 궁금하다면 아래 포스팅들의 내용을 참고하자.



회원 가입 : 해당 부분은 Security에 의존하지 않아, 회원 가입 로직이 필요하다면 참고하면 될 것 같다.

https://sjh9708.tistory.com/83

 

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

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

 

 

 

 

 

 

 

 

 

 

 

 

sjh9708.tistory.com

 

로그인 인증(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


JWT 인가(Authorization) 

https://sjh9708.tistory.com/85

 

[SpringBoot] Spring Security JWT 인증과 인가 - (3) API 인가 (Authorization)

이전 포스팅에서 Spring Boot에서 JWT를 이용한 로그인 인증을 구현하였다. 이번에는 로그인한 사용자에 한해서 API 사용을 승인하는 API 인가(Authorization), 더불어 권한별로 인가를 구현하는 방법을

sjh9708.tistory.com

 

 

 

 


Spring Security

 

https://www.javadevjournal.com/spring-security/spring-security-authentication/#google_vignette

 

 

위 그림은 Spring Security측에서 기본적으로 제공하는 인증 처리에 사용되는 모듈들이다.
Spring은 디폴트로 사용할 수 있는 Username과 Password 기반의 로그인 시스템이 내장되어 있다. 하지만 JWT Auth 등 특정 목적을 위해 확장하려면, 해당 모듈들의 일부를 Extend하여 커스텀 필터로 사용하는 방법을 사용하고 있다.

Filter 체인: Spring Security는 다양한 Filter들의 체인으로 구성되어 있다. 이 Filter 체인은 Request를 가로챈 후 일련의 절차를 처리한다. UsernamePasswordAuthenticationFilter는 사용자가 제출한 인증 정보를 처리한다.

UsernamePasswordAuthenticationToken 생성: UsernamePasswordAuthenticationFilter는 UsernamePasswordAuthenticationToken을 생성하여 AuthenticationManager에게 전달한다. 이 토큰에는 사용자가 제출한 인증 정보가 포함되어 있다.

AuthenticationManager: AuthenticationManager는 실제로 인증을 수행하는데, 여러 AuthenticationProvider들을 이용한다.

AuthenticationProvider : 각각의 Provider들은 특정 유형의 인증을 처리한다. 예시로 DaoAuthenticationProvider는 사용자 정보를 데이터베이스에서 가져와 인증을 수행한다.

PasswordEncoder : 인증과 인가에서 사용될 패스워드의 인코딩 방식을 지정한다.

 

UserDetailsService : AuthenticationProvider는 UserDetailsService를 사용하여 사용자 정보를 가져온다. UserDetailsService는 사용자의 아이디를 받아 loadbyUsername을 호출하여 해당 사용자의 UserDetails를 반환한다.

 

UserDetails : UserDetails에는 사용자의 아이디, 비밀번호, 권한 등이 포함되어 있다.

 

Authentication 객체 생성: 인증이 성공하면, AuthenticationProvider는 Authentication 객체를 생성하여 AuthenticationManager에게 반환한다. 이 Authentication 객체에는 사용자의 세부 정보와 권한이 포함되어 있다.

SecurityContextHolder: 현재 실행 중인 스레드에 대한 SecurityContext를 제공한다. 

 

SecurityContext: 현재 사용자의 Authentication이 저장되어 있다. 애플리케이션은 SecurityContextHolder를 통해 현재 사용자의 권한을 확인하고, 인가 결정을 한다.

 

 

 


프로젝트 의존성

 

 build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.2.1'
	id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '17'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
	implementation 'org.springframework.boot:spring-boot-starter-validation'

	runtimeOnly 'org.mariadb.jdbc:mariadb-java-client:2.7.4'
	compileOnly 'org.projectlombok:lombok'

	//Jwt
	implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
	implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
	implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

	//ModelMapper
	implementation group: 'org.modelmapper', name: 'modelmapper', version: '2.4.2'
    
    //...
}

 

Jwt 사용을 위해 jsonwebtoken 관련 라이브러리와, Spring Security 관련 모듈들을 의존성으로 추가해두자.

 

 

 

 application.yml

jwt:
  expiration_time: 86400000 #1일
  secret: VlwEyVBsYt9V7zq57TejMnVUyzblYcfPQye08f7MGVA9XkHa

 

JWT의 만료기간과 서명키를 Base64로 암호화하여 설정파일에 작성해두었다.

 

 

 


JWT 처리 유틸리티 클래스 작성

 

/**
 * [JWT 관련 메서드를 제공하는 클래스]
 */
@Slf4j
@Component
public class JwtUtil {

    private final Key key;
    private final long accessTokenExpTime;

    public JwtUtil(
            @Value("${jwt.secret}") String secretKey,
            @Value("${jwt.expiration_time}") long accessTokenExpTime
    ) {
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        this.key = Keys.hmacShaKeyFor(keyBytes);
        this.accessTokenExpTime = accessTokenExpTime;
    }

    /**
     * Access Token 생성
     * @param member
     * @return Access Token String
     */
    public String createAccessToken(CustomUserInfoDto member) {
        return createToken(member, accessTokenExpTime);
    }


    /**
     * JWT 생성
     * @param member
     * @param expireTime
     * @return JWT String
     */
    private String createToken(CustomUserInfoDto member, long expireTime) {
        Claims claims = Jwts.claims();
        claims.put("memberId", member.getMemberId());
        claims.put("email", member.getEmail());
        claims.put("role", member.getRole());

        ZonedDateTime now = ZonedDateTime.now();
        ZonedDateTime tokenValidity = now.plusSeconds(expireTime);


        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(Date.from(now.toInstant()))
                .setExpiration(Date.from(tokenValidity.toInstant()))
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
    }


    /**
     * Token에서 User ID 추출
     * @param token
     * @return User ID
     */
    public Long getUserId(String token) {
        return parseClaims(token).get("memberId", Long.class);
    }


    /**
     * JWT 검증
     * @param token
     * @return IsValidate
     */
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT Token", e);
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT Token", e);
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT Token", e);
        } catch (IllegalArgumentException e) {
            log.info("JWT claims string is empty.", e);
        }
        return false;
    }


    /**
     * JWT Claims 추출
     * @param accessToken
     * @return JWT Claims
     */
    public Claims parseClaims(String accessToken) {
        try {
            return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
        } catch (ExpiredJwtException e) {
            return e.getClaims();
        }
    }


}

 

JWT를 다루기 위한 기능들을 작성한 클래스를 정의해주자.

JWT를 생성하고, 유효성을 검증하고, 안의 Claim들을 추출할 수 있는 메서드들을 작성하였다.

 

 

 


로그인 구현 (Authentication)

 

 

 Controller

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/auth")
public class AuthApiController {

    private final AuthService authService;

    @PostMapping("login")
    public ResponseEntity<String> getMemberProfile(
            @Valid @RequestBody LoginRequestDto request
    ) {
        String token = this.authService.login(request);
        return ResponseEntity.status(HttpStatus.OK).body(token);
    }
}

 

 Service

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuthServiceImpl implements AuthService{

    private final JwtUtil jwtUtil;
    private final MemberRepository memberRepository;
    private final PasswordEncoder encoder;
    private final ModelMapper modelMapper;
    @Override
    @Transactional
    public String login(LoginRequestDto dto) {
        String email = dto.getEmail();
        String password = dto.getPassword();
        Member member = memberRepository.findMemberByEmail(email);
        if(member == null) {
            throw new UsernameNotFoundException("이메일이 존재하지 않습니다.");
        }

        // 암호화된 password를 디코딩한 값과 입력한 패스워드 값이 다르면 null 반환
        if(!encoder.matches(password, member.getPassword())) {
            throw new BadCredentialsException("비밀번호가 일치하지 않습니다.");
        }

        CustomUserInfoDto info = modelMapper.map(member, CustomUserInfoDto.class);

        String accessToken = jwtUtil.createAccessToken(info);
        return accessToken;
    }
}

 

 Repository

public interface MemberRepository extends JpaRepository<Member, Long> {

    // 쿼리 메서드
    Member findMemberByEmail(String email);

}

 

 Entity

@Entity
@Table(name = "MEMBER")
@Getter
@Setter
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "MEMBER_ID")
    private Long memberId;

    @Column(name = "email", nullable = false, unique = true)
    private String email;

    @Column(name = "NAME", nullable = false)
    private String name;

    @Column(name = "PASSWORD", nullable = false)
    private String password;

    @Enumerated(EnumType.STRING)
    @Column(name = "ROLE", nullable = false)
    private RoleType role;

    @OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
    private List<Category> categories;
}

 

 Encoder

@Configuration
public class PasswordEncoderConfig {

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

 

 

 

간단하게만 설명하고 넘어가면 로그인 시, Email과 패스워드를 사용자로부터 받아, 데이터베이스의 Email과 Password와 비교한다.

Password는 Encoder에 의해 Bcrypt로 암호화된 형태로 데이터베이스에 저장되어 있도록 하였기 때문에, Encoder를 통해 비교하는 로직을 사용한다.

 

비교하여 통과한다면 JwtUtil의 JWT 생성 메서드를 호출하여 해당 토큰을 응답으로 반환하는 Service 계층을 작성하였다. 이를 컨트롤러에서 사용하여 로그인 성공 시 토큰을 반환하고, 실패 시 Exception을 반환한다.

 

 

 

 

로그인 요청 DTO와, 로직 내부에서 유저 정보를 저장해 둘 DTO 코드

더보기

로그인 요청 DTO

@Data
@AllArgsConstructor
@NoArgsConstructor
@Schema(title = "AUTH_REQ_01 : 로그인 요청 DTO")
public class LoginRequestDto {

    @NotNull(message = "이메일 입력은 필수입니다.")
    @Email
    private String email;


    @NotNull(message = "패스워드 입력은 필수입니다.")
    private String password;
}

 

 

로직 내부에서 유저 정보를 저장해 둘 DTO

@Data
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class CustomUserInfoDto extends MemberDto{
    private Long memberId;
    
    private String email;
    
    private String name;

    private String password;

    private RoleType role;

}

 

 

 




인가 (Authrozation)

 

 

 

그래서 우리가 JWT 인가를 수행하기 위해 이번 포스팅에서 구현할 내용은 위의 내용과 같다.

사용자의 요청을 가로채 Header의 JWT에 대한 검증을 수행하는 JwtAuthFilter를 만들 것이고, Spring이 기본적으로 제공하는 UserDetails에서 속성들을 추가시키기 위해서 Custom UserDetails (Service)를 사용할 것이다.
해당 필터를 SecurityContext에 등록하여 요청에 대한 JWT 필터가 수행되도록 해 보자.


 

커스텀 UserDetails, UserDetailsService

 

@Getter
@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {

    private final CustomUserInfoDto member;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<String> roles = new ArrayList<>();
        roles.add("ROLE_" + member.getRole().toString());


        return roles.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
        return member.getPassword();
    }

    @Override
    public String getUsername() {
        return member.getMemberId().toString();
    }


    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }



}
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
    private final MemberRepository memberRepository;
    private final ModelMapper mapper;

    @Override
    public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException {
        Member member = memberRepository.findById(Long.parseLong(id))
                .orElseThrow(() -> new UsernameNotFoundException("해당하는 유저가 없습니다."));

        CustomUserInfoDto dto = mapper.map(member, CustomUserInfoDto.class);

        return new CustomUserDetails(dto);
    }
}

 

 

 

JwtAuthFilter

@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter { // OncePerRequestFilter -> 한 번 실행 보장

    private final CustomUserDetailsService customUserDetailsService;
    private final JwtUtil jwtUtil;

    @Override
    /**
     * JWT 토큰 검증 필터 수행
     */
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String authorizationHeader = request.getHeader("Authorization");

        //JWT가 헤더에 있는 경우
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            String token = authorizationHeader.substring(7);
            //JWT 유효성 검증
            if (jwtUtil.validateToken(token)) {
                Long userId = jwtUtil.getUserId(token);

                //유저와 토큰 일치 시 userDetails 생성
                UserDetails userDetails = customUserDetailsService.loadUserByUsername(userId.toString());

                if (userDetails != null) {
                    //UserDetsils, Password, Role -> 접근권한 인증 Token 생성
                    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                            new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

                    //현재 Request의 Security Context에 접근권한 설정
                    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                }
            }
        }

        filterChain.doFilter(request, response); // 다음 필터로 넘기기
    }
}

 

 

 


SecurityContext

 

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@AllArgsConstructor
public class SecurityConfig  {
    private final CustomUserDetailsService customUserDetailsService;
    private final JwtUtil jwtUtil;
    private final CustomAccessDeniedHandler accessDeniedHandler;
    private final CustomAuthenticationEntryPoint authenticationEntryPoint;

    private static final String[] AUTH_WHITELIST = {
            "/api/v1/member/**", "/swagger-ui/**", "/api-docs", "/swagger-ui-custom.html",
            "/v3/api-docs/**", "/api-docs/**", "/swagger-ui.html", "/api/v1/auth/**"
    };

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        //CSRF, CORS
        http.csrf((csrf) -> csrf.disable());
        http.cors(Customizer.withDefaults());

        //세션 관리 상태 없음으로 구성, Spring Security가 세션 생성 or 사용 X
        http.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(
                SessionCreationPolicy.STATELESS));

        //FormLogin, BasicHttp 비활성화
        http.formLogin((form) -> form.disable());
        http.httpBasic(AbstractHttpConfigurer::disable);


        //JwtAuthFilter를 UsernamePasswordAuthenticationFilter 앞에 추가
        http.addFilterBefore(new JwtAuthFilter(customUserDetailsService, jwtUtil), UsernamePasswordAuthenticationFilter.class);

        http.exceptionHandling((exceptionHandling) -> exceptionHandling
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler)
        );

        // 권한 규칙 작성
        http.authorizeHttpRequests(authorize -> authorize
                        .requestMatchers(AUTH_WHITELIST).permitAll()
                        //@PreAuthrization을 사용할 것이기 때문에 모든 경로에 대한 인증처리는 Pass
                        .anyRequest().permitAll()
//                        .anyRequest().authenticated()
        );

        return http.build();
    }


}

 

@EnableWebSecurity : Spring Security 컨텍스트 설정임을 명시한다.

@EnableGlobalMethodSecurity: Annotation을 통해서 Controller의 API들의 보안 수준을 설정할 수 있도록 활성화한다.

 

Spring 2점대 버전에서는 WebSecurityConfigurerAdapter를 상속받아 구현하는 형태로 많이 사용했었는데, Spring 3에 들어서 해당 방식보다 SecurityFilterChain을 Bean으로 등록하는 방식을 권장하고, Adapter 방식은 Deprecated되었다.

 

따라서 SecurityFilterChain을 반환하는 filterChain 메서드를 Bean으로 등록하고, 내부에서 Security Chain 설정을 진행한다.

 

 

설정한 내용들은 아래와 같다.

 

1. CSRF 보호 비활성화 :  CSRF 토큰을 사용하지 않을 것이므로 확인하지 않도록 설정

 

2. CORS 설정을 적용 : 다른 도메인의 웹 페이지에서 리소스에 접근할 수 있도록 허용

 

3. 폼 로그인과 HTTP 기본 인증을 비활성화 : Spring 웹 페이지에서 제공되는 로그인 폼을 통해 사용자를 인증하는 메커니즘과 HTTP 기반 기본 인증을 비활성화한다.

 

4. JwtAuthFilter를 UsernamePasswordAuthenticationFilter 앞에 추가하여, JWT 필터를 거치도록 설정한다. 만약 JwtAuthFilter을 통과하여 Authentication을 획득하였다면 인증 필요(Authenticated)한 자원의 사용이 가능해질 것이다.

 

5. 권한에 대한 규칙을 작성해주었다. anyRequest()에 대해 permitAll() 해주었는데 기본적으로는 모두 허용해 줄 것이고, EnableGlobalMethodSecurity를 설정해둔 이유가 Annotation으로 접근 제한을 하려고 했기 때문에 메서드 단위로 보안 수준을 설정해 줄 것이다.

 

6. 인증과 인가 실패 시 Exception Handler를 추가해주었다. Security 단에서 권한 관련 401이나 403 에러 등을 처리해 줄 핸들러를 함께 등록해주었다.
authenticationEntryPoint는 인증되지 않은 사용자에 대해 처리하는 Handler를 정의하고, accessDeniedHandler는 인증되었지만, 특정 리소스에 대한 권한이 없을 경우 호출되는 Handler를 정의한다.

 

 

 사용자 정의 핸들러 코드

더보기
@Slf4j(topic = "UNAUTHORIZATION_EXCEPTION_HANDLER")
@AllArgsConstructor
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    private final ObjectMapper objectMapper;

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        log.error("Not Authenticated Request", authException);

        ErrorResponseDto errorResponseDto = new ErrorResponseDto(HttpStatus.UNAUTHORIZED.value(), authException.getMessage(), LocalDateTime.now());

        String responseBody = objectMapper.writeValueAsString(errorResponseDto);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(responseBody);
    }
}

 

 

@Slf4j(topic = "FORBIDDEN_EXCEPTION_HANDLER")
@AllArgsConstructor
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    private final ObjectMapper objectMapper;

    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
        log.error("No Authorities", accessDeniedException);

        ErrorResponseDto errorResponseDto = new ErrorResponseDto(HttpStatus.FORBIDDEN.value(), accessDeniedException.getMessage(), LocalDateTime.now());

        String responseBody = objectMapper.writeValueAsString(errorResponseDto);
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpStatus.FORBIDDEN.value());
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(responseBody);
    }
}

 

 

2점대 버전 Spring Security Config과 비교하기

더보기

 

이전 방식과 비교해보면 차이점이 꽤 있는데, 앞에서 언급한 WebSecurityConfigurerAdater를 사용하지 않는 것과 이에 따른 메서드 오버라이딩을 사용하지 않고 Bean을 직접 등록하여 사용한다는 점, antMathcer대신 requestMatcher 사용 하는 것 등이 눈에 보인다.

 

@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);
    }

 

 

 

 


메서드 단위 권한 제어

 

    @PostMapping("")
    @PreAuthorize("hasRole('ROLE_COMMON')")
    public BasicResponseDto exampleAPI(
            
    ) {
		//...
    }

 

위에서 EnableGlobalMethodSecurity 설정을 해주었기 때문에 접근 권한의 제어를 메서드 단위로 수행할 수 있다.

PreAuthorize 어노테이션을 통해서 Security 보안 수준을 통과해야 해당 API를 사용할 수 있다.

 

위에서 CustomUserDetails에서 Role을 등록해 둔 것이 기억날 것이다. hasRole은 해당 권한을 가진 유저인지를 확인한다는 것이다. 이 경우, JWT 인증 + Role이 ROLE_COMMON이어야 해당 API를 사용할 수 있게 된다.

 

 

 


Authorization(인가) 정리

 

 

1. 사용자가 Header에 JWT를 포함하여 API 요청을 수행한다.

2. JwtAuthFilter가 해당 요청을 가로채 유효성 검증을 수행한다.

  • JWT가 포함되어 있는지 및 서버의 Secret을 사용하여 JWT의 유효성 검증
  • JWT의 Claim을 추출하여 UserDetailsService의 loadByUserName을 호출 -> 일치하는 User가 존재하면 UserDetails 생성
  • UserDetails을 통해 UsernamePasswordAuthenticationToken을 생성한 후, 컨텍스트에 인증 정보를 저장한 후 다음 필터의 처리를 수행하게 한다.
  • 만약 해당 과정이 실패 시 컨텍스트에 인증 정보를 저장하지 않고 다음 필터의 처리로 넘긴다.

3. 이제 SecurityConfig에 작성된 권한 규칙이나, @PreAuthorize와 같은 권한 접근 제어 시, 해당 인증 정보를 기반으로 인가 처리가 승인나거나, 실패하게 된다.

 

 

 

 

반응형

BELATED ARTICLES

more