Auth , Gateway 토큰 Secret Key Manager 등록
Front Payco Oauth2.0 부를 떄 사용하는 Client Id , Client Secret 등록
AOP 스펙 수정
메서드에 Request , Response 를 인자로 받지 않도록
(기존에는 aop적용 메서드에 인자로 request, response 필요했음)
또한 Annotation 이름을 Trace -> RequiredAuthorization 변경
my-books.store 인증/인가처리
로그인 요청 -> 로그인 정보 확인 -> 로그인 완료 -> 토큰 발행 -> Identity-Cookie 라는 이름의 쿠키에 토큰을 넣음
-> 서버는 사용자 인증과 관련된 정보를 갖고있지 않음
-> 사용자가 요청을 줌
1. 권한이 필요없다면? (장바구니,물건정보) 그대로 게이트웨이가 요청보냄
2. 권한이 필요하다면? (유저페이지,관리자페이지) Identity-Cookie 에 있는 토큰 값을 Authorization 헤더에 등록 후 요청보냄
게이트웨이에서 헤더에있는 토큰을 검증한 후 권한이있는지 인가처리함
에러 발생 시 HttpStatus Code
권한없는경우 403 , 인덱스 페이지로 이동시킴
토큰위조된경우 401 , 쿠키 삭제한 뒤 로그인페이지로 이동
토큰 만료된경우 401 , 리프래시토큰을 확인한 후 유효하면 토큰 재발급 후 요청 다시보내기 , 아니라면 로그인페이지로
유저가 활성상태가 아님 401 , 유저인증 페이지로 이동
에러메시지
TOKEN_EXPIRED("Token has expired"),
INVALID_TOKEN("Invalid token"),
INVALID_ACCESS("Access forbidden"),
INACTIVE_USER("User is inactive");
인가처리 완료시
X-User-Id 헤더에 유저정보를 담아 보냄
권한이 필요할시 AOP 를 적용해 위의 절차를 진행 함
GateWay
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("auth", r -> r.path("/auth/**") // 전부 허용 할 것
.uri(urlProperties.getAuth()))
.route("api_user", p -> p.path("/api/member/**") // 유저 권한이 필요 한 경우
.filters(f -> f.filter(new UserAuthFilter().apply(new UserAuthFilter.Config())))
.uri("lb://RESOURCE-SERVICE")
)
.route("api_admin", p -> p.path("/api/admin/**") // 어드민 권한이 필요 한 경우
.filters(f->f.filter(new AdminAuthFilter().apply(new AdminAuthFilter.Config())))
.uri("lb://RESOURCE-SERVICE")
)
.route("api_all", p -> p.path("/api/**") // 권한이 필요 없는 경우
.uri("lb://RESOURCE-SERVICE")
)
.build();
}
GateWayFilter
public UserAuthFilter() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String token = HttpUtils.getAuthorizationHeaderValue(exchange); // auth 헤더값 찾기
String originalPath = HttpUtils.getPath(exchange); // 원래 url
DecodedJWT jwt;
try {
jwt = TokenValidator.isValidToken(token); // 토큰 만료시간 , 위조여부 검증
TokenValidator.isValidAuthority(jwt, Config.STATUS_ACTIVE, Config.ROLE_USER, Config.ROLE_ADMIN); // 올바른 권한 갖고있는지 , 활성상태인지 검증
} catch (InvalidStatusException e) {
return ErrorResponseHandler.handleInvalidToken(exchange, HttpStatus.FORBIDDEN,
ErrorMessage.INACTIVE_USER.getMessage()); // 토큰은 유효한데 활성 상태 아님
} catch (ForbiddenAccessException e) {
return ErrorResponseHandler.handleInvalidToken(exchange, HttpStatus.FORBIDDEN,
ErrorMessage.INVALID_ACCESS.getMessage()); // 토큰은 유효한데 권한 없음 403
} catch (TokenExpiredException e) {
return ErrorResponseHandler.handleInvalidToken(exchange, HttpStatus.UNAUTHORIZED,
ErrorMessage.TOKEN_EXPIRED.getMessage()); // 토큰 만료됐음 인증 필요 401
} catch (JWTVerificationException e) {
return ErrorResponseHandler.handleInvalidToken(exchange, HttpStatus.UNAUTHORIZED,
ErrorMessage.INVALID_TOKEN.getMessage()); // 토큰이 조작됐음 올바르지 않은 요청 401
}
ServerHttpRequest modifiedRequest = exchange.getRequest().mutate()
.path(originalPath.replace("/api/member/", "/api/")) // 새로운 URL 경로 설정
.header("X-User-Id", jwt.getSubject()) // 유저 정보 보내기
.build();
ServerWebExchange modifiedExchange = exchange.mutate()
.request(modifiedRequest)
.build();
return chain.filter(modifiedExchange);
};
}
public static class Config { // // 필요한 전달할 설정
private static final String ROLE_USER = "ROLE_USER";
private static final String ROLE_ADMIN = "ROLE_ADMIN";
private static final String STATUS_ACTIVE = "활성";
}
AOP
@Around(value = "@annotation(store.mybooks.front.auth.Annotation.RequiredAuthorization)")
public Object afterMethod(ProceedingJoinPoint joinPoint)
throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(
RequestContextHolder.getRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
// RequestContextHolder = 스프링이 제공하는 RequestAttributes 의 구현체
// Spring Context 가 관리하는 것이 아님 , 현재 요청과 관련된 정보를 ThreadLocal에 저장해서 사용
// 요청이 처리되는 동안만 유효하며 , 요청이 끝난다면 정보는 자동으로 삭제됨
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
RequestContextHolder.currentRequestAttributes()
.setAttribute("authHeader", Utils.addAuthHeader(request), RequestAttributes.SCOPE_REQUEST);
try {
return joinPoint.proceed();
} catch (RuntimeException e) {
String error = e.getMessage();
log.warn(error);
if (error.contains(ErrorMessage.INVALID_ACCESS.getMessage())) { // 권한이 없음 -> ControllerAdvice 가 잡아서 index 페이지로
throw new AccessIdForbiddenException(); // 인덱스로 보내기
} else if (error.contains(ErrorMessage.TOKEN_EXPIRED.getMessage())) { // 토큰만료 재발급 받고 다시 부르기
// 리프래시 토큰이 만료면 throw Authentication -> 로그인하세요
// 리프래시 토큰 만료 아니면 토큰은 멀쩡하니 다시부르면 원래 요청대로 가짐
joinPoint.proceed();
} else if (error.contains(ErrorMessage.INVALID_TOKEN.getMessage())) { // 토큰위조됨 쿠키삭제하기 -> ControllerAdvice 가 잡아서 로그인페이지로
Utils.deleteJwtCookie(Objects.requireNonNull(response));
throw new AuthenticationIsNotValidException();
} else if(error.contains(ErrorMessage.INACTIVE_USER.getMessage())){ // 활성상태가 아님 -> ControllerAdvice 가 잡아서 인증사이트로
throw new StatusIsNotActiveException();
}
}
throw new RuntimeException();
}
AOP 는 RequiredAutorization 어노테이션이 있는 경우만 적용됨
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredAuthorization {
}
권한이 필요함을 마킹해주기 위한 어노테이션 , 다른 기능은 하지 않음
메서드 적용 방법
@RequiredAuthorization
public void modifyUserPassword(UserPasswordModifyRequest modifyRequest) {
ResponseEntity<UserPasswordModifyResponse> responseEntity =
restTemplate.exchange(gatewayAdaptorProperties.getAddress() + URL_MEMBER + "/password",
HttpMethod.PUT,
new HttpEntity<>(modifyRequest,Utils.getAuthHeader()),
new ParameterizedTypeReference<>() {
});
if (responseEntity.getStatusCode() != HttpStatus.OK) {
throw new RuntimeException();
}
}
유저의 비밀번호를 변경하는 기능 (인가처리가 필요함)
URL 은 /api/member/user/password
게이트웨이에서는 /api/member 를 보고 유저권한이 필요하구나! 생각하고 인가처리
후에 api서버로 요청을 보낼때는 /api/user/password 로 변경하여 보냄
만약 어드민 권한이 필요하다면?
/api/admin/user/password 식으로 보내야 함
기술공유 issue https://github.com/nhnacademy-be4-My-Books/etc/issues/161
Auth , Gateway 토큰 Secret Key Manager 등록 Front Payco Oauth2.0 부를 떄 사용하는 Client Id , Client Secret 등록
AOP 스펙 수정 메서드에 Request , Response 를 인자로 받지 않도록 (기존에는 aop적용 메서드에 인자로 request, response 필요했음)
또한 Annotation 이름을 Trace -> RequiredAuthorization 변경
my-books.store 인증/인가처리
로그인 요청 -> 로그인 정보 확인 -> 로그인 완료 -> 토큰 발행 -> Identity-Cookie 라는 이름의 쿠키에 토큰을 넣음
-> 서버는 사용자 인증과 관련된 정보를 갖고있지 않음
-> 사용자가 요청을 줌
1. 권한이 필요없다면? (장바구니,물건정보) 그대로 게이트웨이가 요청보냄
2. 권한이 필요하다면? (유저페이지,관리자페이지) Identity-Cookie 에 있는 토큰 값을 Authorization 헤더에 등록 후 요청보냄
게이트웨이에서 헤더에있는 토큰을 검증한 후 권한이있는지 인가처리함
에러 발생 시 HttpStatus Code
에러메시지
인가처리 완료시
X-User-Id 헤더에 유저정보를 담아 보냄
권한이 필요할시 AOP 를 적용해 위의 절차를 진행 함
GateWay
GateWayFilter
AOP
AOP 는 RequiredAutorization 어노테이션이 있는 경우만 적용됨
권한이 필요함을 마킹해주기 위한 어노테이션 , 다른 기능은 하지 않음
메서드 적용 방법
유저의 비밀번호를 변경하는 기능 (인가처리가 필요함)
URL 은 /api/member/user/password 게이트웨이에서는 /api/member 를 보고 유저권한이 필요하구나! 생각하고 인가처리 후에 api서버로 요청을 보낼때는 /api/user/password 로 변경하여 보냄
만약 어드민 권한이 필요하다면? /api/admin/user/password 식으로 보내야 함