[Spring Security] Security & JWT
- ✅ Spring Security + JWT + CORS + XSS 방어 전체 코드
- 1.
pom.xml
(필요한 의존성 추가) - 2. JWT 유틸리티 클래스 (
JwtUtil.java
) - 3. XSS 방어 필터 (
XssFilter.java
) - 4. XSS 필터를 위한
HttpServletRequestWrapper
(XssRequestWrapper.java
) - 5. JWT 인증 필터 (
JwtAuthenticationFilter.java
) - 6. Spring Security 설정 (
SecurityConfig.java
) - 🔥 최종 정리
- 1.
SecurityFilterChain
에서 권한 정보가 처리되는 흐름 - 2.
JwtAuthenticationFilter
에서 권한 정보를 SecurityContext에 저장하는 방법 - 3. JWT에서 권한 정보를 포함하는 방법 (
JwtUtil.java
) - 4. Spring Security에서 권한 기반 접근 제어
- 5. 컨트롤러에서 권한 기반 분기 처리
- 6. SecurityContext에서 직접 권한 정보 확인
- 🔥 최종 정리
- 1.
✅ Spring Security + JWT + CORS + XSS 방어 전체 코드
MSA 환경에서 JWT 인증, CORS 설정, XSS 방어를 모두 포함한 완전한 Spring Security 설정을 제공합니다.
이제 모든 요청에서 JWT 검증, CORS 처리, XSS 보호를 적용할 수 있습니다. ✅
1. pom.xml
(필요한 의존성 추가)
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT (io.jsonwebtoken) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.11.5</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
📌 Spring Security + JWT + Lombok 포함.
2. JWT 유틸리티 클래스 (JwtUtil.java
)
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import java.util.Date;
import java.util.List;
import javax.crypto.SecretKey;
public class JwtUtil {
private static final String SECRET_KEY = "mysecretkeymysecretkeymysecretkeymysecretkey"; // 32바이트 이상 필요
private static final long EXPIRATION_TIME = 1000 * 60 * 60; // 1시간
private static final SecretKey key = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
// ✅ JWT 생성 (권한 정보 포함)
public static String generateToken(String username, List<String> roles) {
return Jwts.builder()
.setSubject(username)
.claim("roles", roles) // ✅ 권한 정보 추가
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
// ✅ JWT에서 사용자 이름 가져오기
public static String getUsername(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
// ✅ JWT에서 권한(Role) 정보 가져오기
public static List<String> getRoles(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody()
.get("roles", List.class);
}
}
3. XSS 방어 필터 (XssFilter.java
)
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class XssFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
XssRequestWrapper wrappedRequest = new XssRequestWrapper((HttpServletRequest) request);
chain.doFilter(wrappedRequest, response);
}
}
4. XSS 필터를 위한 HttpServletRequestWrapper
(XssRequestWrapper.java
)
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class XssRequestWrapper extends HttpServletRequestWrapper {
public XssRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public String getParameter(String name) {
return sanitize(super.getParameter(name));
}
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values == null) return null;
for (int i = 0; i < values.length; i++) {
values[i] = sanitize(values[i]);
}
return values;
}
private String sanitize(String input) {
return input == null ? null : input.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll("\"", """)
.replaceAll("'", "'")
.replaceAll("&", "&");
}
}
5. JWT 인증 필터 (JwtAuthenticationFilter.java
)
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
try {
String username = JwtUtil.getUsername(token.substring(7));
List<String> roles = JwtUtil.getRoles(token.substring(7));
List<SimpleGrantedAuthority> authorities = roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
User user = new User(username, "", authorities);
SecurityContextHolder.getContext().setAuthentication(new JwtAuthenticationToken(user));
} catch (Exception e) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid Token");
return;
}
}
filterChain.doFilter(request, response);
}
}
6. Spring Security 설정 (SecurityConfig.java
)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
public class SecurityConfig {
private final JwtConfirmFilter jwtConfirmFilter;
public SecurityConfig(JwtConfirmFilter jwtConfirmFilter) {
this.jwtConfirmFilter = jwtConfirmFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/login").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtConfirmFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new XssFilter(), JwtConfirmFilter.class)
.formLogin().disable();
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of(
"https://example.com",
"https://*.example.com",
"https://anotherdomain.com"
));
configuration.addAllowedMethod("*");
configuration.addAllowedHeader("*");
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
🔥 최종 정리
✅ JWT 인증 필터 (JwtAuthenticationFilter
) 적용
✅ XSS 방어 필터 (XssFilter
) 적용
✅ CORS 설정 (corsConfigurationSource()
) 적용
✅ Spring Security에서 authorizeHttpRequests()
를 사용한 권한 관리 적용
✅ Spring Security의 SecurityFilterChain
에서 권한(Role) 정보는 어떻게 전달될까?
Spring Security에서 사용자의 권한(Role) 정보는 Authentication
객체를 통해 관리되며, SecurityContext에 저장됨.
이 권한 정보는 JWT 토큰 기반 인증을 사용할 경우, JWT에서 추출하여 SecurityContext에 저장하는 방식으로 전달됩니다.
1. SecurityFilterChain
에서 권한 정보가 처리되는 흐름
[클라이언트 요청]
→ SecurityFilterChain 시작
→ JwtAuthenticationFilter 실행 (JWT에서 권한 정보 추출)
→ SecurityContextHolder에 Authentication 저장
→ SecurityContextPersistenceFilter (SecurityContext 유지)
→ AuthorizationFilter (권한 검사)
→ 컨트롤러 실행
→ 응답 반환
[클라이언트 응답 받음]
✅ JWT 기반 인증을 사용할 경우, JwtAuthenticationFilter
에서 JWT에서 권한 정보를 추출하여 SecurityContext
에 저장.
✅ 컨트롤러에서 @PreAuthorize
, SecurityContextHolder
등을 사용하여 권한 기반 분기 처리 가능.
2. JwtAuthenticationFilter
에서 권한 정보를 SecurityContext에 저장하는 방법
✅ JWT에서 권한(Role) 정보 추출 및 SecurityContext 저장
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
try {
String username = JwtUtil.getUsername(token.substring(7)); // ✅ 사용자 이름 추출
List<String> roles = JwtUtil.getRoles(token.substring(7)); // ✅ JWT에서 역할(Role) 추출
// ✅ 역할 정보를 SecurityContext에 저장 role은 new SimpleGrantedAuthority("ROLE_"+claims.get("role")) 형태로 들어가야 된다.
List<SimpleGrantedAuthority> authorities = roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
User user = new User(username, "", authorities);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user, null, authorities);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
} catch (Exception e) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid Token");
return;
}
}
filterChain.doFilter(request, response);
}
}
📌 JWT에서 getRoles()
메서드를 사용하여 권한 정보를 추출하고, SecurityContext에 저장.
📌 추출된 권한 정보를 SimpleGrantedAuthority
로 변환하여 Spring Security의 권한 시스템에 맞게 적용.
3. JWT에서 권한 정보를 포함하는 방법 (JwtUtil.java
)
JWT 생성 시 사용자의 역할(Role) 정보를 claim
에 추가해야 합니다.
✅ JWT 생성 시 역할(Role) 정보 포함
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import javax.crypto.SecretKey;
public class JwtUtil {
private static final String SECRET_KEY = "mysecretkeymysecretkeymysecretkeymysecretkey"; // 32바이트 이상 필요
private static final long EXPIRATION_TIME = 1000 * 60 * 60; // 1시간
private static final SecretKey key = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
// ✅ JWT 생성 (권한 정보 포함)
public static String generateToken(String username, List<String> roles) {
return Jwts.builder()
.setSubject(username)
.claim("roles", roles) // ✅ 권한 정보 추가
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
// ✅ JWT에서 사용자 역할(Role) 정보 추출
public static List<String> getRoles(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
return claims.get("roles", List.class);
}
}
📌 JWT 생성 시 "roles"
클레임에 역할(Role) 정보를 포함하여 저장.
📌 JWT에서 역할 정보를 getRoles()
메서드를 통해 추출 가능.
4. Spring Security에서 권한 기반 접근 제어
✅ SecurityFilterChain에서 권한별 접근 제한
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf().disable()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN") // ✅ ADMIN 권한 필요
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN") // ✅ USER 또는 ADMIN 가능
.requestMatchers("/public/**").permitAll() // ✅ 누구나 접근 가능
.anyRequest().authenticated()
)
.formLogin().disable();
return http.build();
}
📌 SecurityFilterChain에서 hasRole("ADMIN")
을 사용하여 특정 URL에 대한 접근을 제한.
📌 JWT에서 추출한 권한 정보를 SecurityContext에 저장하면 Spring Security에서 자동으로 검증 가능.
5. 컨트롤러에서 권한 기반 분기 처리
✅ @PreAuthorize
를 활용한 권한 검증
@EnableWebSecurity
@EnableMethodSecurity // ✅ 추가: @PreAuthorize 활성화
@Configuration
public class SecurityConfig {
...
}
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class RoleController {
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin")
public String adminAccess() {
return "Welcome, Admin!";
}
@PreAuthorize("hasAnyRole('USER', 'ADMIN')")
@GetMapping("/user")
public String userAccess() {
return "Welcome, User!";
}
@PreAuthorize("isAuthenticated()")
@GetMapping("/profile")
public String profile() {
return "This is your profile.";
}
}
📌 JWT에서 추출한 권한 정보가 SecurityContext
에 저장되므로, @PreAuthorize
를 통해 권한 검증 가능.
📌 hasRole('ADMIN')
을 사용하여 ROLE_ADMIN
을 가진 사용자만 실행 가능.
6. SecurityContext에서 직접 권한 정보 확인
✅ SecurityContext에서 현재 사용자 권한 확인
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class SecurityContextController {
@GetMapping("/dashboard")
public String getDashboard() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMIN"))) {
return "Admin Dashboard";
}
if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_USER"))) {
return "User Dashboard";
}
return "Access Denied";
}
}
📌 SecurityContext에서 현재 사용자의 권한을 가져와 직접 확인 후 분기 처리 가능.
🔥 최종 정리
✅ JWT 생성 시 권한(Role) 정보를 claim
에 포함하여 저장
✅ JwtAuthenticationFilter
에서 JWT에서 권한 정보를 추출하여 SecurityContext에 저장
✅ SecurityFilterChain에서 authorizeHttpRequests()
를 사용하여 권한 기반 접근 제어
✅ 컨트롤러에서는 @PreAuthorize
또는 SecurityContextHolder
를 사용하여 권한 기반 분기 처리 가능
🚀 즉, Spring Security의 SecurityFilterChain
에서는 JwtAuthenticationFilter
를 통해 권한을 SecurityContext에 전달하고, 이후 Spring Security가 이를 기반으로 권한 검증을 수행함! 🎯