-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
[FEAT/#49] 스프링 시큐리티를 통한 임시 인증 인가 처리 로직 추가
- Loading branch information
Showing
14 changed files
with
443 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
17 changes: 17 additions & 0 deletions
17
src/main/java/com/acon/server/global/auth/MemberAuthentication.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package com.acon.server.global.auth; | ||
|
||
import java.util.Collection; | ||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
import org.springframework.security.core.GrantedAuthority; | ||
|
||
public class MemberAuthentication extends UsernamePasswordAuthenticationToken { | ||
|
||
// 사용자 인증 객체 생성 | ||
public MemberAuthentication( | ||
Object principal, | ||
Object credentials, | ||
Collection<? extends GrantedAuthority> authorities | ||
) { | ||
super(principal, credentials, authorities); | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
src/main/java/com/acon/server/global/auth/PrincipalHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package com.acon.server.global.auth; | ||
|
||
import com.acon.server.global.exception.BusinessException; | ||
import com.menugraphy.server.global.exception.ErrorType; | ||
import org.springframework.security.core.context.SecurityContextHolder; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class PrincipalHandler { | ||
|
||
private static final String ANONYMOUS_USER = "anonymousUser"; | ||
|
||
public Long getUserIdFromPrincipal() { | ||
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); | ||
isPrincipalNull(principal); | ||
return Long.valueOf(principal.toString()); | ||
} | ||
|
||
public void isPrincipalNull( | ||
final Object principal | ||
) { | ||
if (principal.toString().equals(ANONYMOUS_USER)) { | ||
throw new BusinessException(ErrorType.EMPTY_PRINCIPAL_ERROR); | ||
} | ||
} | ||
} |
25 changes: 25 additions & 0 deletions
25
src/main/java/com/acon/server/global/auth/filter/CustomAccessDeniedHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package com.acon.server.global.auth.filter; | ||
|
||
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; | ||
|
||
@Component | ||
public class CustomAccessDeniedHandler implements AccessDeniedHandler { | ||
// 사용자가 인증은 되었지만 특정 리소스에 접근할 권한이 없을 때 호출 | ||
|
||
@Override | ||
public void handle( | ||
HttpServletRequest request, | ||
HttpServletResponse response, | ||
AccessDeniedException accessDeniedException | ||
) { | ||
setResponse(response); | ||
} | ||
|
||
private void setResponse(HttpServletResponse response) { | ||
response.setStatus(HttpServletResponse.SC_FORBIDDEN); | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
src/main/java/com/acon/server/global/auth/filter/CustomJwtAuthenticationEntryPoint.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package com.acon.server.global.auth.filter; | ||
|
||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.security.core.AuthenticationException; | ||
import org.springframework.security.web.AuthenticationEntryPoint; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint { | ||
// 사용자가 인증되지 않은 상태에서 보호된 리소스에 접근하려고 할 때 호출 | ||
|
||
@Override | ||
public void commence( | ||
HttpServletRequest request, | ||
HttpServletResponse response, | ||
AuthenticationException authException | ||
) { | ||
setResponse(response); | ||
} | ||
|
||
private void setResponse(HttpServletResponse response) { | ||
response.setStatus(HttpServletResponse.SC_FORBIDDEN); | ||
} | ||
} |
71 changes: 71 additions & 0 deletions
71
src/main/java/com/acon/server/global/auth/filter/JwtAuthenticationFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
package com.acon.server.global.auth.filter; | ||
|
||
import static com.acon.server.global.auth.jwt.JwtValidationType.VALID_JWT; | ||
|
||
import com.acon.server.global.auth.MemberAuthentication; | ||
import com.acon.server.global.auth.jwt.JwtTokenProvider; | ||
import com.acon.server.global.exception.BusinessException; | ||
import com.acon.server.global.exception.ErrorType; | ||
import jakarta.servlet.FilterChain; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import java.io.IOException; | ||
import lombok.NonNull; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.security.core.context.SecurityContextHolder; | ||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.util.StringUtils; | ||
import org.springframework.web.filter.OncePerRequestFilter; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
@Slf4j | ||
public class JwtAuthenticationFilter extends OncePerRequestFilter { | ||
|
||
private final JwtTokenProvider jwtTokenProvider; | ||
|
||
// 각 HTTP 요청에 대해 토큰이 유효한지 확인하고, 유효하다면 해당 사용자를 인증 설정하는 필터링 로직 | ||
@Override | ||
protected void doFilterInternal(@NonNull HttpServletRequest request, | ||
@NonNull HttpServletResponse response, | ||
@NonNull FilterChain filterChain) throws ServletException, IOException { | ||
try { | ||
final String token = getJwtFromRequest(request); | ||
|
||
if (jwtTokenProvider.validateToken(token) == VALID_JWT) { | ||
Long memberId = jwtTokenProvider.getMemberIdFromJwt(token); | ||
|
||
// authentication 객체 생성 -> principal에 유저정보를 담는다. | ||
MemberAuthentication authentication = new MemberAuthentication(memberId.toString(), null, null); | ||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); | ||
SecurityContextHolder.getContext().setAuthentication(authentication); | ||
} | ||
} catch (Exception exception) { | ||
// SecurityConfig에서 permitAll을 적용해도, Spring Security의 필터 체인을 거치므로 | ||
// 여기서 바로 Exception throw를 하게 되면 permitAll과 상관 없이 ExceptionTranslationFilter로 처리가 넘어간다. | ||
// 따라서 예외를 직접 throw로 던져주는 것이 아닌, 발생시키기만 하고 다음 필터 호출로 이어지게끔 해야 하고, (doFilter) | ||
// 이렇게 하면 API의 permitAll 적용 여부에 따라 ExceptionTranslationFilter를 거칠지 판단하게 된다. | ||
log.error("JwtAuthentication Authentication Exception Occurs! - {}", exception.getMessage()); | ||
} | ||
// 다음 필터로 요청 전달 (호출) | ||
filterChain.doFilter(request, response); | ||
} | ||
|
||
// Authorization 헤더에서 JWT 토큰을 추출 | ||
private String getJwtFromRequest(HttpServletRequest request) { | ||
String bearerToken = request.getHeader("Authorization"); | ||
|
||
if (!StringUtils.hasText(bearerToken)) { | ||
throw new BusinessException(ErrorType.UN_LOGIN_ERROR); | ||
} else if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { | ||
return bearerToken.substring("Bearer ".length()); | ||
} else if (StringUtils.hasText(bearerToken) && !bearerToken.startsWith("Bearer ")) { | ||
throw new BusinessException(ErrorType.BEARER_LOST_ERROR); | ||
} | ||
|
||
return null; | ||
} | ||
} |
115 changes: 115 additions & 0 deletions
115
src/main/java/com/acon/server/global/auth/jwt/JwtTokenProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
package com.acon.server.global.auth.jwt; | ||
|
||
import io.jsonwebtoken.Claims; | ||
import io.jsonwebtoken.ExpiredJwtException; | ||
import io.jsonwebtoken.Header; | ||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.MalformedJwtException; | ||
import io.jsonwebtoken.UnsupportedJwtException; | ||
import io.jsonwebtoken.security.Keys; | ||
import jakarta.annotation.PostConstruct; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.Base64; | ||
import java.util.Date; | ||
import java.util.List; | ||
import javax.crypto.SecretKey; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.security.core.Authentication; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class JwtTokenProvider { | ||
|
||
private static final String MEMBER_ID = "memberId"; | ||
|
||
@Value("${jwt.access-token-expire-time}") | ||
private long ACCESS_TOKEN_EXPIRATION_TIME; | ||
|
||
@Value("${jwt.refresh-token-expire-time}") | ||
private long REFRESH_TOKEN_EXPIRATION_TIME; | ||
|
||
@Value("${jwt.secret}") | ||
private String JWT_SECRET; | ||
|
||
@PostConstruct | ||
protected void init() { | ||
//base64 라이브러리에서 encodeToString을 이용해서 byte[] 형식을 String 형식으로 변환 | ||
JWT_SECRET = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes(StandardCharsets.UTF_8)); | ||
} | ||
|
||
public List<String> issueToken(Long memberId) { | ||
return List.of(issueAccessToken(memberId), issueRefreshToken(memberId)); | ||
} | ||
|
||
public String issueAccessToken(final Authentication authentication) { | ||
return generateToken(authentication, ACCESS_TOKEN_EXPIRATION_TIME); | ||
} | ||
|
||
public String issueRefreshToken() { | ||
final Date now = new Date(); | ||
|
||
return Jwts.builder() | ||
.setHeaderParam(Header.TYPE, Header.JWT_TYPE) | ||
.setIssuedAt(now) | ||
.setExpiration(new Date(now.getTime() + REFRESH_TOKEN_EXPIRATION_TIME)) | ||
.signWith(getSigningKey()) | ||
.compact(); | ||
} | ||
|
||
public String generateToken( | ||
Authentication authentication, | ||
Long tokenExpirationTime | ||
) { | ||
final Date now = new Date(); | ||
|
||
final Claims claims = Jwts.claims() | ||
.setIssuedAt(now) | ||
.setExpiration(new Date(now.getTime() + tokenExpirationTime)); // 만료 시간 설정 | ||
|
||
claims.put(MEMBER_ID, authentication.getPrincipal()); | ||
|
||
return Jwts.builder() | ||
.setHeaderParam(Header.TYPE, Header.JWT_TYPE) // Header | ||
.setClaims(claims) // Claim | ||
.signWith(getSigningKey()) // Signature | ||
.compact(); | ||
} | ||
|
||
private SecretKey getSigningKey() { | ||
String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes()); // SecretKey를 통해 서명 생성 | ||
|
||
// 일반적으로 HMAC (Hash-based Message Authentication Code) 알고리즘을 사용 | ||
return Keys.hmacShaKeyFor(encodedKey.getBytes()); | ||
} | ||
|
||
public JwtValidationType validateToken(String token) { | ||
try { | ||
final Claims claims = getBody(token); | ||
return JwtValidationType.VALID_JWT; | ||
} catch (MalformedJwtException ex) { | ||
return JwtValidationType.INVALID_JWT_TOKEN; | ||
} catch (ExpiredJwtException ex) { | ||
return JwtValidationType.EXPIRED_JWT_TOKEN; | ||
} catch (UnsupportedJwtException ex) { | ||
return JwtValidationType.UNSUPPORTED_JWT_TOKEN; | ||
} catch (IllegalArgumentException ex) { | ||
return JwtValidationType.EMPTY_JWT; | ||
} | ||
} | ||
|
||
private Claims getBody(final String token) { | ||
return Jwts.parserBuilder() | ||
.setSigningKey(getSigningKey()) | ||
.build() | ||
.parseClaimsJws(token) | ||
.getBody(); | ||
} | ||
|
||
public Long getMemberIdFromJwt(String token) { | ||
Claims claims = getBody(token); | ||
|
||
return Long.valueOf(claims.get(MEMBER_ID).toString()); | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
src/main/java/com/acon/server/global/auth/jwt/JwtValidationType.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package com.acon.server.global.auth.jwt; | ||
|
||
public enum JwtValidationType { | ||
VALID_JWT, // 유효한 JWT | ||
INVALID_JWT_SIGNATURE, // 유효하지 않은 서명 | ||
INVALID_JWT_TOKEN, // 유효하지 않은 토큰 | ||
EXPIRED_JWT_TOKEN, // 만료된 토큰 | ||
UNSUPPORTED_JWT_TOKEN, // 지원하지 않는 형식의 토큰 | ||
EMPTY_JWT // 빈 JWT | ||
} |
51 changes: 51 additions & 0 deletions
51
src/main/java/com/acon/server/global/auth/security/SecurityConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package com.acon.server.global.auth.security; | ||
|
||
import com.acon.server.global.auth.filter.CustomAccessDeniedHandler; | ||
import com.acon.server.global.auth.filter.CustomJwtAuthenticationEntryPoint; | ||
import com.acon.server.global.auth.filter.JwtAuthenticationFilter; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
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.web.SecurityFilterChain; | ||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | ||
|
||
@Configuration | ||
@RequiredArgsConstructor | ||
@EnableWebSecurity // WebSecurity를 사용할 수 있게 | ||
public class SecurityConfig { | ||
|
||
private final JwtAuthenticationFilter jwtAuthenticationFilter; | ||
private final CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint; | ||
private final CustomAccessDeniedHandler customAccessDeniedHandler; | ||
|
||
private static final String[] AUTH_WHITE_LIST = { | ||
"/api/v1/**", | ||
"/api/v1/auth/**" | ||
}; | ||
|
||
@Bean | ||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { | ||
http.csrf(AbstractHttpConfigurer::disable) | ||
.formLogin(AbstractHttpConfigurer::disable) | ||
.httpBasic(AbstractHttpConfigurer::disable) | ||
.sessionManagement(session -> { | ||
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS); | ||
}) | ||
.exceptionHandling(exception -> { | ||
exception.authenticationEntryPoint(customJwtAuthenticationEntryPoint); | ||
exception.accessDeniedHandler(customAccessDeniedHandler); | ||
}); | ||
|
||
http.authorizeHttpRequests(auth -> { | ||
auth.requestMatchers(AUTH_WHITE_LIST).permitAll(); | ||
auth.anyRequest().authenticated(); | ||
}) | ||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); | ||
|
||
return http.build(); | ||
} | ||
} |
Oops, something went wrong.