-
Notifications
You must be signed in to change notification settings - Fork 0
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] 카카오 로그인 피드백 반영 #20
Changes from all commits
c06f21b
23521cf
a4ba7ba
77f2e11
2979ee9
6e269f9
3f8cf7b
0ff9054
e0c79c8
bb390fa
f622620
36a5c62
ec91658
478659e
201ea82
f10b671
8431954
9f90963
494497b
167f6ea
2471a77
fb0b9bf
926dd3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
GET http://localhost:8080/api/health/v2 | ||
|
||
POST http://localhost:8080/oauth/login | ||
Content-Type: application/json | ||
|
||
{ | ||
"id": 999, | ||
"value": "content" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package sopt.org.motivooServer.domain.auth.config; | ||
|
||
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,19 @@ | ||
package sopt.org.motivooServer.domain.auth.config; | ||
|
||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import org.springframework.security.core.AuthenticationException; | ||
import org.springframework.security.web.AuthenticationEntryPoint; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
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_UNAUTHORIZED); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package sopt.org.motivooServer.domain.auth.config; | ||
|
||
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 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; | ||
|
||
import java.io.IOException; | ||
|
||
@Slf4j | ||
@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 { | ||
|
||
final String token = getJwtFromRequest(request); | ||
jwtTokenProvider.validateToken(token); | ||
try { | ||
Long memberId = Long.parseLong(jwtTokenProvider.getPayload(token)); | ||
// authentication 객체 생성 -> principal에 유저정보를 담는다. | ||
UserAuthentication authentication = new UserAuthentication(memberId.toString(), null, null); | ||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); | ||
SecurityContextHolder.getContext().setAuthentication(authentication); | ||
} catch (NumberFormatException e) { | ||
log.error("refresh token은 유저 아이디를 담고있지 않습니다."); | ||
} | ||
// 다음 필터로 요청 전달 | ||
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,111 @@ | ||
package sopt.org.motivooServer.domain.auth.config; | ||
|
||
import io.jsonwebtoken.*; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.security.oauth2.jwt.JwtException; | ||
import org.springframework.stereotype.Component; | ||
import sopt.org.motivooServer.domain.auth.dto.response.OauthTokenResponse; | ||
import sopt.org.motivooServer.domain.auth.repository.TokenRedisRepository; | ||
import sopt.org.motivooServer.domain.user.exception.UserException; | ||
|
||
import java.util.*; | ||
|
||
import static sopt.org.motivooServer.domain.user.exception.UserExceptionType.*; | ||
|
||
@Slf4j | ||
@Component | ||
public class JwtTokenProvider { | ||
private static final String BEARER_TYPE = "Bearer"; | ||
|
||
@Value("${jwt.access-token.expire-length}") | ||
private long accessTokenValidityInMilliseconds; | ||
|
||
@Value("${jwt.refresh-token.expire-length}") | ||
private long refreshTokenValidityInMilliseconds; | ||
|
||
@Value("${jwt.token.secret-key}") | ||
private String secretKey; | ||
|
||
private TokenRedisRepository tokenRedisRepository; | ||
|
||
public JwtTokenProvider(TokenRedisRepository tokenRedisRepository){ | ||
this.tokenRedisRepository = tokenRedisRepository; | ||
} | ||
|
||
public String createAccessToken(String payload) { | ||
return createToken(payload, accessTokenValidityInMilliseconds); | ||
} | ||
|
||
public String createRefreshToken() { | ||
byte[] array = new byte[7]; | ||
new Random().nextBytes(array); | ||
String generatedString = Base64.getEncoder().encodeToString(array); | ||
return createToken(generatedString, refreshTokenValidityInMilliseconds); | ||
} | ||
|
||
public String createToken(String payload, long expireLength) { | ||
Map<String, Object> claims = new HashMap<>(); | ||
claims.put("payload", payload); | ||
Comment on lines
+47
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. payload에 어떤 값을 넣고자 하셨는지 궁금합니다! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Access Token에 userId를 담으려 했고, Refresh Token은 랜덤값을 인코딩하여 전달하고자 하였습니다. |
||
Date now = new Date(); | ||
Date validity = new Date(now.getTime() + expireLength); | ||
return Jwts.builder() | ||
.setClaims(claims) | ||
.setIssuedAt(now) | ||
.setExpiration(validity) | ||
.signWith(SignatureAlgorithm.HS256,secretKey) | ||
.compact(); | ||
} | ||
Comment on lines
+36
to
+58
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P4Access Token과 Refresh Token을 발급받는 메서드가 동일해보이는데, 두 토큰의 구성을 같이 가져가는 이유가 있을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Access Token에만 userId를 담고 Refresh Token은 String generatedString = Base64.getEncoder().encodeToString(array); 으로 인코딩한 데이터를 담고 있습니다! |
||
|
||
public String getPayload(String token){ | ||
try { | ||
Claims claims = Jwts.parserBuilder() | ||
.setSigningKey(secretKey) | ||
.build() | ||
.parseClaimsJws(token) | ||
.getBody(); | ||
|
||
Object subject = claims.get("payload"); | ||
|
||
return String.valueOf(subject); | ||
|
||
} catch (ExpiredJwtException e) { | ||
return e.getClaims().getSubject(); | ||
} catch (JwtException e){ | ||
throw new RuntimeException(String.valueOf(JwtValidationType.INVALID_JWT_TOKEN)); | ||
} | ||
} | ||
|
||
public void validateToken(String token) { | ||
try { | ||
token = token.replaceAll("\\s+", ""); | ||
token = token.replace(BEARER_TYPE, ""); | ||
Jwts.parserBuilder() | ||
.setSigningKey(secretKey) | ||
.build() | ||
.parseClaimsJws(token); | ||
} catch (MalformedJwtException ex){ | ||
throw new UserException(TOKEN_NOT_FOUND); | ||
} catch (ExpiredJwtException ex) { | ||
throw new UserException(TOKEN_EXPIRED); | ||
} catch (UnsupportedJwtException ex) { | ||
throw new UserException(TOKEN_UNSUPPORTED); | ||
} catch (IllegalArgumentException ex) { | ||
throw new UserException(TOKEN_NOT_FOUND); | ||
} | ||
} | ||
|
||
|
||
public OauthTokenResponse reissue(Long userId, String refreshToken) { | ||
validateToken(refreshToken); | ||
|
||
String reissuedAccessToken = createAccessToken(String.valueOf(userId)); | ||
String reissuedRefreshToken = createRefreshToken(); | ||
OauthTokenResponse tokenResponse = new OauthTokenResponse(reissuedAccessToken, reissuedAccessToken); | ||
|
||
tokenRedisRepository.saveRefreshToken(reissuedRefreshToken, String.valueOf(userId)); | ||
tokenRedisRepository.deleteRefreshToken(refreshToken); | ||
|
||
return tokenResponse; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package sopt.org.motivooServer.domain.auth.config; | ||
|
||
public enum JwtValidationType { | ||
VALID_JWT, // 유효한 JWT | ||
INVALID_JWT_SIGNATURE, // 유효하지 않은 서명 | ||
INVALID_JWT_TOKEN, // 유효하지 않은 토큰 | ||
EXPIRED_JWT_TOKEN, // 만료된 토큰 | ||
UNSUPPORTED_JWT_TOKEN, // 지원하지 않는 형식의 토큰 | ||
EMPTY_JWT // 빈 JWT | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package sopt.org.motivooServer.domain.auth.config; | ||
|
||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; | ||
import org.springframework.boot.autoconfigure.security.oauth2.client.ClientsConfiguredCondition; | ||
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties; | ||
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesRegistrationAdapter; | ||
import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Conditional; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.security.oauth2.client.registration.ClientRegistration; | ||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; | ||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
@Configuration | ||
@EnableConfigurationProperties(OAuth2ClientProperties.class) | ||
@Conditional(ClientsConfiguredCondition.class) | ||
public class OAuth2ClientRegistrationRepositoryConfig { | ||
private final OAuth2ClientProperties properties; | ||
|
||
OAuth2ClientRegistrationRepositoryConfig(OAuth2ClientProperties properties) { | ||
this.properties = properties; | ||
} | ||
|
||
@Bean | ||
@ConditionalOnMissingBean(ClientRegistrationRepository.class) | ||
public InMemoryClientRegistrationRepository clientRegistrationRepository() { | ||
List<ClientRegistration> registrations = new ArrayList<>( | ||
OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(this.properties).values()); | ||
return new InMemoryClientRegistrationRepository(registrations); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package sopt.org.motivooServer.domain.auth.config; | ||
|
||
|
||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.data.redis.connection.RedisConnectionFactory; | ||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; | ||
import org.springframework.data.redis.core.RedisTemplate; | ||
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; | ||
|
||
@Configuration | ||
@EnableRedisRepositories | ||
public class RedisConfig { | ||
@Value("${data.redis.host}") | ||
private String redisHost; | ||
|
||
@Value("${data.redis.port}") | ||
private int redisPort; | ||
|
||
@Bean | ||
public RedisConnectionFactory redisConnectionFactory() { | ||
return new LettuceConnectionFactory(redisHost, redisPort); | ||
} | ||
|
||
@Bean | ||
public RedisTemplate<?, ?> redisTemplate() { | ||
RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>(); | ||
redisTemplate.setConnectionFactory(redisConnectionFactory()); | ||
return redisTemplate; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package sopt.org.motivooServer.domain.auth.config; | ||
|
||
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); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여기 메서드 분리하신 이유가 궁금합니다 !!