-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
- Loading branch information
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package org.sopt.jaksim.auth; | ||
|
||
import org.sopt.jaksim.global.exception.UnauthorizedException; | ||
import org.sopt.jaksim.global.message.ErrorMessage; | ||
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 UnauthorizedException(ErrorMessage.JWT_UNAUTHORIZED_EXCEPTION); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package org.sopt.jaksim.auth; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.sopt.jaksim.auth.filter.CustomAccessDeniedHandler; | ||
import org.sopt.jaksim.auth.filter.CustomJwtAuthenticationEntryPoint; | ||
import org.sopt.jaksim.auth.filter.JwtAuthenticationFilter; | ||
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.annotation.web.configurers.RequestCacheConfigurer; | ||
import org.springframework.security.web.SecurityFilterChain; | ||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | ||
|
||
@Configuration | ||
@RequiredArgsConstructor | ||
@EnableWebSecurity //web Security를 사용할 수 있게 | ||
public class SecurityConfig { | ||
private final JwtAuthenticationFilter jwtAuthenticationFilter; | ||
private final CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint; | ||
private final CustomAccessDeniedHandler customAccessDeniedHandler; | ||
|
||
|
||
private static final String[] AUTH_WHITE_LIST = {"/api/v1/member"}; | ||
|
||
@Bean | ||
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { | ||
http.csrf(AbstractHttpConfigurer::disable) | ||
.formLogin(AbstractHttpConfigurer::disable) | ||
.requestCache(RequestCacheConfigurer::disable) | ||
.httpBasic(AbstractHttpConfigurer::disable) | ||
.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(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package org.sopt.jaksim.auth; | ||
|
||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
import org.springframework.security.core.GrantedAuthority; | ||
|
||
import java.util.Collection; | ||
|
||
public class UserAuthentication extends UsernamePasswordAuthenticationToken { | ||
|
||
public UserAuthentication(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { | ||
super(principal, credentials, authorities); | ||
} | ||
|
||
public static UserAuthentication createUserAuthentication(Long userId) { | ||
return new UserAuthentication(userId, null, null); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package org.sopt.jaksim.auth.filter; | ||
|
||
|
||
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 CustomAccessDeniedHandler implements AccessDeniedHandler { | ||
@Override | ||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { | ||
setResponse(response); | ||
} | ||
|
||
private void setResponse(HttpServletResponse response) { | ||
response.setStatus(HttpServletResponse.SC_FORBIDDEN); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package org.sopt.jaksim.auth.filter; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import lombok.RequiredArgsConstructor; | ||
import org.sopt.jaksim.global.message.ErrorMessage; | ||
import org.springframework.http.MediaType; | ||
import org.springframework.security.core.AuthenticationException; | ||
import org.springframework.security.web.AuthenticationEntryPoint; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.io.IOException; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint { | ||
|
||
private final ObjectMapper objectMapper; | ||
|
||
@Override | ||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { | ||
setResponse(response); | ||
} | ||
|
||
private void setResponse(HttpServletResponse response) throws IOException { | ||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); | ||
response.setContentType(MediaType.APPLICATION_JSON_VALUE); | ||
response.setCharacterEncoding("UTF-8"); | ||
response.getWriter() | ||
.write(objectMapper.writeValueAsString( | ||
ErrorMessage.JWT_UNAUTHORIZED_EXCEPTION.getMessage())); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package org.sopt.jaksim.auth.filter; | ||
|
||
import jakarta.servlet.FilterChain; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import lombok.NonNull; | ||
import lombok.RequiredArgsConstructor; | ||
import org.sopt.jaksim.auth.UserAuthentication; | ||
import org.sopt.jaksim.global.common.jwt.JwtTokenProvider; | ||
import org.sopt.jaksim.global.exception.UnauthorizedException; | ||
import org.sopt.jaksim.global.message.ErrorMessage; | ||
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; | ||
|
||
import java.io.IOException; | ||
|
||
import static org.sopt.jaksim.global.common.jwt.JwtValidationType.VALID_JWT; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class JwtAuthenticationFilter extends OncePerRequestFilter { | ||
|
||
private final JwtTokenProvider jwtTokenProvider; | ||
|
||
@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.getUserFromJwt(token); | ||
UserAuthentication authentication = UserAuthentication.createUserAuthentication(memberId); | ||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); | ||
SecurityContextHolder.getContext().setAuthentication(authentication); | ||
} | ||
} catch (Exception exception) { | ||
throw new UnauthorizedException(ErrorMessage.JWT_UNAUTHORIZED_EXCEPTION); | ||
} | ||
filterChain.doFilter(request, response); | ||
} | ||
|
||
private String getJwtFromRequest(HttpServletRequest request) { | ||
String bearerToken = request.getHeader("Authorization"); | ||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { | ||
return bearerToken.substring("Bearer ".length()); | ||
} | ||
return null; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package org.sopt.jaksim.global.common.jwt; | ||
|
||
import io.jsonwebtoken.*; | ||
import io.jsonwebtoken.security.Keys; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.security.core.Authentication; | ||
import org.springframework.stereotype.Component; | ||
|
||
import javax.crypto.SecretKey; | ||
import java.util.Base64; | ||
import java.util.Date; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class JwtTokenProvider { | ||
|
||
private static final String USER_ID = "userId"; | ||
|
||
private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14; | ||
private static final Long REFRESH_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14; | ||
|
||
@Value("${jwt.secret}") | ||
private String JWT_SECRET; | ||
|
||
|
||
public String issueAccessToken(final Authentication authentication) { | ||
return generateToken(authentication, ACCESS_TOKEN_EXPIRATION_TIME); | ||
} | ||
|
||
public String issueRefreshToken(final Authentication authentication) { | ||
return generateToken(authentication, REFRESH_TOKEN_EXPIRATION_TIME); | ||
} | ||
|
||
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(USER_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 통해 서명 생성 | ||
return Keys.hmacShaKeyFor(encodedKey.getBytes()); //일반적으로 HMAC (Hash-based Message Authentication Code) 알고리즘 사용 | ||
} | ||
|
||
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 getUserFromJwt(String token) { | ||
Claims claims = getBody(token); | ||
return Long.valueOf(claims.get(USER_ID).toString()); | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package org.sopt.jaksim.global.common.jwt; | ||
|
||
public enum JwtValidationType { | ||
VALID_JWT, // 유효한 JWT | ||
INVALID_JWT_SIGNATURE, // 유효하지 않은 서명 | ||
INVALID_JWT_TOKEN, // 유효하지 않은 토큰 | ||
EXPIRED_JWT_TOKEN, // 만료된 토큰 | ||
UNSUPPORTED_JWT_TOKEN, // 지원하지 않는 형식의 토큰 | ||
EMPTY_JWT // 빈 JWT | ||
} |