-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
## 🍀 작업한 내용에 대해 설명해주세요 <!-- 설명하고 싶은 코드가 있다면 첨부해주세요 --> - 진행중이라 마지막에 설명 쓰겠슴니다.... <br> ## 🍀 어떤 것을 중점으로 리뷰 해주길 바라시나요? - <br> ## 🍀 공통 작업 부분에 대한 수정 사항이 있다면 적어주세요 - <br> ## 🍀 PR 유형 어떤 변경 사항인가요? - [ ] 새로운 기능 추가 - [ ] 버그 수정 - [ ] 코드에 영향을 주지 않는 변경사항 (오타 수정, 탭 사이즈 변경, 변수명 변경) - [ ] 코드 리팩토링 - [ ] 주석 추가 및 수정 - [ ] 문서 수정 - [ ] 파일 혹은 패키지명 수정 - [ ] 파일 혹은 패키지 삭제 <br> ## 🍀 Checklist - [ ] 코드 컨벤션을 지켰나요? - [ ] git 컨벤션을 지켰나요? - [ ] PR 날리기 전에 검토하셨나요? <!-- 스스로 QA를 진행해봤는지 (기기 대응, 앱 터지지 않는지 등) --> - [ ] 코드리뷰를 반영했나요? ### 🍀 Issue <!-- 생성한 관련 이슈가 있다면 Resolved #이슈번호로 닫아주세요! --> Resolved #이슈번호
- 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 | ||
} |