JWTAuthenticationFilter๋ ๐์ ํ๋ธ ์์์ ๋ณด๊ณ ๋ฐ๋ผํ๊ณ , ์ด ๊ฒ์๊ธ์ ๊ทธ ๋ด์ฉ์ ์ถ๊ฐ๋ก ์ ๋ฆฌํ ๊ธ์ ๋๋ค.
JWT
Header
ํ ํฐ์ ํ์ (๋์ฒด๋ก JWT)๊ณผ ์ ํํ ์ํธํ ์๊ณ ๋ฆฌ์ฆ์ ๋ํด ๋ช ์ํ๊ณ ์๋ค.
{
"alg": "HS256",
"typ": "JWT"
}
์ํธํ ์๊ณ ๋ฆฌ์ฆ์ผ๋ก๋ HS256๊ณผ RS256์ด ์๋ค.
Payload
Claim์ ๋ด๊ณ ์๋ค. Claim์ด๋ ๋จ์ํ๊ฒ ํ ํฐ์ ๋ด๊ธด ์ ๋ณด๋ฅผ ์๋ฏธํ๋๋ฐ ๋ณดํต ์ฌ์ฉ์์ ๊ดํ ์ ๋ณด๋ฅผ ๋ด๋๋ค. Claim์๋ 3๊ฐ์ง ์ข ๋ฅ๊ฐ ์๋ค.
-
๋ฏธ๋ฆฌ ์ ํด์ง ํญ๋ชฉ๋ค์ด๋ค. ํ์๋ ์๋์ง๋ง ๊ถ์ฅ๋๋ ํญ๋ชฉ๋ค์ด๋ค. iss, exp, sub, aud ๋ฑ์ ํญ๋ชฉ์ด๋ค.
-
์์ ๋กญ๊ฒ ์ค์ ํ ์ ์๋ค. ํ์ง๋ง ๋ค๋ฅธ ์๋น์ค์ ์ถฉ๋์ ํผํ๊ธฐ ์ํด ์ฌ๋งํ๋ฉด ํ์ค์ ํด๋นํ๋ ํ๋๋ฅผ ์ฌ์ฉํด ์ค์ ํ๋๋ก ๊ถ์ฅํ๋ค.
-
Private
ํ ํฐ์ ์ฐ๋ ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ์ ์ ํ ํญ๋ชฉ์ ์๋ฏธํ๊ณ , Registered๋ Public claim์ ํด๋นํ์ง ์์์ผ ํ๋ค.
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
์ด๊ณณ์ ์ ์ ๋ช ์ด๋ ๊ด๋ จ๋ ์ ๋ณด๋ค์ ๋ด์ ์ ์์ง๋ง, ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฆํ๋ ์๊ณ ๋ฆฌ์ฆ ๋๋ฌธ์ ๋ฐ์ดํฐ๊ฐ ๊ณต๊ฐ๋์ด ์์ผ๋ฏ๋ก ๋ฏผ๊ฐํ ์ ๋ณด๋ฅผ ๋ด์ผ๋ฉด ์๋๋ค.
Signature
Header์ Payload๋ฅผ ์ํธํํ์ฌ ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฆํ๋ค. ์ด ๋ ์ฌ์ฉํ๋ ์๊ณ ๋ฆฌ์ฆ์ Header์ ์ ์๋์ด ์๋ค.
HS256๋ ๋์นญํค ์๊ณ ๋ฆฌ์ฆ์ผ๋ก, ๋ ์ก์์ ์ ๊ฐ์ ๊ณต์ ๋๋ ๋์นญํค๊ฐ ์์ด์ผ ํ๋ค.
RS256๋ ๋น๋์นญํค ์๊ณ ๋ฆฌ์ฆ์ผ๋ก, ๋น๋ฐํค์ ๊ณต๊ฐํค๊ฐ ์กด์ฌํ๋ค. ๋น๋ฐํค๋ ๋ ธ์ถ๋์ง ์์์ผ ํ๋ค.
JWT๋ฅผ ์ฐ๋ ์ด์
์ฅ์
- ์๋ช ๋ ํ ํฐ์ด๋ฏ๋ก, ๋ค๋ฅธ ํ ํฐ๋ณด๋ค ๋ณด์์ฑ์ด ๋๋ค.
- ํ ํฐ์ payload์ ์ ๋ณด๋ฅผ ์ ์ฅํ ์ ์๋ค.
- ๋๋ฉ์ธ๊ณผ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ด๊ณ์์ด ์ฌ์ฉ๊ฐ๋ฅํ๋ค.
- ํ์ค์ผ๋ก ์ง์ ๋์ด ์๋ค.
๋จ์
- payload๊ฐ ์ปค์ง ๊ฒฝ์ฐ ํต์ ์ ๋ถํ๊ฐ ๊ฑธ๋ฆฌ๊ฒ ๋๋ค.
- ํ ํฐ์ ์๋ช ์ ๊ด๋ฆฌํด์ผํ๋ค.
DefaultSecurityFilterChain
Spring Security๋ฅผ ์ฌ์ฉํ๋ฉด ๊ธฐ๋ณธ์ ์ผ๋ก ์ ์ฉ๋๋ FilterChain์ DefaultSecurityFilterChain
์ด๋ค. ์ค๋จ์ ์ ์ฐ๊ณ ๋๋ฒ๊น
์ ํด๋ณด๋ฉด ์๋ ์ฌ์ง์ฒ๋ผ FilterChain์ ๋ด๊ฒจ์๋ ์ฌ๋ฌ ํํฐ๋ค์ ๋ณผ ์ ์๋ค.
๊ฐ๋จํ๊ฒ ์ด๋ค ํํฐ๋ค์ธ์ง ์ ๋ฆฌํด๋ณด์๋ค.
ํํฐ | ์ค๋ช |
---|---|
DisableEncodeUrlFilter |
Url๋ก ์ฌ๊ฒจ์ง์ง ์๋ Url์ ์ธ์ id๊ฐ ํฌํจ๋์ง ์๋๋ก ํ๋ค. |
WebAsyncManagerIntegrationFilter |
SecurityContext์ Spring์ WebAsyncManager๋ฅผ ํตํฉํ๋ค. |
SecurityContextHolderFilter |
SecurityContextRepository๋ฅผ ์ฌ์ฉํ์ฌ SecurityContext๋ฅผ ํ๋ํ ํ SecurityContextHolder์ ์ ์ฅํ๋ค. ์ด๋ ๊ฒ ํ๋ฉด ๋ค์ํ ์ธ์ฆ๊ณผ์ ์์ ํ์ฉํ ์ ์๊ฒ ๋๋ค. |
HeaderWriterFilter |
X-Frame-Options, X-Xss-Protection, X-Content-Type-Option๊ณผ ๊ฐ์ ํค๋๋ฅผ ์ถ๊ฐํ๋ค. |
CorsFilter |
pre-flight ์์ฒญ๊ณผ CORS ์์ฒญ์ ์ฒ๋ฆฌํ๊ณ ์๋ต ํค๋๋ฅผ ์ ๋ฐ์ดํธํ๋ค. |
LogoutFilter |
๋ก๊ทธ์์ ์ฒ๋ฆฌ๋ฅผ ํ๋ค. ์ค์ ๋ก ๋ก๊ทธ์์ ์ฒ๋ฆฌ๊ฐ ๋์๋ค๋ฉด ๋ฆฌ๋ค์ด๋ ํธ๋ฅผ ํ๋ค. |
RequestCacheAwareFilter |
ํ์ฌ ์์ฒญ์ด ์บ์๋ ์์ฒญ๊ณผ ์ผ์นํ๋ฉด ์บ์๋ ์์ฒญ์ ๋์ ์ฒ๋ฆฌํ๋ค. |
SecurityContextHolderAwareRequestFilter |
... |
AnonymousAuthenticationFilter |
... |
ExceptionTranslationFilter |
... |
AuthorizationFilter |
... |
Jwt Authentication Flow
์ด๊ธฐํ ๊ณผ์ ์์ 3๊ฐ์ ํํฐ๊ฐ ๋ ์ถ๊ฐ๋๋๋ฐ, ์๋ ์ฌ์ง์ ๋ณด๋ฉด UsernamePasswordAuthenticationFilter์
DefaultLoginPageGeneratingFilter
, DefaultLogoutPageGeneratingFilter๊ฐ
์ถ๊ฐ๋์๋ค.
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
UsernamePasswordAuthenticationFilter
์ ํต๊ณผํ ๋ attemptAuthentication
๋ฉ์๋๊ฐ ์คํ๋๋ค. ์ด ๋ฉ์๋๋ ์์ฒญ์ผ๋ก๋ถํฐ Username, password๋ฅผ ํ๋ํ ํ AuthenticationToken์
์์ฑํ๊ณ AuthenticationManager
์๊ฒ ์ธ์ฆ์ ๋งก๊ธด๋ค.
Jwt๋ก ์ธ์ฆ์ ์งํํ๋ค๋ฉด, ์ด ๊ณผ์ ์ ๊ฑฐ์น์ง ์์ผ๋ฏ๋ก ์ดํ์ ์คํ๋๋ AuthorizationFilter
์์ ์๋ฌ๊ฐ ๋ ์๋ฐ์ ์๋ค(Authentication
๊ฐ์ฒด๊ฐ ์๊ธฐ ๋๋ฌธ์ Role์ด๋ Authority๋ฅผ ์กฐํํ ์ ์๋ค). ๋ฐ๋ผ์ ํํฐ ๋ด๋ถ์์ Jwt๋ฅผ ๊ฒ์ฆํ๊ณ , ์ ์ ํ Authentication
๊ฐ์ฒด๋ฅผ ์ฐพ์ SecurityContext
์ ๋ฑ๋กํด์ผ ํ๋ค.
- http ํจํท ํค๋์ ๋ด๊ธด ํ ํฐ์ ๊ฒ์ฆํ์ฌ ์ ํจํ์ง ํ๋จํ๋ค. ๋ง์ฝ ์ ํจํ์ง ์์ ํ ํฐ์ด๋ผ๋ฉด ํํฐ๋ ๋์ํ์ง ์๋๋ค.
- ํ ํฐ์ ๋ด๊ธด Payload๋ฅผ ํ์ฑํ์ฌ ํ ํฐ์ ๋ด๊ธด ์ฌ์ฉ์๋ช
์ ์ฝ์ด์จ ํ ๋ฏธ๋ฆฌ ์ค์ ํ
UserDetailsService
Bean ๊ฐ์ฒด๋ฅผ ํตํด ํด๋นํ๋ ์ฌ์ฉ์์UserDetails
๊ฐ์ฒด๋ฅผ ์ป์ด์จ๋ค. UserDetails๋ฅผ
AutheticationToken
์ผ๋ก ๊ฐ๊ณตํ์ฌSecurityContext
์ ๋ฑ๋กํ๋ค.AuthorizationFilter
์์SecurityContext
๋ฅผ ์กฐํํ ๋, 3๋ฒ์์ ๋ฑ๋กํ ๊ฐ์ฒด๋ฅผ ์กฐํํจ์ผ๋ก ์ธ์ฆ๊ณผ ํ๊ฐ๊ฐ ํต๊ณผ๋๋ค.
๊ตฌํ
// JwtAuthenticationFilter.java
package com.learn.security;
import com.learn.security.service.UserService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtGenerator jwtGenerator;
@Autowired
private UserService userService;
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
String token = getJwtFromRequest(request);
if (StringUtils.hasText(token) && jwtGenerator.validateToken(token)) {
String username = jwtGenerator.getUsernameFromJWT(token);
UserDetails userDetails = userService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
JwtAuthenticationFilter๋ฅผ ๊ตฌํํ ๋์๋ OncePerRequestFilter
๋ฅผ ์์ํ์ฌ ๊ตฌํํ๋ค. ์ด ํด๋์ค๋ ๊ฐ ์์ฒญ๋ง๋ค ํ ๋ฒ์ฉ ๊ผญ ์คํ๋๋ ํํฐ์ด๋ฏ๋ก, ๋งค ์์ฒญ๋ง๋ค ์ธ์ฆ ์ฒ๋ฆฌ๋ฅผ ๊ฑฐ์น๊ฒ ๋๋ค.
// JwtGenerator.java
package com.learn.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.CompressionAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import static com.learn.security.SecurityConstants.*;
@Component
public class JwtGenerator {
public boolean validateToken(String token) {
SecretKey key = getSecretKey();
try {
Jwts.parser().verifyWith(key).build().parseSignedClaims(token);
return true;
} catch (Exception e) {
throw new AuthenticationCredentialsNotFoundException("Jwt was expired or incorrect.");
}
}
public String getUsernameFromJWT(String token) {
SecretKey key = getSecretKey();
Claims claims = Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload();
return claims.getSubject();
}
public String generateToken(Authentication authentication) {
String username = authentication.getName();
Date currentDate = new Date();
Date expireDate = new Date(currentDate.getTime() + JWT_EXPIRATION);
SecretKey key = getSecretKey();
String token = Jwts.builder()
.subject(username)
.issuedAt(expireDate)
.signWith(key, Jwts.SIG.HS512)
.compact();
return token;
}
public SecretKey getSecretKey() {
// JWT_SECRET์ ๋ค๋ฅธ ํด๋์ค์ ์ ์ํด๋ ์์์ ๋์๋ก ์ํธํ์ ์ฌ์ฉ๋๋ค.
return Keys.hmacShaKeyFor(JWT_SECRET.getBytes());
}
}
// SecurityConfig.java
package com.learn.security;
import com.learn.security.service.UserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.util.Objects;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.exceptionHandling((exception) -> exception.authenticationEntryPoint(jwtAuthEntryPoint()))
.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests((authorize) ->
authorize
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
);
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public UserService userDetailsService() {
return new UserService();
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Bean
public JwtAuthEntryPoint jwtAuthEntryPoint() {
return new JwtAuthEntryPoint();
}
}
Jwt ์ธ์ฆ๋ฐฉ์์ ์ธ์ ์ ์ฌ์ฉํ์ง ์๊ธฐ ๋๋ฌธ์ ์ธ์ ์ ์ํ๋ฅผ STATELESS๋ก ์ค์ ํด์ผํ๋ค.
๊ตฌํํ JwtAuthenticationFilter
๋ฅผ ๋ฑ๋กํ ๋์๋ addFilterBefore
๋ฅผ ์ด์ฉํ์ฌ UsernamePasswordAuthenticationFilter
์ ์ง์ ์ ๋ฑ๋กํ๋ค.
/api/auth/**
ํจํด์ ๋ถํฉํ๋ URL๋ก ์์ฒญ์ด ๋ค์ด์ฌ ๊ฒฝ์ฐ, ๋ก๊ทธ์ธ ๋ฐ ๊ณ์ ๊ณผ ๊ด๋ จ๋ ์์ฒญ์ด๊ธฐ ๋๋ฌธ์ permitAll()
์ฒ๋ฆฌ๋ฅผ ํ์ฌ ์ธ์ฆ์ ์๊ตฌํ์ง ์๋๋ก ์ฒ๋ฆฌํ๋ค. ๊ทธ๋ฆฌ๊ณ ์ธ์ฆ์ ์คํจํ์ ๋ ์์ธ์ฒ๋ฆฌ๋ฅผ ํ๊ธฐ ์ํด JwtAuthEntryPoint
๋ฅผ ๋ฑ๋กํ์๋ค.
// LoginController.java
package com.learn.security.controller;
import com.learn.security.dto.AuthResponseDto;
import com.learn.security.JwtGenerator;
import com.learn.security.dto.LoginDto;
import com.learn.security.entity.User;
import com.learn.security.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;
@RequestMapping("/api/auth")
@RestController
public class LoginController {
private final UserService userService;
private final AuthenticationManager authenticationManager;
private final JwtGenerator jwtGenerator;
@Autowired
public LoginController(UserService userService, AuthenticationManager authenticationManager, JwtGenerator jwtGenerator) {
this.userService = userService;
this.authenticationManager = authenticationManager;
this.jwtGenerator = jwtGenerator;
}
// 201 ์ฝ๋๋ฅผ ๋ฐํํ๊ธฐ ์ํด ResponseEntity๋ฅผ ์ฌ์ฉํ๋ค.
@PostMapping("/signup")
public ResponseEntity<User> signup(@RequestBody LoginDto loginDto) {
Optional<User> joinedUser = userService.join(loginDto);
return new ResponseEntity<>(joinedUser.get(), HttpStatus.CREATED);
}
@PostMapping("/signin")
@ResponseBody
public AuthResponseDto signIn(@RequestBody User user) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
user.getUsername(),
user.getPassword()
));
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = jwtGenerator.generateToken(authentication);
return new AuthResponseDto(token);
}
}
๋ก๊ทธ์ธ์ ํ ๋์๋ username๊ณผ password๊ฐ ์ ๋ฌ๋๊ธฐ ๋๋ฌธ์ ์ด๋ฅผ ์ธ์ฆํ ํ jwt๋ฅผ ์์ฑํด์ ๊ณง๋ฐ๋ก ๋ฐํํ๋๋ก ์ฒ๋ฆฌํ๋ค.
LoginController๋ง permitAll()
์ด ๋์ด์๊ณ ๊ทธ ์ธ์๋ ์ ๋ถ authenticated()
์ด๋ฏ๋ก, JwtAuthenticationFilter
๊ฐ ๋์ํ๋ค.
LoginDto, User, UserService ๊ฐ์ ํด๋์ค๋ค์ ๊นํ๋ธ ๋งํฌ๋ก ๋์ฒดํ๊ฒ ์ต๋๋ค... github.com/10cheon00/learn-spring-security