[Spring Boot] Spring Security, JWT ํ† ํฐ ์ธ์ฆ

2024. 11. 4. 16:40ใ†Spring/[2024] Spring Boot

728x90

 

์กฐ๊ธˆ์˜ ์‹œ๊ฐ„์ด ๋‚จ์•„ ํ•ญ์ƒ ๋ฏธ๋ค„์™”๋˜

์Šคํ”„๋ง ๋ถ€ํŠธ์—์„œ 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 ์˜ค๋ฅ˜ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. 

 

 

๐Ÿ‘ฉ‍๐Ÿ’ป ์˜ค๋Š˜์€ ์šฐ์„  ์—ฌ๊ธฐ๊นŒ์ง€ ๊ตฌํ˜„ํ•˜๊ณ , ๋‹ค์Œ์—” ๋กœ๊ทธ์ธ ๋ฐ ํšŒ์› ๊ฐ€์ž…์„ ๋งŒ๋“ค ์˜ˆ์ •์ด๋‹ค. 

 

 

 

 

 

 

 

 

 

728x90