Spring Security란?

김정수·2023년 7월 23일

Spring Security


스프링 시큐리티는 스프링 기반의 보안 프레임워크로, 웹 애플리케이션의 인증과 인가를 처리하는 역할을 합니다.
Spring 기반 애플리케이션에서 보안과 인증을 처리하기 위한 프레임워크이다.
보안과 관련된 기능을 구현하고, 애플리케이션의 취약점을 방지하여 안전한 서비스를 제공하는 데 사용됩니다.

로그인 인증 구조

OAUTH 2.0 동작 구조

기능과 특징

웹 애플리케이션에 인증과 권한 부여를 적용하는 방법

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
                .withUser("user")
                .password("{noop}password")
                .roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/public/**").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .and()
            .logout()
                .permitAll();
    }
}

configureGlobal 메소드를 통해 사용자를 인증하고, 'configure' 메소드를 통해 URL 패턴에 대한 접근 권한을 설정하고, '/public/**' 경로는 모든 사용자에게 허용되고, '/admin/**' 경로는 ADMIN 권한을 가진 사용자에게만 허용된다.그 외의 모든 요청은 인증된 사용자에게만 허용된다.

간단한 로그인 예시

먼저 Gradle을 추가를 해준다.

plugins {
    id 'org.springframework.boot' version '2.5.4'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
java.sourceCompatibility = JavaVersion.VERSION_17

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    implementation 'io.jsonwebtoken:jjwt:0.9.1'
}

test {
    useJUnitPlatform()
}

그리고 Spring Security 구성

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
                .withUser("user")
                .password("{noop}password")
                .roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/public/**").permitAll()
                .antMatchers("/private/**").authenticated()
                .and()
            .formLogin()
                .and()
            .logout()
                .permitAll();
    }
}

OAuth2 클라이언트 구성:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;

@Configuration
public class OAuth2Config {

    @Bean
    public OAuth2AuthorizedClientService authorizedClientService() {
        return new InMemoryOAuth2AuthorizedClientService(new InMemoryOAuth2AuthorizedClient());
    }

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        return new InMemoryClientRegistrationRepository(clientRegistration());
    }

    private ClientRegistration clientRegistration() {
        return ClientRegistration.withRegistrationId("oauth2-example")
                .clientId("your-client-id")
                .clientSecret("your-client-secret")
                .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                .tokenUri("https://oauth2-provider.com/token")
                .userInfoUri("https://oauth2-provider.com/userinfo")
                .userNameAttributeName("username")
                .clientName("OAuth2 Example")
                .build();
    }
}

JWT 토큰 생성과 검증 유틸리티:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Component
public class JwtTokenUtil {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private long expiration;

    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, userDetails.getUsername());
    }

    private String createToken(Map<String, Object> claims, String subject) {
        Date now = new Date();
        Date expirationDate = new Date(now.getTime() + expiration);

        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(now)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS256, secret)
                .compact();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
    }

    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    private Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    private Boolean isTokenExpired(String token) {
        final Date expiration = extractExpiration(token);
        return expiration.before(new Date());
    }
}

Spring Security는 어떻게 사용되는가?

인증:
▶ 사용자가 로그인한 후, 인증 정보를 제공
▶ 사용자가 제공한 인증 정보를 검증하고, 인증이 성공하면 사용자의 신원을 확인
▶ 인증 과정에서는 주로 사용자 이름과 비밀번호를 확인하거나, 외부 인증 제공자를 통해 인증을 수행할 수도 있다.

권한 부여:
▶ 인증된 사용자에 대해 애플리케이션의 리소스에 접근할 수 있는 권한을 부여한다.
▶ 권한은 역할 또는 권한 단위로 정의되며, 사용자에게 할당된다.
▶ 권한 부여 과정에서는 주로 사용자의 역할 또는 권한을 확인하여 액세스제어를 수행한다.

필터 체인:
▶ Spring Security는 Servlet 필터 기반의 보안 기능을 제공
▶ 보안 필터 체인은 요청을 받아 필터를 순차적으로 적용
▶ 각 필터는 특정한 보안 작업을 수행하거나 보안 검사를 수행
예를 들어, 인증 필터, 권한 검사 필터, CSRF 필터 등이 있다.
▶ 필터 체인은 요청의 보안 처리를 담당하며, 필터의 순서와 구성은 보안 설정에 따라 결정된다.

보안 컨텍스트:
▶ 보안 컨텍스트는 현재 사용자의 보안 정보를 저장하고 관리한다.
▶ 인증된 사용자의 정보, 권한 등을 포함하고 있으며, 애플리케이션에서 이 정보를 사용할 수 있다.
▶ 보안 컨텍스트는 스레드 로컬을 통해 관리되어 다중 스레드 환경에서도 정상적으로 작동한다.

액세스 결정 및 보안 이벤트 처리:
▶ Spring Security는 액세스 결정을 수행하여 사용자의 요청에 대한 접근 권한을 확인
▶ 접근 결정은 주로 URL 패턴, HTTP 메소드, 인증된 사용자의 권한 등을 고려하여 이루어진다.
▶ 액세스 결정 이벤트에 따라 보안 관련 이벤트를 처리하고, 필요한 애플리케이션 로직을 수행한다.

Spring Security를 사용하고있는 곳

Spring Security를 사용하고 있는 회사는 생각보다 엄청 많다. 우리가 흔히 아는 큰 회사들은 기본적으로 사용하고 있으며 오픈소스 프로젝트에서도 많이 사용하고 있다. 회사로 예를 들면 삼성전자, 한전, 애플, 구글 등 큰 기업에서도 사용하고 있으며, Spring Security는 안전성과 유연성, 확장성, 커뮤니티 지원 등의 장점으로 인해 다양한 기업들과 개발자들에게 선택되고 있다.

Spring Security 사용후기

모르는 상태에서 사용하려고 하면 너무 어렵다. SpringSecurity를 쉽게 설명하자면 각 유저를 등급으로 나눈 뒤 그 등급이 SpringSecurity에 등록한 등급이 맞으면 허가를 해주는 방식이다. 예를 들면 User의 등급이 학생, 선생, 부모 이렇게 3가지가 있다고 가정한다면 SpringSecurity에 Login을 할 때 등급이 학생과 선생인 User만 Login을 할 수 있도록 허가해 주겠다고 설정을 하면 학생과 선생의 등급을 가진 User들은 Login이 정상적으로 될 것이고, 부모의 등급을 가진 User는 아마 403에러가 발생할 것이다.
그리고 사용하고 나서 크게 깨달은 것이 SpringSecurity는 제일 마지막에 구현을 해야 개발을 순조롭게 할 수 있을 것 같다. Spring Security를 먼저 구현을 한 뒤 다른 기능들을 개발하고 테스트를 해보면 일단 기본적으로 403에러를 최소 10번 정도 볼 것이다.
결론은 사용하면 좋은 것 같긴 한데 쓸 거면 가장 마지막에 구현을 하자

profile
현재진행형

2개의 댓글

comment-user-thumbnail
2023년 7월 23일

정보 감사합니다.

답글 달기
comment-user-thumbnail
2023년 7월 23일

전에 스프링 시큐리티 사용해서 jwt를 구현했을때는 뭣도 모르고 그냥 막 구현했는데
지금 다시 개념을 보니까 왜 그렇게 구현했었는지 이제 이해가 되네요

답글 달기