Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[REFACTOR] security code 개선 #93

Merged
merged 1 commit into from
Aug 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);

Optional<Object> findByIdAndRefreshToken(Long memberId, String refreshToken);

boolean existsByIdAndRefreshToken(Long id, String refreshToken);
}
51 changes: 31 additions & 20 deletions src/main/java/site/katchup/katchupserver/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpMethod;
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.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
Expand All @@ -23,10 +21,27 @@ public class SecurityConfig {
private final JwtTokenProvider jwtTokenProvider;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

private static final String[] SWAGGER_URL = {
"/swagger-resources/**",
"/favicon.ico",
"/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/swagger-ui/index.html",
"/docs/swagger-ui/index.html",
"/swagger-ui/swagger-ui.css",
};

private static final String[] AUTH_WHITELIST = {
"/api/v1/auth",
"/health"
};


@Bean
@Profile("local")
protected SecurityFilterChain localSecurityConfig(HttpSecurity http) throws Exception{
return http
@Profile("prod")
SecurityFilterChain prodSecurityFilterChain(HttpSecurity http) throws Exception{
http
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
Expand All @@ -37,15 +52,20 @@ protected SecurityFilterChain localSecurityConfig(HttpSecurity http) throws Exce
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.authorizeHttpRequests()
.anyRequest().permitAll()
.and().build();
.requestMatchers(AUTH_WHITELIST).permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, jwtAuthenticationEntryPoint), UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
@Profile("!local")
protected SecurityFilterChain prodSecurityConfig (HttpSecurity http) throws Exception{
@Profile("!prod")
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{
http
.csrf().disable()
.formLogin().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
Expand All @@ -55,21 +75,12 @@ protected SecurityFilterChain prodSecurityConfig (HttpSecurity http) throws Exce
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.authorizeHttpRequests()
.requestMatchers(SWAGGER_URL).permitAll()
.requestMatchers(AUTH_WHITELIST).permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, jwtAuthenticationEntryPoint), UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> {
web.ignoring()
.requestMatchers(
HttpMethod.POST,
"/api/v1/auth"
);
};
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package site.katchup.katchupserver.config.jwt;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package site.katchup.katchupserver.config.jwt;

import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
Expand All @@ -18,56 +19,61 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

private static final String ISSUE_TOKEN_API_URL = "/api/v1/auth/token";


@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String accessToken = jwtTokenProvider.resolveToken(request);

if (request.getRequestURI().equals("/api/v1/auth/token")) {
String refreshToken = jwtTokenProvider.resolveRefreshToken(request);
try {
String accessToken = jwtTokenProvider.resolveToken(request);
if (ISSUE_TOKEN_API_URL.equals(request.getRequestURI())) {
String refreshToken = jwtTokenProvider.resolveRefreshToken(request);

if (jwtTokenProvider.validateToken(refreshToken) == JwtExceptionType.EMPTY_JWT || jwtTokenProvider.validateToken(accessToken) == JwtExceptionType.EMPTY_JWT) {
jwtAuthenticationEntryPoint.setResponse(response, ErrorStatus.NO_TOKEN);
return;
} else if (jwtTokenProvider.validateToken(accessToken) == JwtExceptionType.EXPIRED_JWT_TOKEN) {
if (jwtTokenProvider.validateToken(refreshToken) == JwtExceptionType.EXPIRED_JWT_TOKEN) {
// access, refresh 둘 다 만료
jwtAuthenticationEntryPoint.setResponse(response, ErrorStatus.SIGNIN_REQUIRED);
if (jwtTokenProvider.validateToken(refreshToken) == JwtExceptionType.EMPTY_JWT || jwtTokenProvider.validateToken(accessToken) == JwtExceptionType.EMPTY_JWT) {
jwtAuthenticationEntryPoint.setResponse(response, ErrorStatus.NO_TOKEN);
return;
} else if (jwtTokenProvider.validateToken(refreshToken) == JwtExceptionType.VALID_JWT_TOKEN) {
// 토큰 재발급
Long memberId = jwtTokenProvider.validateMemberRefreshToken(accessToken, refreshToken);
Authentication authentication = new UserAuthentication(memberId, null, null);
} else if (jwtTokenProvider.validateToken(accessToken) == JwtExceptionType.EXPIRED_JWT_TOKEN) {
if (jwtTokenProvider.validateToken(refreshToken) == JwtExceptionType.EXPIRED_JWT_TOKEN) {
// access, refresh 둘 다 만료
jwtAuthenticationEntryPoint.setResponse(response, ErrorStatus.SIGNIN_REQUIRED);
return;
} else if (jwtTokenProvider.validateToken(refreshToken) == JwtExceptionType.VALID_JWT_TOKEN) {
// 토큰 재발급
Long memberId = jwtTokenProvider.validateMemberRefreshToken(accessToken, refreshToken);
Authentication authentication = new UserAuthentication(memberId, null, null);

String newAccessToken = jwtTokenProvider.generateAccessToken(authentication);
String newAccessToken = jwtTokenProvider.generateAccessToken(authentication);

setAuthentication(newAccessToken);
request.setAttribute("newAccessToken", newAccessToken);
setAuthentication(newAccessToken);
request.setAttribute("newAccessToken", newAccessToken);
}
} else if (jwtTokenProvider.validateToken(accessToken) == JwtExceptionType.VALID_JWT_TOKEN) {
jwtAuthenticationEntryPoint.setResponse(response, ErrorStatus.VALID_ACCESS_TOKEN);
return;
} else {
throw new CustomException(ErrorStatus.UNAUTHORIZED_TOKEN);
}
} else if (jwtTokenProvider.validateToken(accessToken) == JwtExceptionType.VALID_JWT_TOKEN) {
jwtAuthenticationEntryPoint.setResponse(response, ErrorStatus.VALID_ACCESS_TOKEN);
return;
} else {
throw new CustomException(ErrorStatus.UNAUTHORIZED_TOKEN);
}
}
else {
JwtExceptionType jwtException = jwtTokenProvider.validateToken(accessToken);
else {
JwtExceptionType jwtException = jwtTokenProvider.validateToken(accessToken);

if (accessToken != null) {
// 토큰 검증
if (jwtException == JwtExceptionType.VALID_JWT_TOKEN) {
setAuthentication(accessToken);
} else {
throw new CustomException(ErrorStatus.UNAUTHORIZED_TOKEN);
if (accessToken != null) {
// 토큰 검증
if (jwtException == JwtExceptionType.VALID_JWT_TOKEN) {
setAuthentication(accessToken);
}
}
}
} catch (Exception e) {
throw new CustomException(ErrorStatus.UNAUTHORIZED_TOKEN);
}

chain.doFilter(request, response);
}

private void setAuthentication(String jwtToken) {
Long userId = jwtTokenProvider.getAccessTokenPayload(jwtToken);
Authentication authentication = new UserAuthentication(userId, null, null);
private void setAuthentication(String token) {
Claims claims = jwtTokenProvider.getAccessTokenPayload(token);
Authentication authentication = new UserAuthentication(Long.valueOf(String.valueOf(claims.get("memberId"))), null, null);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package site.katchup.katchupserver.config.jwt;

import com.auth0.jwt.JWT;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
Expand Down Expand Up @@ -39,10 +38,15 @@ public String generateAccessToken(Authentication authentication) {
Date now = new Date();
Date expiration = new Date(now.getTime() + accessTokenExpireLength);

return Jwts.builder()
.setSubject(String.valueOf(authentication.getPrincipal()))
final Claims claims = Jwts.claims()
.setIssuedAt(now)
.setExpiration(expiration)
.setExpiration(expiration);

claims.put("memberId", authentication.getPrincipal());

return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setClaims(claims)
.signWith(getSignKey(), SignatureAlgorithm.HS256)
.compact();
}
Expand All @@ -51,16 +55,20 @@ public String generateRefreshToken() {
Date now = new Date();
Date expiration = new Date(now.getTime() + refreshTokenExpireLength);

return Jwts.builder()
final Claims claims = Jwts.claims()
.setIssuedAt(now)
.setExpiration(expiration)
.setExpiration(expiration);

return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setClaims(claims)
.signWith(getSignKey(), SignatureAlgorithm.HS256)
.compact();
}

// 회원 정보 추출
public Long getAccessTokenPayload(String token) {
return Long.valueOf(Jwts.parserBuilder().setSigningKey(getSignKey()).build().parseClaimsJws(token).getBody().getSubject());
public Claims getAccessTokenPayload(String token) {
return Jwts.parserBuilder().setSigningKey(getSignKey()).build().parseClaimsJws(token).getBody();
}

// Request Header에서 token 값 가져옴
Expand All @@ -87,19 +95,14 @@ public JwtExceptionType validateToken(String token) {
Jwts.parserBuilder().setSigningKey(getSignKey()).build().parseClaimsJws(token);
return JwtExceptionType.VALID_JWT_TOKEN;
} catch (io.jsonwebtoken.security.SignatureException exception) {
log.error("잘못된 JWT 서명을 가진 토큰입니다.");
return JwtExceptionType.INVALID_JWT_SIGNATURE;
} catch (MalformedJwtException exception) {
log.error("잘못된 JWT 토큰입니다.");
return JwtExceptionType.INVALID_JWT_TOKEN;
} catch (ExpiredJwtException exception) {
log.error("만료된 JWT 토큰입니다.");
return JwtExceptionType.EXPIRED_JWT_TOKEN;
} catch (UnsupportedJwtException exception) {
log.error("지원하지 않는 JWT 토큰입니다.");
return JwtExceptionType.UNSUPPORTED_JWT_TOKEN;
} catch (IllegalArgumentException exception) {
log.error("JWT Claims가 비어있습니다.");
return JwtExceptionType.EMPTY_JWT;
}
}
Expand All @@ -110,12 +113,11 @@ private Key getSignKey() {
}

public Long validateMemberRefreshToken(String accessToken, String refreshToken) {
Long memberId = Long.valueOf(JWT.decode(accessToken).getSubject());

memberRepository.findByIdAndRefreshToken(memberId, refreshToken)
.orElseThrow(() -> new CustomException(ErrorStatus.INVALID_MEMBER)
);

Claims claims = getAccessTokenPayload(accessToken);
Long memberId = Long.valueOf(String.valueOf(claims.get("memberId")));
if (!memberRepository.existsByIdAndRefreshToken(memberId, refreshToken)) {
throw new CustomException(ErrorStatus.INVALID_MEMBER);
}
return memberId;
}
}