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] 카카오 로그인 피드백 반영 #20

Merged
merged 23 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c06f21b
✨ feat: 카카오 회원가입/로그인 구현
hyeyeonnnnn Jan 5, 2024
23521cf
✨ feat: redis를 활용한 refreshtoken 재발급
hyeyeonnnnn Jan 6, 2024
a4ba7ba
✨ feat: 카카오 로그아웃
hyeyeonnnnn Jan 7, 2024
77f2e11
✨ feat: 카카오 로그아웃
hyeyeonnnnn Jan 7, 2024
2979ee9
🐛 bugfix : logout controller mapping 문제 수정
hyeyeonnnnn Jan 8, 2024
6e269f9
🐛 bugfix : 충돌 해결
hyeyeonnnnn Jan 8, 2024
3f8cf7b
bugfix : 충돌 해결
hyeyeonnnnn Jan 8, 2024
0ff9054
bugfix : 충돌 해결
hyeyeonnnnn Jan 8, 2024
e0c79c8
♻️ refactor: 코드리뷰 반영
hyeyeonnnnn Jan 9, 2024
bb390fa
♻️ refactor: 코드리뷰 반영
hyeyeonnnnn Jan 9, 2024
f622620
♻️ refactor: 코드리뷰 반영
hyeyeonnnnn Jan 9, 2024
36a5c62
♻️ refactor: 코드리뷰 반영
hyeyeonnnnn Jan 9, 2024
ec91658
♻️ refactor: 코드리뷰 반영
hyeyeonnnnn Jan 9, 2024
478659e
Merge branch 'develop' of https://github.com/Team-Motivoo/Motivoo-Ser…
jun02160 Jan 9, 2024
201ea82
🚑 hotfix: 테스트에 의한 빌드 오류 해결 #2
jun02160 Jan 9, 2024
f10b671
Merge remote-tracking branch 'origin/refactor/#2-social_login_kako' i…
jun02160 Jan 9, 2024
8431954
🔧 chore: HealthCheckController 필드 정리 #2
jun02160 Jan 9, 2024
9f90963
Merge pull request #22 from Team-Motivoo/feat/#2-social_login_build
jun02160 Jan 9, 2024
494497b
🔧 chore: 테스트 환경으로 지정
jun02160 Jan 9, 2024
167f6ea
Merge pull request #23 from Team-Motivoo/feat/#2-social_login_build
jun02160 Jan 9, 2024
2471a77
♻️ refactor: 코드리뷰 반영
hyeyeonnnnn Jan 9, 2024
fb0b9bf
Merge remote-tracking branch 'origin/refactor/#2-social_login_kako' i…
hyeyeonnnnn Jan 9, 2024
926dd3d
✅ test: 테스트 빌드 환경 변경
jun02160 Jan 9, 2024
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
17 changes: 15 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,20 @@ dependencies {
implementation("software.amazon.awssdk:bom:2.21.0")
implementation("software.amazon.awssdk:s3:2.21.0")

//OAuth2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-webflux'

//jwt
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'

//redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

testImplementation 'org.springframework.boot:spring-boot-starter-test'

}

tasks.named('test') {
Expand Down Expand Up @@ -129,5 +143,4 @@ openapi3 {
format = "json"
outputFileNamePrefix = "open-api-3.0.1"
outputDirectory = 'build/resources/main/static/docs'
}

}
9 changes: 9 additions & 0 deletions http/motivoo.http
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
Expand Up @@ -2,8 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;

@SpringBootApplication
@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
public class MotivooServerApplication {

public static void main(String[] args) {
Expand Down
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);
}
Comment on lines +12 to +18
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 메서드 분리하신 이유가 궁금합니다 !!

}
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

payload에 어떤 값을 넣고자 하셨는지 궁금합니다!

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P4

Access Token과 Refresh Token을 발급받는 메서드가 동일해보이는데, 두 토큰의 구성을 같이 가져가는 이유가 있을까요?
Refresh Token은 Access Token 만료 시에 재발급을 하기 위한 용도라 굳이 userId를 담지 않아도 될 것 같아요!!

Copy link
Member Author

Choose a reason for hiding this comment

The 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);
}
}
Loading