1๏ธโฃ ์ต์ ๊ถํ + ๊ธฐ๋ณธ๊ฐ ๋ณด์ํ : ํ๋ ์์ํฌ ๊ธฐ๋ณธ ๋ณด์ ํค๋/์ ์ฑ
์ ๊บผ๋จ๋ฆฌ์ง ์์ ๊ฒ
2๏ธโฃ ์
๋ ฅ๊ฒ์ฆ & ์ถ๋ ฅ ์ธ์ฝ๋ฉ : ์๋ฒ/ํด๋ผ์ด์ธํธ ๋ชจ๋์ ์ ์ฉ
3๏ธโฃ ์ฟ ํค/ํ ํฐ ์์ : HttpOnly, Secure, SameSite, ์งง์ ๋ง๋ฃ, ํ์ (rotation).
ํตํฉ ๋ณด์ ํค๋ & ๊ธฐ๋ณธ ์ค์ ํ ํ๋ฆฟ
@Bean
SecurityFilterChain harden(HttpSecurity http) throws Exception {
http
.headers(h -> h
.contentSecurityPolicy(c -> c.policyDirectives(
"default-src 'self'; " +
"script-src 'self'; " +
"object-src 'none'; " +
"frame-ancestors 'none'; " +
"base-uri 'self'"))
.referrerPolicy(r -> r.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER))
.permissionsPolicy(p -> p.policy("geolocation=(), microphone=(), camera=()"))
.xssProtection(x -> x.block(true))
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true).preload(true).maxAgeInSeconds(31536000)))
.requiresChannel(c -> c.anyRequest().requiresSecure()); // HTTPS ๊ฐ์
return http.build();
}
server:
servlet:
session:
tracking-modes: cookie
cookie:
http-only: true
secure: true
same-site: lax
์๋ฒ ํ ํ๋ฆฟ / ํผ๊ธฐ๋ฐ
@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf // ๊ธฐ๋ณธ ๊ฐ ์ ์ง ( ํผ ๊ธฐ๋ฐ์ผ์์๋ ํ์ฑ ์ ์ง )
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // JS๊ฐ ์ฝ์ด ํค๋๋ก ๋ณด๋ผ ๋์๋ง
)
.headers(h -> h.contentSecurityPolicy(c -> c.policyDirectives("default-src 'self'")))
.authorizeHttpRequests(reg -> reg
.requestMatchers("/admin/**").authenticated()
.anyRequest().permitAll()
);
return http.build();
}
REST API + Bearer ํ ํฐ
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // ์ธ์
/์ฟ ํค ๋์ Authorization ํค๋ ์ฌ์ฉ์
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
๋ณด์ ํค๋ / CSP
@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf // ๊ธฐ๋ณธ๊ฐ ์ ์ง! (ํผ ๊ธฐ๋ฐ์ด๋ผ๋ฉด ํ์ฑ ์ ์ง)
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // JS๊ฐ ์ฝ์ด ํค๋๋ก ๋ณด๋ผ ๋๋ง
)
.headers(h -> h.contentSecurityPolicy(c -> c.policyDirectives("default-src 'self'")))
.authorizeHttpRequests(reg -> reg
.requestMatchers("/admin/**").authenticated()
.anyRequest().permitAll()
);
return http.build();
}
JSoup๋ก ์ ๋ ฅ ์ ํ
String sanitizeHtml(String raw) {
return Jsoup.clean(raw, Safelist.basic());
}
@Bean
SecurityFilterChain sessionFix(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/public/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(sm -> sm
.sessionFixation(sf -> sf.migrateSession())
);
return http.build();
}
server:
servlet:
session:
tracking-modes: cookie # URL๋ก ์ธ์
์ ํ ๊ธ์ง
cookie:
http-only: true
secure: true # HTTPS ์ ์
same-site: lax
Spring Security : JWT ํํฐ ๊ณจ๊ฒฉ
@Bean
SecurityFilterChain jwt(HttpSecurity http, JwtAuthFilter jwtFilter) throws Exception {
http
.csrf(csrf -> csrf.disable()) // Bearer ํค๋ ๊ธฐ๋ฐ
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
String auth = req.getHeader("Authorization");
if (auth != null && auth.startsWith("Bearer ")) {
String token = auth.substring(7);
// 1) ์๋ช
/๋ง๋ฃ/issuer/audience/jti ๊ฒ์ฆ
// 2) ๋ธ๋๋ฆฌ์คํธ/๋ฆฌ์ ์ฆ-๊ฒ์ถ ์กฐํ
// 3) ๊ถํ ๋ถ์ฌ
}
chain.doFilter(req, res);
}
}
์ฒดํฌ๋ฆฌ์คํธ
์น ๋ณด์์ ๋ค์ธต ๋ฐฉ์ด๋ฅผ ์งํฅํ๋ค.
๊ทธ๋ ๊ธฐ์ CSRF, XSS, ์ธ์ ๊ณ ์ , JWT ํ์ทจ ๊ฐ๊ฐ์ ๋ฌธ์ ๋ซ์๋๊ณ , ๊ธฐ๋ณธ ๊ฐ์ ๊ฐํํ๋ฉฐ, ์งง์ ๋ง๋ฃ/ํค๋กคํ ์ด์ / ๋ชจ๋ํฐ๋ง์ ์ต๊ดํํ์ฌ์ผํ๋ค.
๋ณด์์ ํ ๋ฒ์ ์ค์ ์ผ๋ก ๋๋๋ ๊ฒ์ด ์๋ ๋ฐฐํฌํ ๋๋ง๋ค ํจ๊ป ์ ๊ฒํด์ผ ํ๋ ์ฒดํฌ๋ฆฌ์คํธ์ด๋คโ
์ค๋ ๊ฐ์ด ์์๋ณธ ๊ฒ์ ํ ๋๋ก ๋ณด์์ ์ต๊ดํํ ์ ์๊ฒ ๋ ธ๋ ฅํ์ ๐ฏ