์คํ๋ง ๋ณด์์ ๊ฝ์ธ ์คํ๋ง ์ํ๋ฆฌํฐ๋ฅผ ๊ตฌํํด๋ณด์!
| ํญ๋ชฉ | ์ผ๋ฐ ์๋ธ๋ฆฟ ํํฐ | Spring Security |
|---|---|---|
| ์ธ์ฆ/์ธ๊ฐ ๋ถ๋ฆฌ | ์ง์ ๊ตฌํํด์ผ ํจ | ๊ตฌ์กฐํ๋์ด ์์ (Filter, Provider, Context ๋ฑ) |
| ์ฌ์ฌ์ฉ์ฑ/์ ์ง๋ณด์ | ๋ก์ง ์ค๋ณต ๋ง์ | ๊ฐ ๋ก์ง์ด ๋ชจ๋ํ๋์ด ์ฌ์ฌ์ฉ ์ฌ์ |
| ์์ธ ์ฒ๋ฆฌ | try-catch ์ง์ ์ฒ๋ฆฌ | ExceptionTranslationFilter๊ฐ ์ฒ๋ฆฌ |
| ThreadLocal ์ธ์ฆ ์ ๋ณด ์ ์ฅ | ์ง์ ๊ตฌํํด์ผ ํจ | SecurityContextHolder ์ ๊ณต |
| ์ค์ /ํ์ฅ์ฑ | ํ๋์ฝ๋ฉ/๋ณต์กํจ | DSL ๋ฐฉ์ (SecurityFilterChain)์ผ๋ก ์ง๊ด์ |
์ฆ, ์คํ๋ง ์ํ๋ฆฌํฐ๋ ๋ณด์์ ํ์ํ ๊ณตํต ๊ตฌ์กฐ๋ฅผ ์ ๋ถ๋ฆฌํ๊ณ ํ์ฅํ๊ธฐ ์ฝ๊ฒ ๋ง๋ค์ด์ค๋ค!!
์คํ๋ง ์ํ๋ฆฌํฐ๋ Filter ๊ธฐ๋ฐ ๋ณด์ ํ๋ ์์ํฌ!
๊ตฌ์กฐ๋ ์๋์ฒ๋ผ ๊ณ์ธต์ ์ผ๋ก ๊ตฌ์ฑ๋์ด ์๋ค
SecurityFilterChain
HTTP ์์ฒญ๋ง๋ค ์๋ํ๋ ๋ณด์ ํํฐ ์ฒด์ธ
์ฐ๋ฆฌ๊ฐ httpSecurity๋ฅผ ํตํด ์ค์ ํ๋ ๊ณณ์ด ์ฌ๊น๋๋ค.
Filter ๊ณ์ธต
์ํ๋ฆฌํฐ๋ ์ฌ๋ฌ ๊ฐ์ Filter๋ฅผ ์ฒด์ธ์ผ๋ก ๋๊ณ ์์ผ๋ฉฐ, ์์ฒญ์ด ์ค๋ฉด ์ด ํํฐ๋ค์ ํต๊ณผํ๋ฉด์ ์ธ์ฆ/์ธ๊ฐ ๋ฑ์ ์ฒ๋ฆฌํฉ๋๋ค. ์ฃผ์ ํํฐ๋ ์๋์ ๊ฐ์ต๋๋ค:
UsernamePasswordAuthenticationFilter
์ ์ ์ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ
ExceptionTranslationFilter
์์ธ ์ฒ๋ฆฌ
FilterSecurityInterceptor
์ธ๊ฐ(์ ๊ทผ ๊ถํ) ์ฒ๋ฆฌ
AuthenticationManager
์ธ์ฆ ๋ก์ง์ ์ฒ๋ฆฌํ๋ ํต์ฌ ๊ฐ์ฒด์
๋๋ค. ์ธ์ฆ ์์ฒญ์ ๋ฐ์์ ์ค์ ์ธ์ฆ์ ์ํํฉ๋๋ค.
AuthenticationProvider
ํน์ ๋ฐฉ์์ ์ธ์ฆ์ ์ฒ๋ฆฌํ๋ ์ปดํฌ๋ํธ
(ex. ์ฌ์ฉ์๋ช
/๋น๋ฐ๋ฒํธ, JWT ๋ฑ)
SecurityContext + SecurityContextHolder
: ์ธ์ฆ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ ์ฅ!
| ์ปดํฌ๋ํธ | ์ญํ |
|---|---|
| JwtFilter (JwtAuthenticationFilter) | ์์ฒญ์์ JWT ํ ํฐ์ ์ถ์ถํ๊ณ , ์ ํจ์ฑ ๊ฒ์ฆ ํ Authentication ๊ฐ์ฒด ์์ฑ ๋ฐ SecurityContext์ ์ ์ฅ |
| JwtUtil (JwtProvider) | JWT ์์ฑ, ํ์ฑ, ๋ง๋ฃ ์ฌ๋ถ, ์๊ทธ๋์ฒ ๊ฒ์ฆ ๋ฑ JWT ๊ด๋ จ ๊ธฐ๋ฅ ์ ๋ด |
| SecurityFilterChain | ์ด๋ค ์์ฒญ์ ์ธ์ฆ์ด ํ์ํ์ง, ์ด๋ค ํํฐ๋ฅผ ๋จผ์ ์คํํ ์ง, ์ด๋ค ๋ฐฉ์์ผ๋ก ๋ก๊ทธ์ธ/์ธ๊ฐ๋ฅผ ์ฒ๋ฆฌํ ์ง ์ค์ |
1. ๋ก๊ทธ์ธํ ๋ (Jwt ํ ํฐ ํ์ํ์ง ์์)
โ filter์ security๋ฅผ ๊ฑฐ์น์ง ์๊ณ , ๋ฐ๋ก jwtUtil์์ ํ ํฐ์ ์์ฑํด์ ๋ฐ๊ธํด์ค๋ค
[Client] โ POST /login {username, password}
โ [AuthController]
โ AuthenticationManager.authenticate()
โ JWT ์์ฑ โ Response
2. Jwtํ ํฐ(์ธ์ฆ/์ธ๊ฐ)๊ฐ ํ์ํ ์์ฒญ์ผ ๋
โ jwtFilter๊ฐ ์์ฒญ์ ๊ฐ๋ก์ฑ์ ํ ํฐ์ด ์ ํจํ์ง ๊ฒ์ฆ
โ ์ ํจํ๋ค๋ฉด securityContext์ ์ ์ฅ
โ ์์ฒญํ controller์ ๊ธฐ๋ฅ์ผ๋ก ์ ๋ฌ (AuthUser๋ฑ์ ์ ๋ณด)
[Client] โ GET /protected-resource
Authorization: Bearer <JWT>
โ [JwtAuthenticationFilter]
โ ํ ํฐ ์ ํจ์ฑ ๊ฒ์ฆ
โ ์ฌ์ฉ์ ์ ๋ณด ๋ก๋ฉ โ Authentication ์์ฑ
โ SecurityContext์ ์ ์ฅ
โ ๋ค์ ํํฐ ๋๋ ์ปจํธ๋กค๋ฌ๋ก ์ ๋ฌ
โ [Controller] ์ธ์ฆ๋ ์ฌ์ฉ์ ์ ๋ณด ์ฌ์ฉ ๊ฐ๋ฅ
JwtUtil์ ํ ํฐ์ ์์ฑ, ํ์ฑ, ๋ง๋ฃ ๋ฑ์ ๋ด๋นํ๋ค
@Component
public class JwtUtil {
private static final String BEARER_PREFIX = "Bearer ";
private static final long TOKEN_TIME = 60 * 60 * 1000L; // 60๋ถ
@Value("${jwt.secret.key}")
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
public String createToken(Long userId, String email, UserRole userRole, String nickName) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("email", email)
.claim("userRole", userRole)
.claim("nickName", nickName) //ํ๋ก ํธ์์ ๊บผ๋ด ์ธ ์ ์๋๋ก nickName ์ถ๊ฐ
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date) // ๋ฐ๊ธ์ผ
.signWith(key, signatureAlgorithm) // ์ํธํ ์๊ณ ๋ฆฌ์ฆ
.compact();
}
public String substringToken(String tokenValue) {
if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
return tokenValue.substring(7);
}
throw new ServerException("Not Found Token");
}
public Claims extractClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
}
1. ๋ง๋ฃ ์๊ฐ, ์ํธํํ jwt secretkey ์ค์ ํ๊ธฐ
@Component
public class JwtUtil {
private static final String BEARER_PREFIX = "Bearer ";
private static final long TOKEN_TIME = 60 * 60 * 1000L; // 60๋ถ
@Value("${jwt.secret.key}")
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
2. ์๋ช ์ ์ํ key ๊ฐ์ฒด ๋ง๋ค๊ธฐ
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
์ฐ๋ฆฌ๊ฐ ๊ฐ์ ธ์จ secretKey๋ ํ
์คํธ์ด๊ธฐ ๋๋ฌธ์, ์ด๊ฑธ ๋ฐ์ดํธ ๋ฐฐ์ด๋ก ๋ณํํด์ผ ์ค์ HMAC ํค๋ก ์ธ ์ ์๋ค.
โ Base64 ๋์ฝ๋ฉ์ ํตํด ๊ทธ ๋ฐ์ดํธ ๋ฐฐ์ด์ ๋ณต์
Keys.hmacShaKeyFor(bytes)
: ๋์ฝ๋ฉ๋ ๋ฐ์ดํธ ๋ฐฐ์ด์ javax.crypto.SecretKey๋ก ๋ณํ
(JWT๋ฅผ ์์ฑํ ๋ ์ฌ์ฉ๋ ์๋ช
์ฉ Key ๊ฐ์ฒด)
โก๏ธ ์ด ํค๋ HS256 ๊ฐ์ ๋์นญ ์๋ช ์๊ณ ๋ฆฌ์ฆ(HMAC)์ ์ฌ์ฉ๋ค๋ค
3. ํ ํฐ ๋ฐ๊ธ ๋ฉ์๋
public String createToken(Long userId, String email, UserRole userRole, String nickName) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("email", email)
.claim("userRole", userRole)
.claim("nickName", nickName) //ํ๋ก ํธ์์ ๊บผ๋ด ์ธ ์ ์๋๋ก nickName ์ถ๊ฐ
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date) // ๋ฐ๊ธ์ผ
.signWith(key, signatureAlgorithm) // ์ํธํ ์๊ณ ๋ฆฌ์ฆ
.compact();
}
์ด์ ํด๋ผ์ด์ธํธ๊ฐ ์์ฒญ์ ๋ณด๋ผ ๋, ๋ก๊ทธ์ธ ์ ๋ฐ๊ธ๋ฐ์ ํ ํฐ์ ํจ๊ป ๋ณด๋ผ ๊ฒ ์ด๋ค!
์ด ํ ํฐ ๊ฐ์ ๊ฒ์ฆํ๊ณ , ์ธ๊ฐํ ์ ์๋ JwtFilter๋ฅผ ๊ตฌํํด๋ณด์
๋จผ์ Spring Security๋ฅผ ๊ตฌํํ ๋๋ ๊ทธ๋ฅ Filter๋ฅผ ์์ํ๋๊ฒ ์๋๋ผ,OncePerRequestFilter๋ฅผ ์์ํ๋ค
โก๏ธ ๋จ ํ ๋ฒ๋ง ๊ฒ์ฆ์ด ์ด๋ฃจ์ด์ง๋ฉด ๋๋ฏ๋ก, OncePerRequestFilter๋ฅผ ์ฌ์ฉํ๋ ๊ฒ!
(์์ฒญ๋น ํ ๋ฒ๋ง ์คํ๋๋ JWT ํํฐ)
์ด ํํฐ๋ ์ฐ๋ฆฌ๊ฐ ์์ฒญํ ๋ ํค๋์ ๋ด๊ธด "๋น๋ฐ๋ฒํธ ๊ฐ์ ํ ํฐ(JWT)"์ ๊ฒ์ฌํด์,
โ์ด ์ฌ๋์ด ์ง์ง ๋ก๊ทธ์ธํ ์ฌ๋์ธ๊ฐ?โ ๋ฅผ ํ์ธํด์ฃผ๋ ๊ฒฝ๋น์ ๊ฐ์ ์ญํ
@AllArgsConstructor
@Component
@Slf4j
public class JwtAuthFilter extends OncePerRequestFilter {
@Component: ์ด๊ฑธ ์คํ๋ง์ด ์๋์ผ๋ก ๋ง๋ค์ด์ ์ฌ์ฉํ ์ ์๊ฒ
@AllArgsConstructor: JwtUtil์ ์์ฑ์์ ์๋์ผ๋ก ์ฝ์
@Slf4j: ๋ก๊ทธ๋ฅผ ๋จ๊ธฐ๊ธฐ (์: ์๋ฌ ๋ฉ์์ง ๊ธฐ๋ก)
โ์์ฒญ์ด ๋ค์ด์ค๋ฉด ํ ํฐ์ด ์๋์ง ํ์ธํ๊ณ , ์์ผ๋ฉด ๊ฒ์ฌํด์, ์ง์ง ์ฌ์ฉ์๋ผ๋ฉด ์ธ์ฆ ์ฒ๋ฆฌํด์ฃผ๊ณ , ์๋๋ฉด ๊ฑฐ์ ํด!โ
String bearerToken = request.getHeader("Authorization");
String url = request.getRequestURI();
๋๊ตฐ๊ฐ ์น ์์ฒญ์ ํ๋ฉด, ๊ทธ ์์ฒญ ํค๋์ ๋ด๊ธด ํ ํฐ("Authorization"์ด๋ผ๋ ์ด๋ฆ)์ ๊บผ๋ด๊ธฐ
if (bearerToken == null || !bearerToken.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
ํ ํฐ์ด ์์ ์๊ฑฐ๋ "Bearer "๋ก ์์ํ์ง ์์ผ๋ฉด,์ธ์ฆ์ ๋ฐ์ง ์์ ๊ฒ.
๐ ๊ทธ๋ฅ ๋ค์ ํํฐ๋ก ๋๊ธฐ์
(๋นํ์ ํ์ด์ง ๊ฐ์ ๊ณณ์ ์ ๊ทผํ ์ ์๊ฒ ํ๊ธฐ ์ํจ)
String jwt = jwtUtil.substringToken(bearerToken);
"Bearer abc.def.ghi"์ฒ๋ผ ๋์ด ์๋ ํ ํฐ์์ "abc.def.ghi"๋ง ์๋ผ๋ด๊ธฐ
claims = ํ ํฐ ์์ ์ ๋ณด
Claims claims = jwtUtil.extractClaims(jwt);
ํ ํฐ์ ํด์ํด์ ๊ทธ ์์ ๋ค์ด ์๋ ์ฌ์ฉ์ ์ ๋ณด(email, userRole ๋ฑ)๋ฅผ ๊บผ๋ด๊ธฐ
if (claims == null) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid JWT");
return;
}
์ ๋ณด๋ฅผ claims๋ก ๊บผ๋์๋ ์๋ฌด๊ฒ๋ ์์ผ๋ฉด? ์๋ชป๋ ํ ํฐ์ด๊ฒ ์ ธ
๐ "์ด๊ฑฐ ์๋ชป๋ ํ ํฐ์ด์ผ!" ํ๊ณ ์์ฒญ์ ๋๋๋ค
if (url.startsWith("/admin")) {
if (!UserRole.ADMIN.equals(userRole)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "๊ด๋ฆฌ์ ๊ถํ์ด ์์ต๋๋ค.");
return;
}
chain.doFilter(request, response);
return;
}
์์ฒญํ ์ฃผ์๊ฐ /admin์ผ๋ก ์์ํ๋ฉด ๐ โ๊ด๋ฆฌ์๋ง ๋ค์ด์ฌ ์ ์๋ ๊ณณ!โ
๊ทธ๋ฐ๋ฐ ์ผ๋ฐ ์ ์ ๊ฐ ์ ๊ทผํ๋ฉด ๐ 403 Forbidden ์ค๋ฅ๋ก ์ฐจ๋จ
AuthUser authUser = new AuthUser(
Long.parseLong(claims.getSubject()),
(String) claims.get("email"),
userRole
);
ํ ํฐ์ ๋ด๊ธด ๋ด์ฉ์ผ๋ก AuthUser ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์,
"์ด ์ฌ๋์ ๋๊ตฌ์ธ์ง" ์ ๋ณด๋ฅผ ๊ฐ๊ณ ์๊ฒ!
request.setAttribute("userId", ...);
์ปจํธ๋กค๋ฌ์์ request.getAttribute("userId") ํ๋ฉด
๐ ํ ํฐ์์ ๊บผ๋ธ ์ฌ์ฉ์ id, email, role์ ๋ค์ ๊บผ๋ผ ์ ์๊ฒ ํด์ค
Collection<? extends GrantedAuthority> authorities =
List.of(new SimpleGrantedAuthority(authUser.getUserRole().toString()));
Authentication authToken = new UsernamePasswordAuthenticationToken(authUser,
null, authorities);
SecurityContextHolder.getContext().setAuthentication(authToken);
์คํ๋ง ์ํ๋ฆฌํฐ์์๋ Authentication ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์
๐ "์ด ์ฌ๋์ ๋ก๊ทธ์ธํ ์ฌ๋์ด๊ณ , ๊ถํ์ ์ด๋ฐ ๊ฑธ ๊ฐ์ก์ด"๋ผ๊ณ ์๋ ค์ค์ผํจ
๋ง์ง๋ง ์ค์ ์ํ๋ฆฌํฐ ์ธ์ ์ ์ธ์ฆ ์ ๋ณด ์ ์ฅํ๋ ๋ถ๋ถ
chain.doFilter(request, response);
์ฌ๊ธฐ๊น์ง ์ฑ๊ณตํ๋ค๋ฉด ๋๋์ด ์ธ์ฆ๋ ์ฌ์ฉ์๋ก์ ๋ค์ ํํฐ๋ ์ปจํธ๋กค๋ฌ๋ก ๋์ด๊ฐ๋ค
} catch (ExpiredJwtException e) { ... }
ํ ํฐ์ด ๋ง๋ฃ๋์๊ฑฐ๋, ์๋ช
์ด ์ด์ํ๊ฑฐ๋, ํฌ๋งท์ด ํ๋ ธ์ผ๋ฉด
๊ฐ๊ฐ์ ์ํฉ์ ๋ง๊ฒ ๋ก๊ทธ ๊ธฐ๋ก + ์๋ฌ ์ฝ๋๋ฅผ ๋ฐํ
์ฌ์ฉ์
|
| HTTP ์์ฒญ (Authorization: Bearer {JWT})
v
JwtAuthFilter (OncePerRequestFilter)
|
|---> JWT ํ ํฐ ์ถ์ถ ๋ฐ ๊ฒ์ฆ
| |
| |---> JwtUtil.extractClaims(token)
| |
| |---> ์๋ช
๊ฒ์ฆ ๋ฐ Claims ์ถ์ถ
| |
| |<--- Claims ๋ฐํ
| |
| |---> ์ฌ์ฉ์ ์ ๋ณด ๋ฐ ๊ถํ ํ์ธ
| |---> Authentication ๊ฐ์ฒด ์์ฑ
| |---> SecurityContextHolder์ ์ธ์ฆ ์ ๋ณด ์ ์ฅ
|
| HTTP ์์ฒญ ์ ๋ฌ
v
๋ค์ ํํฐ ๋๋ ์ปจํธ๋กค๋ฌ
@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfig {
private final JwtAuthFilter jwtAuthFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// CSRF, Form ๋ก๊ทธ์ธ, HTTP Basic ์ธ์ฆ ๋นํ์ฑํ
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
// JWT ์ฌ์ฉ์ ์ํด ์ธ์
์ STATELESS๋ก ์ค์ (์ธ์
์ ๋ณด ์ ์ฅ x)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
//์ธ์ฆ ์ธ๊ฐ URL ์ค์
.authorizeHttpRequests((auth)-> auth
.requestMatchers("/", "/auth/signup", "/auth/signin").permitAll() //์ธ์ฆ ์์ด ์ฌ์ฉ ๊ฐ๋ฅ
.requestMatchers("/admin/**").hasRole("ADMIN") //ADMIN ๊ถํ์ด ์์ด์ผ๋ง ์ฌ์ฉ ๊ฐ๋ฅ
.anyRequest().authenticated()) //๋๋จธ์ง๋ ์ธ์ฆ๋ง ํ์
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
๋ง์ง๋ง์
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
์ด ๋ถ๋ถ ๋๋ถ์, Spring Security์
UsernamePasswordAuthenticationFilter.class ์ด๋ถ๋ถ์ด ์คํ๋๊ธฐ ์ ์
์๊น ์ค์ ํ jwtAuthFilter๊ฐ ์คํ๋๋ฉด์ ํํฐ ์ฒด์ธ์ด ๋์๊ฐ ์ ์๊ฒ ๋๋๊ฒ!
์์ง๋ง๊ณ , filterConfig์์๋ ์๊น๋ง๋ ํํฐ ๋ฑ๋กํ๊ธฐ
@Configuration
@RequiredArgsConstructor
public class FilterConfig {
private final JwtUtil jwtUtil;
@Bean
public FilterRegistrationBean<JwtAuthFilter> jwtFilter() {
FilterRegistrationBean<JwtAuthFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new JwtAuthFilter(jwtUtil));
registrationBean.addUrlPatterns("/*"); // ํํฐ๋ฅผ ์ ์ฉํ URL ํจํด์ ์ง์ ํฉ๋๋ค.
return registrationBean;
}
}