2024. 11. 4. 16:40ใSpring/[2024] Spring Boot
์กฐ๊ธ์ ์๊ฐ์ด ๋จ์ ํญ์ ๋ฏธ๋ค์๋
์คํ๋ง ๋ถํธ์์ JWT ํ ํฐ์ ์ฌ์ฉํด ๋ก๊ทธ์ธ ๊ธฐ๋ฅ์ ๊ตฌํํ๊ณ , Spring Security๋ก ์ธ์ฆ์ ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ํ ์คํธ ํ๋ก์ ํธ ๋ง๋ค์ด๋ณด๊ณ , ํฐ์คํ ๋ฆฌ์ ์ ๋ฆฌํด๋ณธ๋ค.!!
์ด๋ฒ์ REST API ํ๋ก์ ํธ์ ์ธ์ฆ๋ฐฉ์์ผ๋ก JWT ํ ํฐ์ ์ฌ์ฉํ๊ณ , ์ด ๋ถ๋ถ์ ๋ด๊ฐ ์ ๋๋ก ์๊ณ ์๊ฒ ๋ ์ํฉ์ด ๋์ด์,,,
๋ฏธ๋ฆฌ ๋ฏธ๋ฆฌ ๊ณต๋ถํ๋ฉฐ ์ ๋ฆฌ ํด๋๋ค ใ ใ ๐๐
0๏ธโฃ Spring Security, JWT ๊ด๋ จ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ถ๊ฐ
build.gardle
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'com.nimbusds:nimbus-jose-jwt:3.10'
* ์ ์ฒด dependency
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'com.nimbusds:nimbus-jose-jwt:3.10'
}
1๏ธโฃ JWT ์ ํธ๋ฆฌํฐ ํด๋์ค ์์ฑ : JwtTokenProvider
jwt ํ ํฐ์ ์์ฑํ๊ณ , ์ ํจ์ฑ ๊ฒ์ฆํ๋ ํด๋์ค
package com.example.jwtoken.config.jwt;
import static com.example.jwtoken.common.JwtAuthErrorMessage.*;
import java.security.Key;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import com.example.jwtoken.common.JwtAuthCode;
import com.example.jwtoken.common.JwtAuthErrorMessage;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
public class JwtTokenProvider
{
@Value("${jwt.access.expired}")
private long accessTokenExpired;
@Value("${jwt.refresh.expired}")
private long refreshTokenExpired;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final Key key;
public JwtTokenProvider(@Value("${jwt.secretKey}") String secretKey, AuthenticationManagerBuilder authenticationManagerBuilder)
{
this.authenticationManagerBuilder = authenticationManagerBuilder;
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
}
/**
* ์ ๋ณด ๊ฐ์ง๊ณ AccessToken ์์ฑ ๋ฉ์๋
* @param authentication
* @return
*/
public String generateAccessToken(Authentication authentication)
{
// 0. ํ์ฌ ์ธ์ฆ๋ ์ฌ์ฉ์์ ๊ถํ ์ ๋ณด ๋ฌธ์์ด ๋ฐํ
String authorities = getUserAuthToString(authentication);
Date accessTokenExpiresIn = getExpiredDate(accessTokenExpired);
return Jwts.builder()
.setSubject(authentication.getName()) // ํด๋ ์์ ํ์ฌ ์ธ์ฆ๋ ์ฌ์ฉ์ ์ด๋ฆ(id) ์ค์
.claim(JwtAuthCode.AUTHORITIES_KEY.getKey(), authorities) // ์ถ๊ฐ ํด๋ ์์ผ๋ก ์ฌ์ฉ์ ๊ถํ ์ ๋ณด
.setExpiration(accessTokenExpiresIn) // ํ ํฐ ๋ง๋ฃ ์๊ฐ
.signWith(key, SignatureAlgorithm.HS256) // ํ ํฐ์ ์๋ช
์ถ๊ฐ, HMAC-SHA256 ์๊ณ ๋ฆฌ์ฆ ์ฌ์ฉ, ํ ํฐ ์ง์ ๊ฒ์ฆํ ๋ ์ง์
.compact(); // ์ค์ ํ ์ ๋ณด ๋ฐํ์ผ๋ก JWT ์์ฑ -> ๋ฌธ์์ด ๋ฐํ
}
/**
* RefreshToken ์์ฑ ๋ฉ์๋
* @param id
* @return
*/
public String generateRefreshToken(String id)
{
Date now = new Date();
Date refreshTokenExpiresIn = getExpiredDate(refreshTokenExpired);
return Jwts.builder()
.setIssuedAt(now) // IssuedAt : ํด๋ ์ JWT ๋ฐํ ์๊ฐ, ๋ฐํ ์๊ฐ ๊ธฐ๋ฐ์ผ๋ก ํ ํฐ ์ฌ๋ฐํ ์ํฉ ํ๋จ
.setExpiration(refreshTokenExpiresIn)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
/**
* JWT ํ ํฐ ๋ณตํธํํด์ ํ ํฐ์ ๋ค์ด์๋ ์ ๋ณด ์กฐํ
* @param accessToken
* @return
*/
public Authentication getAuthentication(String accessToken)
{
// JWT ํ ํฐ ๋ณตํธํ
Claims claims = parseClaims(accessToken);
if (claims.get("auth") == null)
{
throw new IllegalArgumentException(JWT_AUTHENTICATION_NOT_VALID.getMessage());
}
// auth ํด๋ ์์์ ๊ถํ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
List<SimpleGrantedAuthority> authorities = Arrays.stream(
claims.get(JwtAuthCode.AUTHORITIES_KEY.getKey()).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
UserDetails principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}
public Authentication setAuthentication(String id, String password)
{
Authentication authentication = null;
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(id, password);
// authenticate ๋ฉ์๋๋ฅผ ํตํด ์ธ์ฆ ๊ฒ์ฆ ์งํ
authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
return authentication;
}
/**
* AccessToken ๋ณตํธํ
* @param accessToken
* @return
*/
public Claims parseClaims(String accessToken)
{
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(accessToken)
.getBody();
}
/**
* ์ธ์ฆ๋ ์ฌ์ฉ์์ ๊ถํ์ ๋ฌธ์์ด๋ก ๋ณํํด ๊ฐ์ ธ์ค๋ ๋ฉ์๋
* @param authentication
* @return
*/
private static String getUserAuthToString(Authentication authentication)
{
return authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(","));
}
/**
* AccessToken ๋ง๋ฃ์ผ ์กฐํ
* @return
*/
private Date getExpiredDate(long expiredDuration)
{
long now = new Date().getTime();
return new Date(now + expiredDuration);
}
public boolean validateToken(String jwtToken)
{
try
{
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(jwtToken);
return true;
}
catch (SecurityException | MalformedJwtException e)
{
log.info(JTW_INVALID_TOKEN.getMessage(), e);
}
catch (ExpiredJwtException e)
{
log.info(JWT_EXPIRED_TOKEN.getMessage(), e);
}
catch (UnsupportedJwtException e)
{
log.info(JWT_UNSUPPORTED_TOKEN.getMessage(), e);
}
catch (IllegalArgumentException e)
{
log.info(JWT_CLAIMS_EMPTY.getMessage(), e);
}
return false;
}
}
/**
* ์ ๋ณด ๊ฐ์ง๊ณ AccessToken ์์ฑ ๋ฉ์๋
* @param authentication
* @return
*/
public String generateAccessToken(Authentication authentication)
{
// 0. ํ์ฌ ์ธ์ฆ๋ ์ฌ์ฉ์์ ๊ถํ ์ ๋ณด ๋ฌธ์์ด ๋ฐํ
String authorities = getUserAuthToString(authentication);
Date accessTokenExpiresIn = getExpiredDate(accessTokenExpired);
return Jwts.builder()
.setSubject(authentication.getName()) // ํด๋ ์์ ํ์ฌ ์ธ์ฆ๋ ์ฌ์ฉ์ ์ด๋ฆ(id) ์ค์
.claim(JwtAuthCode.AUTHORITIES_KEY.getKey(), authorities) // ์ถ๊ฐ ํด๋ ์์ผ๋ก ์ฌ์ฉ์ ๊ถํ ์ ๋ณด
.setExpiration(accessTokenExpiresIn) // ํ ํฐ ๋ง๋ฃ ์๊ฐ
.signWith(key, SignatureAlgorithm.HS256) // ํ ํฐ์ ์๋ช
์ถ๊ฐ, HMAC-SHA256 ์๊ณ ๋ฆฌ์ฆ ์ฌ์ฉ, ํ ํฐ ์ง์ ๊ฒ์ฆํ ๋ ์ง์
.compact(); // ์ค์ ํ ์ ๋ณด ๋ฐํ์ผ๋ก JWT ์์ฑ -> ๋ฌธ์์ด ๋ฐํ
}
-> ์ฌ์ฉ์๊ฐ ์ธ์ฆ๋์ ๋ JWT๋ฅผ ์์ฑํ๋ ๋ฉ์๋๋ก ์คํ๋ง ์ํ๋ฆฌํฐ์ Authentication ๊ฐ์ฒด๋ฅผ ์ฌ์ฉํด ์ธ์ฆ๋ ์ฌ์ฉ์ ์ ๋ณด์ ๊ถํ์ JWT์ ๋ด์ ์์ฑ
.setSubject(authentication.getName())
-> ํ ํฐ์ subject ํด๋ ์์ ํ์ฌ ์ธ์ฆ๋ ์ฌ์ฉ์์ ์ด๋ฆ ์ค์ (๋๋ถ๋ถ ์ฌ์ฉ์ id)
.claim(JwtAuthCode.AUTHORITIES_KEY.getKey(), authorities)
-> ํ ํฐ์ ์ถ๊ฐ์ ์ธ ํด๋ ์์ผ๋ก ์ฌ์ฉ์์ ๊ถํ ์ ๋ณด๋ฅผ ์ค์
.signWith(key, SignatureAlgorithm.HS256)
-> ํ ํฐ์ ์๋ช ์ถ๊ฐ
.compact()
-> ์ต์ข ์ ์ผ๋ก ๋น๋ ๊ฐ์ฒด๊ฐ ์ค์ ํ ์ ๋ณด๋ฅผ ๋ฐํ์ผ๋ก JWT๋ฅผ ์์ฑํ์ฌ ๋ฌธ์์ด๋ก ๋ฐํ
/**
* RefreshToken ์์ฑ ๋ฉ์๋
* @param id
* @return
*/
public String generateRefreshToken(String id)
{
Date now = new Date();
Date refreshTokenExpiresIn = getExpiredDate(refreshTokenExpired);
return Jwts.builder()
.setIssuedAt(now) // IssuedAt : ํด๋ ์ JWT ๋ฐํ ์๊ฐ, ๋ฐํ ์๊ฐ ๊ธฐ๋ฐ์ผ๋ก ํ ํฐ ์ฌ๋ฐํ ์ํฉ ํ๋จ
.setExpiration(refreshTokenExpiresIn)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
-> generateRefreshToken ๋ฉ์๋๋ ๋ฆฌํ๋ ์ ํ ํฐ์ ์์ฑํ๋ค.
๋ฆฌํ๋ ์ฌ ํ ํฐ์ ์ก์ธ์ค ํ ํฐ๊ณผ ๋ฌ๋ฆฌ ์ฃผ๋ก ์ฌ์ธ์ฆํ ๋ ์ฌ์ฉํ๋ ๊ฑธ๋ก ์ก์ธ์ค ํ ํฐ์ ๋นํด ๋ง๋ฃ ์๊ฐ์ด ๋ ๊ธธ๋ค.
.setIssuedAt(now)
-> jwt ๊ฐ ๋ฐํ๋ ์์ ์ ํ์ฌ ์๊ฐ์ ์ค์ ํ๋ค.
-> ์๋ฒ๊ฐ ๋ฆฌํ๋ ์ ํ ํฐ์ ๋ฐํ ์๊ฐ์ ๊ธฐ๋ฐ์ผ๋ก ํ ํฐ์ด ๋ง๋ฃ๋์๋์ง, ์ฌ๋ฐํํด์ผ ํ๋ ์ํฉ์ธ์ง ํ๋จ ๊ฐ๋ฅํ๋ค.
.setExpiration(refreshTokenExpiresIn)
-> ํ ํฐ์ ๋ง๋ฃ ์๊ฐ์ ์ค์ ํ๋ค. ์ด ๋ง๋ฃ ์๊ฐ์ด ์ง๋๋ฉด ํ ํฐ์ด ๋ ์ด์ ์ ํจํ์ง ์๋ค.
/**
* JWT ํ ํฐ ๋ณตํธํํด์ ํ ํฐ์ ๋ค์ด์๋ ์ ๋ณด ์กฐํ
* @param accessToken
* @return
*/
public Authentication getAuthentication(String accessToken)
{
// JWT ํ ํฐ ๋ณตํธํ
Claims claims = parseClaims(accessToken);
if (claims.get("auth") == null)
{
throw new RuntimeExcpetion(JWT_AUTHENTICATION_NOT_VALID.getMessage());
}
// auth ํด๋ ์์์ ๊ถํ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
List<SimpleGrantedAuthority> authorities = Arrays.stream(
claims.get(JwtAuthCode.AUTHORITIES_KEY.getKey()).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
UserDetails principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}
/**
* AccessToken ๋ณตํธํ
* @param accessToken
* @return
*/
public Claims parseClaims(String accessToken)
{
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(accessToken)
.getBody();
}
-> getAuthentication ์ด ๋ฉ์๋๋ JWT ํ ํฐ์ ๊ธฐ๋ฐ์ผ๋ก ์ฌ์ฉ์ ์ธ์ฆ ์ ๋ณด๋ฅผ ์์ฑํ๊ณ ๋ฐํํ๋ค.
Claims claims = parseClaims(accessToken);
-> accessToken์ Claims๋ก ํ์ฑํด JWT ์ ๋ด๊ธด ์ ๋ณด ์ถ์ถํ๋ค. parseClaims ๋ฉ์๋๋ ์ฃผ์ด์ง JWT ํ ํฐ์ ํด์ํด ํ์ด๋ก๋์ ํฌํจ๋ ํด๋ ์์ ๊ฐ์ ธ์ค๋ ๋ฉ์๋์ด๋ค.
List<SimpleGrantedAuthority> authorities = Arrays.stream(...).map(...).collect(Collectors.toList());
-> ํด๋ ์ ํค๋ฅผ ์ฐพ์ ๊ถํ ๋ฌธ์์ด์ ๊ฐ์ ธ์ค๊ณ
UserDetails principal = new User(claims.getSubject(), "", authorities);
-> User๋ UserDetails ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ ๊ฐ์ฒด๋ก User ๊ฐ์ฒด๋ Authentification ๊ฐ์ฒด ์์ฑ ์ ์ฌ์ฉ๋๋ค.
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
-> ์ฌ๊ธฐ์ ์์ฑ๋ ๊ฐ์ฒด๊ฐ ์ฌ์ฉ์์ ๋ํ ์ ๋ณด๋ฅผ ํฌํจํ๋ค. ์จํ๋ฆฌํฐ๋ ์ด ๊ฐ์ฒด๋ฅผ ํตํด ์ฌ์ฉ์ ์ธ์ฆ ์ ๋ณด๋ฅผ ๊ด๋ฆฌํ๊ณ , ๊ถํ์ ๊ฒ์ฌํ๋ฉฐ ์ก์ธ์ค ์ ์ดํ ๋ ์ฌ์ฉํ๋ค.
public Authentication setAuthentication(String id, String password)
{
Authentication authentication = null;
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(id, password);
// authenticate ๋ฉ์๋๋ฅผ ํตํด ์ธ์ฆ ๊ฒ์ฆ ์งํ
authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
return authentication;
}
authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
-> authenticate ๋ฉ์๋๋ฅผ ํธ์ถํด ํ ํฐ์ ๋ด๊ธด id๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ ํจํ์ง ๊ฒ์ฆ ์ํ
2๏ธโฃ Spring Security ์ค์ ํ์ผ : SecurityConfig
package com.example.jwtoken.config;
import java.util.HashMap;
import java.util.Map;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
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 org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import com.example.jwtoken.common.JwtAuthenticationEntryPoint;
import com.example.jwtoken.config.jwt.JwtAuthenticationFilter;
import com.example.jwtoken.config.jwt.JwtTokenProvider;
@Configuration
@EnableMethodSecurity
public class SecurityConfig
{
private final JwtTokenProvider jwtTokenProvider;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
public SecurityConfig(JwtTokenProvider jwtTokenProvider, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint)
{
this.jwtTokenProvider = jwtTokenProvider;
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
}
@Bean
public Map<String, RequestMatcher> publicEndPointMatcher()
{
Map<String, RequestMatcher> requestMatcherMap = new HashMap<>();
requestMatcherMap.put("local", new OrRequestMatcher(
new AntPathRequestMatcher("/**/**")
));
return requestMatcherMap;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception
{
// CSRF ๋นํ์ฑํ -> REST API์ ๊ฒฝ์ฐ CSRF ๋ณดํธ ํ์ X, ํ ํฐ ๊ธฐ๋ฐ ์ธ์ฆ ์ฌ์ฉํ ๊ฒฝ์ฐ ์ธ์
์ฌ์ฉ๋ ์ํ๋ฏ๋ก ์ ์ฉ X
// ๋ชจ๋ ์์ฒญ์ JWT ํ ํฐ ์ธ์ฆ์ ์ํ, REST API๋ ๋ณดํต ๋ฌด์ํ(STATELESS) ๋ฐฉ์์ ์ฌ์ฉํ๋ฏ๋ก ์ธ์
์ฌ์ฉ X
// header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable) -> X-Freame-Options ํค๋ ๋นํ์ฑํ, ํ์ฌ ํ์ด์ง ๋ก๋ ์ฐจ๋จ
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.headers(header -> header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.authorizeHttpRequests(auth -> auth.requestMatchers(
publicEndPointMatcher().get("local")).permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(jwtAuthenticationEntryPoint))
;
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder()
{
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
@EnableMethodSecurity:๋ฉ์๋ ๋จ์์ ๋ณด์ ์ค์ ํ์ฑํ, @PreAuthorize ๊ฐ์ ์ด๋ ธํ ์ด์ ํตํด ๋ฉ์๋ ์ ๊ทผ ์ ์ด ๊ฐ๋ฅ
jwtTokenProvider : JWT ํ ํฐ ์์ฑํ๊ณ ๊ฒ์ฆํ๋ ์ญํ ํ๋ ํด๋์ค
jwtAuthenticationEntryPoint : ์ธ์ฆ ์ค๋ฅ ๋ฐ์ํ์ ๋ ์ค๋ฅ ์๋ต ์ฒ๋ฆฌํ๋ ํด๋์ค
@Bean
public Map<String, RequestMatcher> publicEndPointMatcher() {
Map<String, RequestMatcher> requestMatcherMap = new HashMap<>();
requestMatcherMap.put("local", new OrRequestMatcher(
new AntPathRequestMatcher("/**/**")
));
return requestMatcherMap;
}
-> ์ง๊ธ์ ํ
์คํธ ํ๋ก์ ํธ๋ผ ๋ชจ๋ ๊ฒฝ๋ก๋ฅผ ์ฐ์ ํ์ฉํ๋ ๊ณต๊ฐ ํฌ์ธํธ๋ก ์ง์ ํ๋ค.
(์๋ ์ด๋ ๊ฒ ํ๋ฉด ๋น์ฐํ XX)
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.headers(header -> header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.authorizeHttpRequests(auth -> auth.requestMatchers(
publicEndPointMatcher().get("local")).permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(jwtAuthenticationEntryPoint));
return http.build();
}
.csrf(AbstractHttpConfigurer::disable): CSRF ๋ณดํธ๋ฅผ ๋นํ์ฑํ
-> REST API๋ JWT ๊ธฐ๋ฐ ์ธ์ฆ์์๋ CSRF ๋ณดํธ๊ฐ ํ์ํ์ง ์๊ธฐ ๋๋ฌธ์ ๋นํ์ฑํ ํ๋ค.
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
-> REST API๋ ์ผ๋ฐ์ ์ผ๋ก ๋ฌด์ํ์ฑ์ ์ ์งํ๊ธฐ ๋๋ฌธ์ ์ธ์
์ ์์ฑํ์ง ์๋๋ค.
.headers(header -> header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
-> X-Frame-Options ํค๋๋ฅผ ๋นํ์ฑํํ๋ค. ์น ์ ํ๋ฆฌ์ผ์ด์ ์ด ํน์ ํ์ด์ง์์ ํ๋ ์์ ํตํด ๋ค๋ฅธ ํ์ด์ง๋ก ๋ก๋ํ๋ ๊ฒ์ ์ฐจ๋จํ ์ ์๋ค.
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class):
-> ์์ฒญ๋ง๋ค jwt ํ ํฐ์ ํ์ธํ๊ณ ์ ํจ์ฑ ๊ฒ์ฆํ๋ ํํฐ๋ก UsernamePasswordAuthenticationFilter ์ด์ ์ ์คํ๋จ
.exceptionHandling(...):์ธ์ฆ ์ค๋ฅ ๋ฐ์ํ์ ๋ JwtAuthenticationEntryPoint ์ฌ์ฉํด ์์ธ ์ฒ๋ฆฌ
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
-> ๋น๋ฐ๋ฒํธ ์ํธํ ๋ฉ์๋๋ก ๊ธฐ๋ณธ์ ์ผ๋ก๋ BCryptPasswordEncoder ๋ฅผ ์ฌ์ฉํด ๋น๋ฐ๋ฒํธ๋ฅผ ์ํธํํ๋ค.
3๏ธโฃ JWT ์ธ์ฆ ํํฐ : JwtAuthenticationFilter
JWT ํ ํฐ์ ํ์ฑํด SecurityContext์ ์ธ์ฆ ์ ๋ณด๋ฅผ ์ถ๊ฐํด์ฃผ๋ ํํฐ ํด๋์ค
package com.example.jwtoken.config.jwt;
import java.io.IOException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import jakarta.servlet.FilterChain;
import jakarta.servlet.GenericFilter;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
public class JwtAuthenticationFilter extends GenericFilter
{
private final JwtTokenProvider jwtTokenProvider;
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider)
{
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException
{
String jwtToken = getJwtFromRequest((HttpServletRequest) servletRequest);
if (jwtToken != null && jwtTokenProvider.validateToken(jwtToken))
{
Authentication authentication = jwtTokenProvider.getAuthentication(jwtToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(servletRequest, servletResponse);
}
}
// Request Header์์ JWT ํ ํฐ ์ ๋ณด ์ถ์ถ
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
-> ์์ฒญ ํค๋์ ํ ํฐ์ด null ์ด ์๋๊ณ ์ ํจํ๋ค๋ฉด SecurityContextHodler์ ์ธ์ฆ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์ ์ฅํ๊ณ , ์ดํ ์์ฒญ์์ ์ด ์ ๋ณด๋ฅผ ์ฌ์ฉํ ์ ์๋ค.
filterChain.doFilter(request, response)
-> doFilter ๋ฉ์๋๋ฅผ ํธ์ถํด ๋ค์ ํํฐ๋ก ์์ฒญ ์ ๋ฌํ๋ค.
4๏ธโฃ JwtAuthenticationEntryPoint
Spring Security์์ ์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์๊ฐ ๋ฆฌ์์ค ์ ๊ทผํ๋ ค ํ ๋ ํธ์ถ๋๋ ํด๋์ค
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint
{
// ์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์๊ฐ ์ ๊ทผ ์ ํธ์ถ
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException
{
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
-> AuthentificationEntryPoint ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๊ณ , ์ธ์ฆ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ ๋ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ ์ ์
์ฌ๊ธฐ์๋ ์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์๊ฐ ์ ๊ทผํ๋ ค๊ณ ํ๋ฉด 401 ์ค๋ฅ ์๋ต์ ๋ฐํํ๋ค.
๐ฉ๐ป ์ค๋์ ์ฐ์ ์ฌ๊ธฐ๊น์ง ๊ตฌํํ๊ณ , ๋ค์์ ๋ก๊ทธ์ธ ๋ฐ ํ์ ๊ฐ์ ์ ๋ง๋ค ์์ ์ด๋ค.