일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- mac 화면분할
- 대규모 시스템 설계
- aws
- 성능테스트
- 알고리즘사이트
- DDD
- 알고리즘 추천
- ui 커스텀
- 알고리즘
- 조가사키 해안
- 코드트리
- 알고리즘초보
- JMeter
- 스프링부트
- Java
- 브라우저 단축키
- 초년생
- 판교퇴근길밋업
- 코딩
- 자동화
- 코코테라스
- 기능 많은 브라우저
- 알고리즘분류
- 편한 즐겨찾기 편집
- ddd vs layered
- 프로그래밍
- 오블완
- spring boot
- 스카이라인 열차
- 소프트웨어 지표
- Today
- Total
영감을 (inspire) 주고픈 개발 블로그
Spring Security 이해하기 본문
Spring은 복잡한 개념을 추상화하여 개발자가 손쉽게 보안 기능을 적용할 수 있도록 도와줍니다. 보안 개념을 인터페이스로 명확하게 정리하고, 내부적으로 복잡한 구현을 감춘 덕분에 개발자는 보안 원칙을 쉽게 적용할 수 있습니다. Spring Security는 이러한 철학을 기반으로 웹 애플리케이션 보안의 주요 이론을 체계적으로 적용할 수 있도록 합니다.
그러나 처음 Spring Security를 사용할 때, 많은 어려움을 겪었습니다. 보안 지식이 부족해 인증, 인가, 필터 체인과 같은 개념이 생소했고, 설정을 어떻게 시작해야 할지 막막했습니다. 보안 플로우에 대한 이해가 부족하다 보니 구현을 해도 제대로 한 건지 햇갈렸습니다.
Spring Security를 처음 접하는 개발자들이 어려워할만한 점들을 정리해보았습니다.
- 보안 개념 및 용어의 생소함: 인증, 인가, 필터 체인, CSRF 보호 등의 개념이 생소할 수 있으며, 이러한 개념을 이해하지 않고 설정을 진행하면 올바른 보안 구성을 하기 어렵습니다.
- 구성의 복잡성: 보안 설정을 위한 다양한 옵션과 설정 방식(Spring Boot, XML 설정, 자바 설정 등)으로 인해 초기 설정이 어렵습니다. 다행히 Spring 6 부터는 SecurityFilter와
- 보안 필터 체인의 이해: Spring Security는 여러 개의 보안 필터를 체인으로 연결하여 동작하며, 각 필터의 역할을 이해하기가 쉽지 않습니다.
- 보안 플로우의 복잡성: 로그인 요청부터 토큰 검증 및 세션 관리까지의 전반적인 흐름을 모르면, 보안 설정을 효과적으로 적용하는 데 어려움을 겪을 수 있습니다. 특히 프론트엔드와의 통합 과정에서 토큰 저장 방식(localStorage, HttpOnly 쿠키 등)에 대한 고민이 필요합니다.
- OAuth 및 JWT 연동의 복잡성: OAuth 및 JWT를 조합하여 사용할 때, 클라이언트와 리소스 서버 간의 통신 흐름을 이해하고 올바르게 구현하는 데 어려움이 있을 수 있습니다. 특히, 프론트엔드와의 상호작용이 필수적이며, 올바른 요청 및 응답 처리가 요구됩니다.
- 디버깅 및 문제 해결: 인증 및 인가 과정에서 발생하는 문제를 디버깅하는 것이 어렵고, 스프링의 내부 동작을 이해해야 원인을 파악할 수 있습니다.
Spring Security를 처음 접할 때, 보안이라는 개념이 어렵게 느껴질 수 있습니다. 보안은 단순히 특정 기술을 적용하는 것이 아니라, 애플리케이션의 구조와 작동 방식에 깊이 영향을 미치는 중요한 요소입니다. 사용자 데이터를 안전하게 보호하고, 신뢰할 수 있는 서비스를 제공하는 것이 보안의 핵심 목표입니다. 처음에는 복잡해 보이지만, 보안의 기본 개념을 이해하고 나면 Spring Security를 활용하여 효과적인 보안 솔루션을 구축할 수 있습니다.
이 글에서는 먼저 보안의 기본 개념부터 차근차근 살펴보고, Spring Security를 사용하여 보안을 어떻게 적용할 수 있는지 알아보겠습니다.
웹 애플리케이션 보안의 핵심 개념
웹 애플리케이션 보안을 제대로 적용하려면 몇 가지 핵심 개념을 이해해야 합니다.
인증(Authentication)
인증이란 사용자가 "누구인지" 확인하는 과정입니다. 일반적으로 ID/비밀번호 로그인, 소셜 로그인(OAuth2), JWT 토큰, SSO(싱글 사인온)과 같은 방식이 사용됩니다.
인가(Authorization)
인가란 인증된 사용자가 "무엇을 할 수 있는지" 결정하는 과정입니다. 대표적인 방법으로는 RBAC(역할 기반 접근 제어), ABAC(속성 기반 접근 제어), ACL(접근 제어 목록)이 있습니다.
데이터 보호(Data Protection)
중요한 데이터가 외부에 노출되지 않도록 암호화 및 보안 정책을 적용해야 합니다. HTTPS(SSL/TLS), 암호화 저장, 비밀번호 해싱 등의 방법이 사용됩니다.
공격 방어(Attack Mitigation)
웹 애플리케이션은 XSS(크로스 사이트 스크립팅), SQL Injection, CSRF(사이트 간 요청 위조) 등 다양한 공격에 노출될 수 있습니다. 이를 방어하기 위해 보안 정책과 필터링을 적용해야 합니다.
세션 관리(Session Management)
사용자의 로그인 상태를 안전하게 유지해야 하며, 세션 고정 공격 방지, 만료 정책, 다중 로그인 방지와 같은 기능을 고려해야 합니다.
Spring Security로 보안 적용하기
Spring Security를 사용하면 다양한 보안 기능을 설정할 수 있습니다.
Spring 6에서는 보안 설정 방식에 몇 가지 중요한 변화가 있었습니다. 기존의 WebSecurityConfigurerAdapter 클래스가 더 이상 사용되지 않고, 대신 SecurityFilterChain 빈을 명시적으로 정의하는 방식으로 변경되었습니다. 이를 통해 더 유연하고 명확한 보안 구성이 가능해졌습니다.
SecurityFilterChain 에서 각 설정 옵션의 역할을 이해하는 것이 중요합니다. 아래는 주요 보안 설정 옵션에 대한 설명입니다.
보안 설정 옵션
- 폼 로그인(Form Login):
- 사용자가 웹 폼을 통해 로그인하도록 설정합니다.
- 로그인 페이지 URL 및 성공/실패 후 이동할 페이지를 설정할 수 있습니다.
- OAuth2 로그인:
- 소셜 로그인을 지원하며 Google, Facebook 등 외부 제공자를 통한 인증을 처리합니다.
- 클라이언트 ID, 시크릿, 리디렉션 URL 등을 설정해야 합니다.
- HTTP 기본 인증(Http Basic):
- HTTP 헤더의 Authorization을 통해 사용자 이름과 비밀번호를 전달하는 간단한 인증 방식입니다.
- REST API와 같은 환경에서 자주 사용됩니다.
- 인가(Authorization):
- 특정 URL이나 리소스에 대한 접근 권한을 부여합니다.
- URL 패턴별 접근 제어를 설정할 수 있습니다.
- CSRF 보호:
- Cross-Site Request Forgery 공격을 방지하기 위해 사용됩니다.
- API 서버의 경우 비활성화할 수 있습니다.
- 세션 관리(Session Management):
- 사용자의 세션을 관리하는 방법을 정의합니다.
- 세션 고정 공격 방지, 최대 동시 세션 수 제한 등의 기능을 설정할 수 있습니다.
- CORS(Cross-Origin Resource Sharing):
- 다른 도메인에서의 요청을 허용할지 여부를 설정합니다.
- 프론트엔드와의 연동 시 중요합니다.
- 로그아웃(Logout):
- 로그아웃 URL 및 성공 후 이동할 페이지를 정의합니다.
- JWT 필터 추가:
- JWT 토큰을 검증하기 위한 커스텀 필터를 추가할 수 있습니다.
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.config.http.SessionCreationPolicy;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/home", true)
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/home", true)
)
.httpBasic(httpBasic -> httpBasic.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/user").authenticated()
.requestMatchers("/").permitAll()
)
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(cors -> cors.configure(http))
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
)
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
Spring Security에서 적용한 웹 애플리케이션 보안의 핵심 개념과 구현 방식
Spring Security는 웹 애플리케이션 보안의 핵심 개념을 체계적으로 적용할 수 있도록 다양한 기능을 제공합니다. 이를 효과적으로 구현하기 위해 다음과 같은 요소를 고려해야 합니다.
인증(Authentication) 구현
UserDetailsService 구현
사용자 정보를 불러오기 위해 UserDetailsService 인터페이스를 구현합니다. Spring Security는 이 인터페이스를 통해 사용자 인증 정보를 로드합니다.
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if ("admin".equals(username)) {
return User.withUsername("admin")
.password("{noop}password")
.roles("ADMIN")
.build();
} else {
throw new UsernameNotFoundException("User not found");
}
}
}
PasswordEncoder 적용
비밀번호를 안전하게 저장하기 위해 PasswordEncoder를 사용합니다.
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
AuthenticationProvider 설정
커스텀 인증 로직을 적용하기 위해 AuthenticationProvider를 구현할 수 있습니다.
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final UserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
public CustomAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
var userDetails = userDetailsService.loadUserByUsername(username);
if (passwordEncoder.matches(password, userDetails.getPassword())) {
return new UsernamePasswordAuthenticationToken(username, password, userDetails.getAuthorities());
} else {
throw new AuthenticationException("Invalid credentials") {};
}
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
인가(Authorization)
URL 기반 접근 제어
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/user").authenticated()
.requestMatchers("/").permitAll()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/home", true)
);
return http.build();
}
메서드 기반 접근 제어
import org.springframework.security.access.prepost.PreAuthorize;
@Service
public class UserService {
@PreAuthorize("#username == authentication.name")
public String getUserDetails(String username) {
return "User Details for " + username;
}
}
역할 계층 정의
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_MANAGER > ROLE_USER");
return roleHierarchy;
}
JWT 인증 적용
JWT 토큰 발급 및 검증
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class JwtUtil {
private String secretKey = "yourSecretKey";
private long expirationTime = 86400000; // 1 day
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
public Claims extractClaims(String token) {
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
}
public boolean validateToken(String token) {
try {
Claims claims = extractClaims(token);
if (!claims.getExpiration().before(new Date())) {
return true;
}
return false;
} catch (Exception e) {
return false;
}
}
public String refreshAccessToken(String refreshToken) {
Claims claims = extractClaims(refreshToken);
if (!claims.getExpiration().before(new Date())) {
return generateToken(claims.get("sub").toString());
}
throw new RuntimeException("Refresh token is expired");
}
}
JWT 기반 인증 및 리프레시 토큰 적용
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
if (jwtUtil.validateToken(token)) {
Claims claims = jwtUtil.extractClaims(token);
String username = claims.getSubject();
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
chain.doFilter(request, response);
}
}
리프레시 토큰 적용 흐름
- 사용자가 액세스 토큰 만료 시 리프레시 토큰을 전송
- 서버에서 리프레시 토큰의 만료 여부 확인
- 만료되지 않았다면 새로운 액세스 토큰 발급
public String refreshToken(String refreshToken) {
Claims claims = jwtUtil.extractClaims(refreshToken);
if (!claims.getExpiration().before(new Date())) {
return jwtUtil.generateToken(claims.get("sub").toString());
}
throw new RuntimeException("Refresh token expired");
}
마무리
Spring Security는 웹 애플리케이션의 다양한 보안 요구사항을 충족할 수 있는 강력하고 유연한 프레임워크입니다. 하지만 처음 접하면 복잡하게 느껴질 수 있고, 인증, 인가, 필터 체인 같은 개념들이 낯설게 다가올 수도 있습니다. 그래서 기본 개념과 Spring Security의 동작 방식을 차근차근 이해하는 게 중요합니다.
이 글에서 다룬 보안 개념과 설정 방법을 익히고, 실전 프로젝트에 하나씩 적용해보면서 보안 역량을 키워나가면 점점 익숙해질 거예요. 또한, 보안 설정이 애플리케이션 성능과 사용자 경험에 어떤 영향을 미치는지도 함께 고민해보는 것이 필요합니다.