ํ๋ก์ ํธ์์ ๊ฐ๋จํ jwt๋ฅผ ์ด์ฉํ ๋ก๊ทธ์ธ์ ๊ตฌํํด ๋ดค๋ค!
๐ฑ Spring Security๋?
์ธ์ฆ(Authentication), ๊ถํ(Authorize) ๋ถ์ฌ ๋ฐ ๋ถํธ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ํ๋ ์์ํฌ์ด๋ค.
์ธ์ฆ vs ์ธ๊ฐ
๋ณดํต ์ธ์ฆ ์ ์ฐจ๋ฅผ ๊ฑฐ์น ํ ์ธ๊ฐ ์ ์ฐจ๋ฅผ ๊ฑฐ์น๋ค.
์ธ์ฆ
- ํ์ฌ ์ ์ ๊ฐ ๋๊ตฌ์ธ์ง ํ์ธํ๋ ๊ณผ์ (๋ก๊ทธ์ธ)
- ์ ํ๋ฆฌ์ผ์ด์ ์ ์์ ์ ์ํํ ์ ์๋์ง ์ฆ๋ช
์ธ๊ฐ
- ํ์ฌ ์ ์ ์ ๊ถํ์ ๊ฒ์ฌํ๋ ๊ณผ์
- ํ์ด์ง๋ ๋ฆฌ์์ค ๋ฑ์ ์ ๊ทผํ ์ ์๋์ง ๊ฒ์ฌ
Spring Security ๊ตฌ์กฐ
1. Http Request ์์
์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ ์ ๋ณด์ ํจ๊ป ์ธ์ฆ ์์ฒญ์ ํ๋ค.
2. ์ ์ ์๊ฒฉ ๊ธฐ๋ฐ์ผ๋ก ์ธ์ฆ ํ ํฐ ์์ฑ
AuthenticationFilter๊ฐ ์์ฒญ์ ๊ฐ๋ก์ฑ๊ณ , ๊ฐ๋ก์ฑ ์ ๋ณด๋ฅผ ํตํด UsernamePasswordAuthenticationToken์ ์ธ์ฆ์ฉ ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ค.
3. Filter๋ฅผ ํตํด AuthenticationToken์ AuthenticationManager๋ก ์์
AuthenticationManager์ ๊ตฌํ์ฒด์ธ ProviderManager์๊ฒ ์์ฑํ UsernamePasswordToken ๊ฐ์ฒด๋ฅผ ์ ๋ฌํ๋ค.
4. AuthenticationProvider์ ๋ชฉ๋ก์ผ๋ก ์ธ์ฆ ์๋
AuthenticationManager๋ ๋ฑ๋ก๋ AuthenticationProvider๋ฅผ ์กฐํํ๋ฉฐ ์ธ์ฆ์ ์๊ตฌํ๋ค.
5. UserDetailsService ์๊ตฌ
์ค์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ฌ์ฉ์ ์ธ์ฆ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ UserDetailsService์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋๊ฒจ์ค๋ค.
6. UserDetails๋ฅผ ์ด์ฉํด User ๊ฐ์ฒด์ ๋ํ ์ ๋ณด ํ์
๋๊ฒจ๋ฐ์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ํตํด ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ฐพ์๋ธ ์ฌ์ฉ์ ์ ๋ณด์ธ UserDetails ๊ฐ์ฒด๋ฅผ ๋ง๋ ๋ค.
7. User ๊ฐ์ฒด์ ์ ๋ณด๋ค์ UserDetails๊ฐ UserDetailsService(LoginService)๋ก ์ ๋ฌ
AuthenticationProvider๋ค์ UserDetails๋ฅผ ๋๊ฒจ๋ฐ๊ณ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋น๊ตํ๋ค.
8. ์ธ์ฆ ๊ฐ์ฒด or AuthenticationException
์ธ์ฆ์ด ์๋ฃ๋๋ฉด ๊ถํ ๋ฑ์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ด์ Authentication ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ค.
9. ์ธ์ฆ ์๋ฃ
๋ค์ ์ต์ด์ AuthenticationFilter์ Authentication ๊ฐ์ฒด๊ฐ ๋ฐํ๋๋ค.
10. SpringContext์ ์ธ์ฆ ๊ฐ์ฒด ์ค์
Authentication ๊ฐ์ฒด๋ฅผ Security Context์ ์ ์ฅํ๋ค.
์ต์ข ์ ์ผ๋ก๋ SpringContext์ ์ ์ฅ์ด ๋๊ณ , ์ฌ๊ธฐ์๋ ์ธ์ /์ฟ ํค ๋ฐฉ์์ ์ฌ์ฉํ๊ฒ ๋๋ค.
๊ทผ๋ฐ ์ฐ๋ฆฌ๋ jwt๋ฅผ ์ด์ฉํ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ์ถ๊ฐ์ ์ธ ์ฝ๋ ์์ ์ด ํ์ํ๋ค!
โ๏ธ ์ฝ๋
1. ํด๋ผ์ด์ธํธ ๋ก๊ทธ์ธ
2. ์ ๋ณด ๊ฒ์ฆ ํ access token/ refresh token ๋ฐ๊ธ
3. refresh token์ redis์ ์ ์ฅ ํ ํด๋ผ์ด์ธํธ์๊ฒ access token, refresh token ์ ๋ฌ
4. access token์ด ๋ง๋ฃ๋์์ผ๋ฉด refresh token์ ํตํด access token, refresh token ์ฌ์์ฑ
JwtTokenProvider
- ์ ์ ์ ๋ณด๋ก jwt access/refresh token ์์ฑ ๋ฐ ์ฌ๋ฐ๊ธ
- ํ ํฐ์ผ๋ก๋ถํฐ ์ ์ ์ ๋ณด ๋ฐ์์ด
JwtFilter
- request ์๋จ์ ๋ถ์ด๋ ํํฐ๋ก http request์์ ํ ํฐ์ ๋ฐ์์ ์ ์ ํ ํฐ์ผ ๊ฒฝ์ฐ์ securityContext์ ์ ์ฅ
JwtSecurityConfig
- JwtFilter๋ฅผ Spring Security Filter Chain์ ์ถ๊ฐํ๊ธฐ ์ํ ์ค์
SecurityConfig
- ์คํ๋ง์์ ๊ธฐ๋ณธ์ ์ผ๋ก ํ์ํ ์ํ๋ฆฌํฐ ์ค์
- jwt ์ ์ฉ ๋ฐ authentication ํ์ํ API ์ฃผ์ ์ค์
JwtAccessDeniedHandler
- ์ ๊ทผ ๊ถํ ์์ ๋ 403 ์๋ฌ
JwtAuthenticationEntryPoing
- ์ธ์ฆ ์ ๋ณด ์์ ๋ 401 ์๋ฌ
CorsConfig
- ์๋ก ๋ค๋ฅธ ์๋ฒ ํ๊ฒฝ์์ ์์์ ๊ณต์ ํ๊ธฐ ์ํด ํ์ํ ์ค์
๊ฐ๋ฐ ํ๊ฒฝ
- SpringBoot 3.2.1
- Java 17
- Gradle
build.gradle ์์กด์ฑ ์ถ๊ฐ
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-security'
// JWT
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
application-security.yml
jwt:
#HS512 ์๊ณ ๋ฆฌ์ฆ์ ์ฌ์ฉํ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ 512bit, ์ฆ 64byte ์ด์์ secret key๋ฅผ ์ฌ์ฉํด์ผ ํ๋ค.
secret: lkasjfljadakldjflkajefijalefjjahfglaiefjkleajdflkdklafjdskljflasjeifjaefjfdasfdagega
access-token-expire-time: 43200000 # 12์๊ฐ
refresh-token-expire-time: 604800000 # 7์ผ
RedisConfig
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.port}")
private int port;
@Value("${spring.data.redis.host}")
private String host;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
public RedisTemplate<String, String> redisTemplate() {
// redisTemplate๋ฅผ ๋ฐ์์์ set, get, delete๋ฅผ ์ฌ์ฉ
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
// setKeySerializer, setValueSerializer ์ค์
// redis-cli์ ํตํด ์ง์ ๋ฐ์ดํฐ๋ฅผ ์กฐํ ์ ์์๋ณผ ์ ์๋ ํํ๋ก ์ถ๋ ฅ๋๋ ๊ฒ์ ๋ฐฉ์ง
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
JwtTokenProvider
import com.study.footprint.common.exception.CommonBadRequestException;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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 java.security.Key;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
@Component
public class JwtTokenProvider {
private static final String AUTHORITIES_KEY = "auth";
private static final String BEARER_TYPE = "Bearer";
@Value("${jwt.access-token-expire-time}")
private long accessExpirationTime;
@Value("${jwt.refresh-token-expire-time}")
private long refreshExpirationTime;
private final Key key;
private final RedisTemplate<String, String> redisTemplate;
public JwtTokenProvider(@Value("${jwt.secret}") String secretKey, RedisTemplate<String, String> redisTemplate) {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
this.redisTemplate = redisTemplate;
}
// Access Token ์์ฑ
public String createAccessToken(Authentication authentication) {
// ๊ถํ๋ค ๊ฐ์ ธ์ค๊ธฐ
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
long now = (new Date()).getTime();
// Access Token ์์ฑ
Date accessTokenExpiresIn = new Date(now + accessExpirationTime);
return Jwts.builder()
.setSubject(authentication.getName()) // payload "sub": "name"
.claim(AUTHORITIES_KEY, authorities) // payload "auth": "ROLE_USER"
.setExpiration(accessTokenExpiresIn) // payload "exp": 151621022 (ex)
.signWith(key, SignatureAlgorithm.HS512) // header "alg": "HS512"
.compact();
}
// Refresh Token ์์ฑ
public String createRefreshToken(Authentication authentication) {
long now = (new Date()).getTime();
String refreshToken = Jwts.builder()
.setExpiration(new Date((new Date()).getTime() + refreshExpirationTime))
.signWith(key, SignatureAlgorithm.HS512)
.compact();
//redis ์ ์ฅ
redisTemplate.opsForValue().set(
authentication.getName(),
refreshToken,
refreshExpirationTime,
TimeUnit.MICROSECONDS
);
return refreshToken;
}
public Authentication getAuthentication(String accessToken) {
// ํ ํฐ ๋ณตํธํ
Claims claims = parseClaims(accessToken);
if (claims.get(AUTHORITIES_KEY) == null) {
throw new CommonBadRequestException("invalidToken");
}
// ํด๋ ์์์ ๊ถํ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// UserDetails ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ Authentication ๋ฆฌํด
UserDetails principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (ExpiredJwtException e) {
log.info("๋ง๋ฃ๋ JWT ํ ํฐ์
๋๋ค.");
throw new CommonBadRequestException("expiredToken");
} catch (Exception e) {
log.info("์ ํจํ์ง ์์ JWT ํ ํฐ์
๋๋ค.");
throw new CommonBadRequestException("invalidToken");
}
}
private Claims parseClaims(String accessToken) {
try {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
}
createAccessToken, createRefreshToken
- ์ ์ ์ ๋ณด๋ฅผ ๋๊ฒจ๋ฐ์์ ํ ํฐ์ ์์ฑํ๋ค.
- ๋๊ฒจ๋ฐ์ authentication์ getName() ๋ฉ์๋๋ฅผ ํตํด username์ ๊ฐ์ ธ์จ๋ค.
- ๋ง๋ฃ์๊ฐ์ ์ค์ ํ๋ค.
getAuthentication
- ํ ํฐ์ ๋ณตํธํํ์ฌ ํ ํฐ์ ๋ค์ด์๋ ์ ์ ์ ๋ณด๋ฅผ ๊บผ๋ธ ํ authentication ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ค.
validateToken
- ํ ํฐ ์ ๋ณด๋ฅผ ๊ฒ์ฆํ๋ค.
JwtFilter
import com.study.footprint.common.exception.CommonServerException;
import com.study.footprint.config.auth.JwtTokenProvider;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class JwtFilter extends OncePerRequestFilter {
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String BEARER_PREFIX = "Bearer ";
private final JwtTokenProvider jwtTokenProvider;
public JwtFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
// ์ค์ ํํฐ๋ง ๋ก์ง์ doFilterInternal ์ ๋ค์ด๊ฐ
// JWT ํ ํฐ์ ์ธ์ฆ ์ ๋ณด๋ฅผ ํ์ฌ ์ฐ๋ ๋์ SecurityContext ์ ์ ์ฅํ๋ ์ญํ ์ํ
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
//Request Header ์์ ํ ํฐ์ ๊บผ๋
String jwt = resolveToken(request);
try {
if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
Authentication authentication = jwtTokenProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (RedisConnectionFailureException e) {
SecurityContextHolder.clearContext();
throw new CommonServerException("redisError");
} catch (Exception e) {
SecurityContextHolder.clearContext();
throw new CommonServerException("invalidToken");
}
filterChain.doFilter(request, response);
}
// Request Header ์์ ํ ํฐ ์ ๋ณด๋ฅผ ๊บผ๋ด์ค๊ธฐ
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.split(" ")[1].trim();
}
return null;
}
}
OncePerRequestFilter
- ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๊ธฐ ๋๋ฌธ์ ์์ฒญ๋ฐ์ ๋ ๋จ ํ ๋ฒ๋ง ์คํ๋๋ค.
doFilterInternal
- ์ค์ ํํฐ๋ง ๋ก์ง์ ์ํํ๋ค.
- Request Header์์ Access Token์ ๊บผ๋ด๊ณ ์ ์ ์ ๋ณด๋ฅผ ๊บผ๋ด SecurityContext์ ์ ์ฅํ๋ค.
- SecurityConfig์์ ๊ถํ ์ฒดํฌ๊ฐ ํ์ํ์ง ์๋ค๊ณ ์ ์ํ ๊ฒ ์ด์ธ์ ๋ชจ๋ ์์ฒญ์ ์ด ํํฐ๋ฅผ ๊ฑฐ์น๊ธฐ ๋๋ฌธ์ ํ ํฐ ์ ๋ณด๊ฐ ์๊ฑฐ๋ ์ ํจํ์ง ์๋ค๋ฉด ์ ์์ ์ผ๋ก ์ํ๋์ง ์๋๋ค.
resolveToken
- Request Header์์ ํ ํฐ ์ ๋ณด๋ฅผ ๊บผ๋ด์ค๊ธฐ ์ํ ๋ฉ์๋์ด๋ค.
JwtSecurityConfig
import com.study.footprint.config.auth.filter.JwtFilter;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
// ์ง์ ๋ง๋ TokenProvider ์ JwtFilter ๋ฅผ SecurityConfig ์ ์ ์ฉํ ๋ ์ฌ์ฉ
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final JwtTokenProvider jwtTokenProvider;
public JwtSecurityConfig(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
// TokenProvider ๋ฅผ ์ฃผ์
๋ฐ์์ JwtFilter ๋ฅผ ํตํด Security ๋ก์ง์ ํํฐ๋ฅผ ๋ฑ๋ก
@Override
public void configure(HttpSecurity http) {
JwtFilter customFilter = new JwtFilter(jwtTokenProvider);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
- SecurityConfigurerAdapter <DefaultSecurityFilterChain, HttpSecurity> ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๋ ๊ตฌํ์ฒด์ด๋ค.
- ์ง์ ๋ง๋ JwtFilter๋ฅผ Security Filter ์์ ์ถ๊ฐํ๋ค.
JwtAuthenticationEntryPoint
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence (HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// ์ ํจํ ์๊ฒฉ์ฆ๋ช
์ ์ ๊ณตํ์ง ์๊ณ ์ ๊ทผํ๋ ค ํ ๋ 401
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
- ์ฌ์ฉ์ ์ ๋ณด๊ฐ ์๋ชป๋๊ฑฐ๋, ํ ํฐ์ด ์ ํจํ์ง ์์ ๊ฒฝ์ฐ 401 ์๋ฌ๋ฅผ ๋ฐ์์ํจ๋ค.
JwtAccessDeniedHandler
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle (HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
// ํ์ํ ๊ถํ์ด ์์ด ์ ๊ทผํ๋ ค ํ ๋ 403
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
}
- ํ์ํ ๊ถํ์ด ์กด์ฌํ์ง ์์ ๊ฒฝ์ฐ 403 ์๋ฌ๋ฅผ ๋ฐ์์ํจ๋ค.
CorsConfig
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true); //๋ด ์๋ฒ๊ฐ ์๋ตํ ๋ json ์๋ฐ ์คํฌ๋ฆฝํธ ํ์ฉ
config.addAllowedOrigin("*"); //ํฌํธ๋ฒํธ ์๋ต ๋ค๋ฆ ํ์ฉ
config.addAllowedHeader("*"); //ํค๋ ๊ฐ ์๋ต ํ์ฉ
config.addAllowedMethod("*"); //๋ฉ์๋ ์๋ต ํ์ฉ
source.registerCorsConfiguration("/**", config); //๋ชจ๋ url์ ๋ํ์ฌ ์ ๋ค์ ์ ์ฉ์ํค๊ฒ ๋ค.
return new CorsFilter(source);
}
}
- ํ๋ก ํธ์๋, ๋ฐฑ์๋๋ฅผ ๊ตฌ๋ถ์ง์ด์ ๊ฐ๋ฐํ๊ฑฐ๋ ์๋ก ๋ค๋ฅธ ์๋ฒ ํ๊ฒฝ์์ ์์์ ๊ณต์ ํ ๋ ์ค์ ํ๋ค.
- Cors ์ค์ ์ด ์ ๋์ด์์ผ๋ฉด ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ค.
SecurityConfig
import com.study.footprint.config.auth.JwtSecurityConfig;
import com.study.footprint.config.auth.JwtTokenProvider;
import com.study.footprint.config.auth.handler.JwtAccessDeniedHandler;
import com.study.footprint.config.auth.handler.JwtAuthenticationEntryPoint;
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.configuration.EnableWebSecurity;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.CorsFilter;
@EnableWebSecurity
@EnableMethodSecurity
@Configuration
public class SecurityConfig {
private final JwtTokenProvider jwtTokenProvider;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
private final CorsFilter corsFilter;
public SecurityConfig(JwtTokenProvider jwtTokenProvider, CorsFilter corsFilter,
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
JwtAccessDeniedHandler jwtAccessDeniedHandler) {
this.jwtTokenProvider = jwtTokenProvider;
this.corsFilter = corsFilter;
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.httpBasic(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable) // token์ ์ฌ์ฉํ๋ฏ๋ก csrf ๋ณด์์ด ํ์์์ผ๋ฏ๋ก disable ์ฒ๋ฆฌ
.formLogin(AbstractHttpConfigurer::disable)
.addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(config -> config
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
)
.headers(header -> header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.authorizeHttpRequests(
registry -> registry.requestMatchers("/", "v1/join/**", "v1/login/**", "v1/reissue", "/profile", "/css/**","/images/**","/js/**","/h2-console/**","/favicon.ico")
.permitAll()
.anyRequest()
.authenticated()
)
// ์ธ์
์ ์ฌ์ฉํ์ง ์๊ธฐ ๋๋ฌธ์ STATELESS๋ก ์ค์
.sessionManagement(sessionManagement ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// JwtFilter๋ฅผ addFilterBefore๋ก ๋ฑ๋กํ๋ JwtSecurityConfig ํด๋์ค๋ฅผ ์ ์ฉ
.with(new JwtSecurityConfig(jwtTokenProvider), customizer -> {});
return http.build();
}
}
SecurityUtil
import com.study.footprint.common.exception.CommonBadRequestException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
@Slf4j
public class SecurityUtil {
private SecurityUtil() { }
// SecurityContext ์ ์ ์ ์ ๋ณด๊ฐ ์ ์ฅ๋๋ ์์
// Request ๊ฐ ๋ค์ด์ฌ ๋ JwtFilter ์ doFilter ์์ ์ ์ฅ
public static Long getCurrentMemberId() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication.getName() == null) {
throw new CommonBadRequestException("authenticationInfoNotFound");
}
return Long.parseLong(authentication.getName());
}
}
- JwtFilter์์ ์ธํ ํ ์ ์ ์ ๋ณด๋ฅผ ๊บผ๋ธ๋ค.
- MemberId๋ฅผ ์ ์ฅํ๊ธฐ ๋๋ฌธ์ Long ํ์ ์ผ๋ก ๋ฐํํ๋ค.
- SecurityContext๋ ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ์ ์ฅํ๋ค.
CustomUserDetailsService
import com.study.footprint.common.exception.CommonNotFoundException;
import com.study.footprint.domain.member.Member;
import com.study.footprint.domain.member.MemberRepository;
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.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
@Transactional(readOnly = true)
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final MemberRepository memberRepository;
public CustomUserDetailsService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return memberRepository.findByEmail(username)
.map(this::createUserDetails)
.orElseThrow(() -> new CommonNotFoundException("userNotFound"));
}
// DB ์ User ๊ฐ์ด ์กด์ฌํ๋ค๋ฉด UserDetails ๊ฐ์ฒด๋ก ๋ง๋ค์ด์ ๋ฆฌํด
private UserDetails createUserDetails(Member member) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(member.getRole().toString());
return new User(
String.valueOf(member.getId()),
member.getPassword(),
Collections.singleton(grantedAuthority)
);
}
}
- loadUserByUsername ๋ฉ์๋๋ฅผ ์ค๋ฒ๋ผ์ด๋ํ๋๋ฐ ์ฌ๊ธฐ์ ๋๊ฒจ๋ฐ์ UserDetails๋ Authentication์ ํจ์ค์๋๋ฅผ ๋น๊ตํ๊ณ ๊ฒ์ฆํ๋ค.
- DB์ User๊ฐ์ด ์กด์ฌํ๋ฉด UserDetails ๊ฐ์ฒด๋ฅผ ๋ง๋ค์ด์ ๋ฆฌํดํ๋ค.
AuthService
import com.study.footprint.common.exception.CommonBadRequestException;
import com.study.footprint.common.response.SingleResult;
import com.study.footprint.config.auth.JwtTokenProvider;
import com.study.footprint.dto.member.request.LoginReqDto;
import com.study.footprint.dto.member.request.TokenReqDto;
import com.study.footprint.dto.member.response.LoginResDto;
import com.study.footprint.dto.member.response.TokenResDto;
import com.study.footprint.service.common.ResponseService;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Transactional(readOnly = true)
@Service
public class AuthService {
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final ResponseService responseService;
private final JwtTokenProvider jwtTokenProvider;
private final RedisTemplate<String, String> redisTemplate;
public AuthService(AuthenticationManagerBuilder authenticationManagerBuilder, ResponseService responseService,
JwtTokenProvider jwtTokenProvider, RedisTemplate<String, String> redisTemplate) {
this.authenticationManagerBuilder = authenticationManagerBuilder;
this.responseService = responseService;
this.jwtTokenProvider = jwtTokenProvider;
this.redisTemplate = redisTemplate;
}
@Transactional
public SingleResult<LoginResDto> login(LoginReqDto loginReqDto) {
// 1. Login ID/PW ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก AuthenticationToken ์์ฑ
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginReqDto.getEmail(), loginReqDto.getPassword());
// 2. ์ค์ ๋ก ๊ฒ์ฆ (์ฌ์ฉ์ ๋น๋ฐ๋ฒํธ ์ฒดํฌ) ์ด ์ด๋ฃจ์ด์ง๋ ๋ถ๋ถ
// authenticate ๋ฉ์๋๊ฐ ์คํ์ด ๋ ๋ CustomUserDetailsService ์์ ๋ง๋ค์๋ loadUserByUsername ๋ฉ์๋๊ฐ ์คํ๋จ
Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
// 3. ์ธ์ฆ ์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก JWT ํ ํฐ ์์ฑ
LoginResDto loginResDto = LoginResDto.builder()
.accessToken(jwtTokenProvider.createAccessToken(authentication))
.refreshToken(jwtTokenProvider.createRefreshToken(authentication))
.build();
return responseService.getSingleResult(loginResDto);
}
@Transactional
public SingleResult<TokenResDto> reissue(TokenReqDto tokenReqDto) {
// Refresh Token ๊ฒ์ฆ
jwtTokenProvider.validateToken(tokenReqDto.getRefreshToken());
// Access Token ์์ Member ID ๊ฐ์ ธ์ค๊ธฐ
Authentication authentication = jwtTokenProvider.getAuthentication(tokenReqDto.getAccessToken());
// ์ ์ฅ์์์ Member ID ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก Refresh Token ๊ฐ ๊ฐ์ ธ์ด
String refreshToken = redisTemplate.opsForValue().get(authentication.getName());
// Refresh Token ์ผ์นํ๋์ง ๊ฒ์ฌ
if (!refreshToken.equals(tokenReqDto.getRefreshToken())) {
throw new CommonBadRequestException("invalidRefreshToken");
}
// ์๋ก์ด ํ ํฐ ์์ฑ
TokenResDto tokenResDto = TokenResDto.builder()
.accessToken(jwtTokenProvider.createAccessToken(authentication))
.refreshToken(jwtTokenProvider.createRefreshToken(authentication))
.build();
return responseService.getSingleResult(tokenResDto);
}
}
๊ฒฐ๊ณผํ๋ฉด
reference
https://9keyyyy.tistory.com/48
https://kimtaesoo99.tistory.com/118
'Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Spring] @PrePersist์ @PreUpdate (0) | 2024.03.16 |
---|---|
[Spring] N+1 ๋ฌธ์ ์ fetch join ํด๊ฒฐ ๋ฐ ํ ์คํธ (0) | 2024.03.15 |
[Spring] @AllArgsConstructor, @RequiredArgsConstructor ์ฌ์ฉ์ ์ง์ํ ์ด์ (0) | 2024.03.05 |
[Spring] @ExceptionHandler๋ฅผ ํตํ ์์ธ ์ฒ๋ฆฌ (+ DTO Validation) (0) | 2024.03.05 |
[Spring] MessageSource๋ฅผ ์ด์ฉํ ๊ตญ์ ํ exception ์ ์ฉํ๊ธฐ (0) | 2024.03.02 |