Skip to content

Commit

Permalink
Merge pull request #20 from Team-Motivoo/refactor/#2-social_login_kako
Browse files Browse the repository at this point in the history
[REFACTOR] 카카오 로그인 피드백 반영
  • Loading branch information
hyeyeonnnnn authored Jan 9, 2024
2 parents 3544218 + 926dd3d commit d956242
Show file tree
Hide file tree
Showing 33 changed files with 834 additions and 42 deletions.
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);
}
}
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);
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();
}

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

0 comments on commit d956242

Please sign in to comment.