Skip to content

Commit

Permalink
feat : redis 연동 및 로그인 (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
GaBaljaintheroom authored Jun 15, 2024
1 parent 1103652 commit 7c1ef11
Show file tree
Hide file tree
Showing 59 changed files with 490 additions and 82 deletions.
12 changes: 4 additions & 8 deletions app/api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ jar.enabled = true

allprojects {
dependencies {
implementation project(":app:domain:common-domain")

//swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'

implementation 'org.springframework.data:spring-data-commons:3.3.0'

implementation "org.springframework.boot:spring-boot-starter-web"
implementation 'org.springframework.boot:spring-boot-starter-validation'
testImplementation "org.springframework.boot:spring-boot-starter-test"
Expand All @@ -14,12 +18,4 @@ allprojects {

dependencies {
implementation project(":app:api:user-api")

// spring-security
implementation 'org.springframework.boot:spring-boot-starter-security'

// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
}
14 changes: 14 additions & 0 deletions app/api/common-api/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
bootJar.enabled = false
jar.enabled = true

dependencies {
implementation project(":app:domain:user-domain")

// spring-security
implementation 'org.springframework.boot:spring-boot-starter-security'

// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.example.config;

import org.example.property.TokenProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(TokenProperty.class)
@ComponentScan(basePackages = "org.example")
public class CommonApiConfig {

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@
import java.io.IOException;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.example.exception.BusinessException;
import org.example.repository.TokenRepository;
import org.example.security.dto.AuthenticatedUser;
import org.example.security.dto.TokenParam;
import org.example.security.dto.UserParam;
import org.example.security.token.JWTGenerator;
import org.example.security.token.JWTHandler;
import org.example.security.token.RefreshTokenProcessor;
import org.example.security.vo.TokenError;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
Expand All @@ -24,7 +28,9 @@
public class JWTFilter extends OncePerRequestFilter {

private final JWTHandler jwtHandler;
private final JWTGenerator jwtGenerator;
private final RefreshTokenProcessor refreshTokenProcessor;
private final TokenRepository tokenRepository;

@Override
protected void doFilterInternal(
Expand All @@ -33,7 +39,7 @@ protected void doFilterInternal(
FilterChain filterChain
) throws ServletException, IOException {
if (request.getHeader("Refresh") != null) {
TokenParam token = refreshTokenProcessor.process(request, response);
TokenParam token = refreshTokenProcessor.reissueToken(request);
response.getWriter().write(new ObjectMapper().writeValueAsString(token));
return;
}
Expand All @@ -48,9 +54,16 @@ protected void doFilterInternal(
private void handleAccessToken(HttpServletRequest request) {
String accessToken = jwtHandler.extractAccessToken(request);
UserParam userParam = jwtHandler.extractUserFrom(accessToken);
verifyLogoutAccessToken(userParam);
saveOnSecurityContextHolder(userParam);
}

public void verifyLogoutAccessToken(UserParam userParam) {
if (tokenRepository.existAccessToken(userParam.userId().toString())) {
throw new BusinessException(TokenError.INVALID_TOKEN);
}
}

private void saveOnSecurityContextHolder(UserParam userParam) {
AuthenticatedUser authenticatedUser = AuthenticatedUser.builder()
.userId(userParam.userId())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.example.repository;

import java.util.Optional;
import org.springframework.stereotype.Component;

@Component
public interface TokenRepository {

void save(String userId, String refreshToken);

Optional<String> getExistRefreshToken(String userId);

Boolean existAccessToken(String userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,29 @@
import io.jsonwebtoken.Jwts;
import java.util.Date;
import lombok.RequiredArgsConstructor;
import org.example.exception.BusinessException;
import org.example.property.TokenProperty;
import org.example.repository.TokenRepository;
import org.example.security.dto.TokenParam;
import org.example.security.dto.UserParam;
import org.example.security.vo.TokenError;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class JWTGenerator {

private final TokenProperty tokenProperty;
private final TokenRepository tokenRepository;

public TokenParam generate(UserParam userParam, Date from) {
return TokenParam.builder()
TokenParam tokenParam = TokenParam.builder()
.accessToken(createAccessToken(userParam, from))
.refreshToken(createRefreshToken(userParam, from))
.build();

tokenRepository.save(userParam.userId().toString(), tokenParam.refreshToken());
return tokenParam;
}

private String createAccessToken(UserParam userParam, Date from) {
Expand All @@ -42,4 +49,10 @@ private String createRefreshToken(UserParam userParam, Date from) {
.signWith(tokenProperty.getBase64URLSecretKey())
.compact();
}

public String getExistRefreshToken(UserParam userParam) {
return tokenRepository.getExistRefreshToken(userParam.userId().toString())
.orElseThrow(() -> new BusinessException(TokenError.WRONG_HEADER));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.example.security.token;

import jakarta.servlet.http.HttpServletRequest;
import java.util.Date;
import lombok.RequiredArgsConstructor;
import org.example.exception.BusinessException;
import org.example.security.dto.TokenParam;
import org.example.security.dto.UserParam;
import org.example.security.vo.TokenError;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class RefreshTokenProcessor {

private final JWTHandler jwtHandler;
private final JWTGenerator jwtGenerator;

public TokenParam reissueToken(HttpServletRequest request) {
String refreshToken = jwtHandler.extractRefreshToken(request);
UserParam userParam = jwtHandler.extractUserFrom(refreshToken);

String oldRefreshToken = jwtGenerator.getExistRefreshToken(userParam);
if (!refreshToken.equals(oldRefreshToken)) {
throw new BusinessException(TokenError.INVALID_TOKEN);
}

return jwtGenerator.generate(userParam, new Date());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ public enum UserRoleApiType {
UserRoleApiType(String authority) {
this.authority = authority;
}

public static UserRoleApiType from(UserRole userRole) {
return UserRoleApiType.valueOf(userRole.name());
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package org.example.security.token;

import static org.mockito.Mockito.mock;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import java.util.Date;
import java.util.UUID;
import org.example.property.TokenProperty;
import org.example.repository.TokenRepository;
import org.example.security.dto.TokenParam;
import org.example.security.dto.UserParam;
import org.example.vo.UserRoleApiType;
Expand All @@ -23,8 +26,8 @@ class JWTGeneratorTest {
hour,
twoWeeks
);

JWTGenerator tokenGenerator = new JWTGenerator(tokenProperty);
TokenRepository tokenRepository = mock(TokenRepository.class);
JWTGenerator tokenGenerator = new JWTGenerator(tokenProperty, tokenRepository);
UserParam userParam = new UserParam(
UUID.randomUUID(),
UserRoleApiType.USER
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;

import java.util.Date;
import java.util.UUID;
import org.example.exception.BusinessException;
import org.example.property.TokenProperty;
import org.example.repository.TokenRepository;
import org.example.security.dto.TokenParam;
import org.example.security.dto.UserParam;
import org.example.security.vo.TokenError;
Expand All @@ -22,8 +24,9 @@ class JWTHandlerTest {
3600000L,
1209600000L
);
TokenRepository tokenRepository = mock(TokenRepository.class);
JWTHandler jwtHandler = new JWTHandler(tokenProperty);
JWTGenerator jwtGenerator = new JWTGenerator(tokenProperty);
JWTGenerator jwtGenerator = new JWTGenerator(tokenProperty, tokenRepository);
UserParam userParam = new UserParam(
UUID.randomUUID(),
UserRoleApiType.USER
Expand Down
5 changes: 0 additions & 5 deletions app/api/src/main/java/org/example/config/ApiConfig.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
package org.example.config;

import org.example.property.TokenProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import(UserApiConfig.class)
@EnableConfigurationProperties(TokenProperty.class)
@ComponentScan(basePackages = "org.example")
public class ApiConfig {

}

This file was deleted.

3 changes: 2 additions & 1 deletion app/api/user-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ bootJar.enabled = false
jar.enabled = true

dependencies {
implementation project(":app:domain")
implementation project(":app:domain:user-domain")

implementation project(":app:api:common-api")
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package org.example.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.example.dto.request.SignUpRequest;
import org.example.entity.User;
import org.example.controller.dto.request.LoginApiRequest;
import org.example.security.dto.TokenParam;
import org.example.service.UserService;
import org.example.service.dto.request.LoginServiceRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -19,14 +21,14 @@ public class UserController {

private final UserService userService;

@PostMapping("/sign-up")
@Operation(summary = "유저 회원가입", description = "사용자는 회원가입을 할 수 있다.", tags = {"user"})
public ResponseEntity<String> signUp(@Valid @RequestBody SignUpRequest request) {
final User createdUser = request.toUser();
final User user = userService.signUp(createdUser);
final String nickName = userService.findNickname(user);
@PostMapping("/login")
@Tag(name = "user")
@Operation(summary = "유저 로그인", description = "사용자는 소셜 로그인을 할 수 있다.")
public ResponseEntity<TokenParam> signUp(@Valid @RequestBody LoginApiRequest request) {
LoginServiceRequest loginServiceRequest = request.toLoginServiceRequest();
TokenParam tokenParam = userService.login(loginServiceRequest);

return ResponseEntity.ok(nickName + "사용자 생성 성공!");
return ResponseEntity.ok(tokenParam);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.example.controller.dto.request;

import jakarta.validation.constraints.NotNull;
import org.example.entity.credential.AppleSocialCredential;
import org.example.entity.credential.GoogleSocialCredential;
import org.example.entity.credential.KakaoSocialCredential;
import org.example.entity.credential.SocialCredential;
import org.example.entity.credential.SocialCredentials;
import org.example.service.dto.request.LoginServiceRequest;
import org.example.vo.SocialLoginType;

public record LoginApiRequest(
@NotNull(message = "소셜 로그인 타입은 필수 입력값입니다.")
SocialLoginType socialLoginType,

@NotNull(message = "소셜 로그인 식별값은 필수 입력값입니다.")
String socialLoginIdentifier
) {

public LoginServiceRequest toLoginServiceRequest() {
return LoginServiceRequest.builder()
.socialCredentials(socialCredentials())
.build();
}

private SocialCredentials socialCredentials() {
SocialCredentials socialCredentials = new SocialCredentials();
socialCredentials.saveCredentials(socialLoginType, socialCredential());
return socialCredentials;
}

private SocialCredential socialCredential() {
return switch (socialLoginType) {
case GOOGLE -> new GoogleSocialCredential(socialLoginIdentifier);
case KAKAO -> new KakaoSocialCredential(socialLoginIdentifier);
case APPLE -> new AppleSocialCredential(socialLoginIdentifier);
};
}
}

This file was deleted.

Loading

0 comments on commit 7c1ef11

Please sign in to comment.