Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3d44031
refactor : Move file's directory
isyoudwn Nov 1, 2024
522328f
refactor : Refactor JwtConfig
isyoudwn Nov 1, 2024
110f022
refactor : Rename JwtResponse
isyoudwn Nov 1, 2024
2a5a279
refactor : Refactor JwtProvider
isyoudwn Nov 1, 2024
62b57de
refactor : Move JwtConfig's directory path
isyoudwn Nov 1, 2024
c902cd7
feat : Add refresh token model
isyoudwn Nov 1, 2024
fbee8cd
feat : Add refresh token repsitory
isyoudwn Nov 1, 2024
ea7968e
feat : Add Role enum
isyoudwn Nov 1, 2024
a810d0a
refactor : Delete userRole class
isyoudwn Nov 1, 2024
dc488dd
refactor : Add Accessor domain
isyoudwn Nov 1, 2024
d90a93f
refactor : Add Auth annotation
isyoudwn Nov 1, 2024
a9ee79f
refactor : Add AuthException class
isyoudwn Nov 1, 2024
b92dda3
refactor : Add TokenExtractor
isyoudwn Nov 1, 2024
cc4468e
refactor : Refactor TokenExtractor
isyoudwn Nov 1, 2024
dcc0fe0
feat : Add RefreshTokenExtractor
isyoudwn Nov 1, 2024
5ae66f2
feat : Add AuthenticationResolver
isyoudwn Nov 16, 2024
1197c69
feat : Add find user by email method
isyoudwn Nov 18, 2024
09874d9
feat : Add get user detail method
isyoudwn Nov 18, 2024
72eaf1f
feat : Add login method
isyoudwn Nov 18, 2024
2558555
feat : Add login controller
isyoudwn Nov 18, 2024
f13cc9e
refactor : Refactor auth domain
isyoudwn Nov 18, 2024
b6ee0b3
refactor : Refactor auth service layer
isyoudwn Nov 18, 2024
9d1bfaa
refactor : Refactor refresh token from cookie
isyoudwn Nov 19, 2024
6bb43a3
feat : Add reissueResponseTokens api
isyoudwn Nov 20, 2024
3eafa46
feat : Add Permission check AOP
isyoudwn Dec 1, 2024
7fdb53b
refactor : Delete test directory
isyoudwn Dec 1, 2024
ed6649b
Merge branch 'develop' into isyoudwn/auth
polyglot-k Dec 21, 2024
70d44f3
fix(user) : 오타 수정
polyglot-k Dec 21, 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
44 changes: 44 additions & 0 deletions src/main/java/com/tasksprints/auction/api/auth/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.tasksprints.auction.api.auth;

import static org.springframework.http.HttpHeaders.SET_COOKIE;

import com.tasksprints.auction.common.constant.ApiResponseMessages;
import com.tasksprints.auction.common.response.ApiResult;
import com.tasksprints.auction.domain.auth.dto.request.LoginRequest;
import com.tasksprints.auction.domain.auth.dto.response.AccessToken;
import com.tasksprints.auction.domain.auth.dto.response.ResponseTokens;
import com.tasksprints.auction.domain.auth.service.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequiredArgsConstructor
@RequestMapping("/api/v1/auth")
public class AuthController {
private final AuthService authService;

@PostMapping("/login")
public ResponseEntity<ApiResult<AccessToken>> login(@RequestBody LoginRequest.Login login) {
Long userId = authService.validateLogin(login.email(), login.password());
ResponseTokens responseTokens = authService.issueResponseTokens(userId);

return ResponseEntity.ok()
.header(SET_COOKIE, responseTokens.refreshToken().toString())
.body(ApiResult.success(ApiResponseMessages.LOGIN_SUCCESS, responseTokens.accessToken()));
}

@GetMapping("/reissue")
public ResponseEntity<ApiResult<AccessToken>> reissueTokens(@CookieValue("refresh-token") String refreshToken) {
ResponseTokens responseTokens = authService.reissueResponseTokens(refreshToken);

return ResponseEntity.ok()
.header(SET_COOKIE, responseTokens.refreshToken().toString())
.body(ApiResult.success(ApiResponseMessages.LOGIN_SUCCESS, responseTokens.accessToken()));
}
}
24 changes: 24 additions & 0 deletions src/main/java/com/tasksprints/auction/common/config/JwtConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.tasksprints.auction.common.config;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
@Getter
@RequiredArgsConstructor
public class JwtConfig {

@Value("${jwt.expire-ms}")
private final Long accessExpireMs;

@Value("${jwt.expire-ms}")
private final Long refreshExpireMs;

@Value("${jwt.issuer}")
private final String issuer;

@Value("${jwt.secret}")
private final String secretKey;
}
Comment thread
polyglot-k marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.tasksprints.auction.common.config;

import com.tasksprints.auction.common.resolver.AuthenticationResolver;
import com.tasksprints.auction.common.resolver.SearchConditionResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
Expand All @@ -12,14 +12,17 @@
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final SearchConditionResolver searchConditionResolver;
private final AuthenticationResolver authenticationResolver;

public WebConfig(SearchConditionResolver searchConditionResolver) {
public WebConfig(SearchConditionResolver searchConditionResolver, AuthenticationResolver authenticationResolver) {
this.searchConditionResolver = searchConditionResolver;
this.authenticationResolver = authenticationResolver;
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(searchConditionResolver);
resolvers.add(authenticationResolver);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ public class ApiResponseMessages {
public static final String REVIEWS_RETRIEVED = "Reviews successfully retrieved";
public static final String REVIEW_RETRIEVED = "Review successfully retrieved";

// AUTH
public static final String ACCESS_TOKEN_NOT_FOUND = "Access token Not found";
public static final String REFRESH_TOKEN_NOT_FOUND = "Refresh token not found";
public static final String LOGIN_SUCCESS = "Login Success";

// Additional messages can be defined as needed

// PAYMENT
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.tasksprints.auction.domain.auction.exception.AuctionEndedException;
import com.tasksprints.auction.domain.auction.exception.AuctionNotFoundException;
import com.tasksprints.auction.domain.auction.exception.InvalidAuctionTimeException;
import com.tasksprints.auction.domain.auth.exception.AuthException;
import com.tasksprints.auction.domain.bid.exception.BidNotFoundException;
import com.tasksprints.auction.domain.bid.exception.InvalidBidAmountException;
import com.tasksprints.auction.domain.payment.exception.InvalidSessionException;
Expand Down Expand Up @@ -89,4 +90,9 @@ public ResponseEntity<ApiResult<String>> handleIllegalStateException(IllegalStat
public ResponseEntity<ApiResult<String>> handleRuntimeException(RuntimeException ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ApiResult.failure(ex.getMessage()));
}

@ExceptionHandler(AuthException.class)
public ResponseEntity<ApiResult<String>> handleAuthException(AuthException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ApiResult.failure(ApiResponseMessages.USER_NOT_FOUND));
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/tasksprints/auction/common/jwt/Auth.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.tasksprints.auction.common.jwt;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
}

This file was deleted.

67 changes: 39 additions & 28 deletions src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

import static com.tasksprints.auction.common.util.TimeUtil.*;

import com.tasksprints.auction.common.jwt.dto.response.JwtResponse;
import com.tasksprints.auction.common.config.JwtConfig;
import com.tasksprints.auction.domain.auth.dto.response.AccessToken;
import com.tasksprints.auction.domain.auth.dto.response.UserTokens;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.time.Clock;
Expand All @@ -16,44 +20,51 @@
@Component
@RequiredArgsConstructor
public class JwtProvider {

private final JwtProperties jwtProperties;
private static final String EMPTY_SUBJECT = "";
private final JwtConfig jwtConfig;
private final Clock clock;

public JwtResponse generateToken(Long userId, String userRole) {
return JwtResponse.of(createAccessToken(userId, userRole), createRefreshToken());
}
public UserTokens generateToken(String subject) {
String accessTokenValue = createToken(subject, jwtConfig.getAccessExpireMs());
AccessToken accessToken = AccessToken.of(accessTokenValue);

public String createAccessToken(Long userId, String userRole) {
String refreshToken = createToken(EMPTY_SUBJECT, jwtConfig.getRefreshExpireMs());

Date now = localDateTimeToDate(LocalDateTime.now(clock));

return Jwts.builder().setIssuer(jwtProperties.getIssuer()).claim("userId", userId).claim("userRole", userRole)
.setIssuedAt(now).setExpiration(new Date(now.getTime() + jwtProperties.getExpireMs()))
.signWith(SignatureAlgorithm.HS256, JwtUtil.encodeSecretKey(jwtProperties.getSecretKey())).compact();
return UserTokens.of(
accessToken,
refreshToken
);
}

public String createRefreshToken() {

private String createToken(String subject, Long expiredMs) {
byte[] secretKey = JwtUtil.encodeSecretKey(jwtConfig.getSecretKey());
Date now = localDateTimeToDate(LocalDateTime.now(clock));

return Jwts.builder().setIssuer(jwtProperties.getIssuer()).setIssuedAt(now)
.setExpiration(new Date(now.getTime() + jwtProperties.getRefreshExpireMs()))
.signWith(SignatureAlgorithm.HS256, JwtUtil.encodeSecretKey(jwtProperties.getSecretKey())).compact();
Date expirationTime = new Date(now.getTime() + expiredMs);

return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setIssuer(jwtConfig.getIssuer())
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expirationTime)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}

public boolean verifyToken(String token) {

Date now = localDateTimeToDate(LocalDateTime.now(clock));

Claims claims = getClaims(token);
public void validateToken(String token) {
parseToken(token);
}

return !claims.getExpiration().before(now);
public String getSubject(String token) {
return parseToken(token)
.getBody()
.getSubject();
}

public Claims getClaims(String token) {
return Jwts.parser().setSigningKey(JwtUtil.encodeSecretKey(jwtProperties.getSecretKey()))
.parseClaimsJws(token)
.getBody();
private Jws<Claims> parseToken(String token) {
byte[] secretKey = JwtUtil.encodeSecretKey(jwtConfig.getSecretKey());
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token);
}
}
25 changes: 25 additions & 0 deletions src/main/java/com/tasksprints/auction/common/jwt/UserCheck.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.tasksprints.auction.common.jwt;

import com.tasksprints.auction.domain.auth.exception.AuthException;
import com.tasksprints.auction.domain.auth.model.Accessor;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class UserCheck {
@Before("@annotation(UserOnly)")
public void userCheck(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();

for (Object arg : args) {
if (arg instanceof Accessor accessor) {
if (!accessor.isUser()) {
throw new AuthException("Invalid Access");
}
}
}
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/tasksprints/auction/common/jwt/UserOnly.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.tasksprints.auction.common.jwt;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserOnly {
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.tasksprints.auction.common.resolver;

import com.tasksprints.auction.common.jwt.Auth;
import com.tasksprints.auction.common.jwt.JwtProvider;
import com.tasksprints.auction.domain.auth.TokenExtractor;
import com.tasksprints.auction.domain.auth.exception.RefreshTokenException;
import com.tasksprints.auction.domain.auth.model.Accessor;
import com.tasksprints.auction.domain.auth.service.RefreshTokenCookieManager;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

@Component
@RequiredArgsConstructor
public class AuthenticationResolver implements HandlerMethodArgumentResolver {
private final JwtProvider jwtProvider;
private final RefreshTokenCookieManager refreshTokenCookieManager;
private final TokenExtractor accessTokenExtractor;

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter
.hasParameterAnnotation(Auth.class);
}

@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);

if (request == null) {
throw new IllegalArgumentException();
}
try {
String refreshToken = refreshTokenCookieManager.extractRefreshToken(request);
String accessToken = accessTokenExtractor.extractToken(request);

jwtProvider.validateToken(accessToken);
jwtProvider.validateToken(refreshToken);

Long userId = Long.valueOf(jwtProvider.getSubject(refreshToken));
return Accessor.user(userId);
} catch (RefreshTokenException e) {
return Accessor.guest();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.tasksprints.auction.domain.auth;

import static com.tasksprints.auction.common.constant.ApiResponseMessages.ACCESS_TOKEN_NOT_FOUND;

import com.tasksprints.auction.domain.auth.exception.AccessTokenException;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
@Qualifier("accessTokenExtractor")
public class AccessTokenExtractor implements TokenExtractor {
private static final String TYPE = "Bearer ";
private static final String HEADER = "Authorization";

public String extractToken(HttpServletRequest request) {
String header = request.getHeader(HEADER);
if(header != null && header.startsWith(TYPE)) {
return header.substring(TYPE.length());
}
throw new AccessTokenException(ACCESS_TOKEN_NOT_FOUND);
}
}
Loading