From 720475b4e991127c1cff41d49adab37339c24ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johnny=20Miller=20=28=E9=94=BA=E4=BF=8A=29?= Date: Thu, 12 Mar 2020 11:32:23 +0800 Subject: [PATCH] feat($Common): add new module `Auth Center` Add new module `Auth Center`: Authentication and Authorization Center (AAC) for incoming requests from clients. BREAKING CHANGE: support Redis operations --- auth-center/README.md | 8 + auth-center/pom.xml | 132 +++++++++++ .../authcenter/AuthCenterApplication.java | 51 ++++ .../aspect/ExceptionControllerAdvice.java | 138 +++++++++++ .../MethodArgumentValidationAspect.java | 138 +++++++++++ .../universal/aspect/ValidateArgument.java | 19 ++ .../universal/aspect/WebRequestLogAspect.java | 126 ++++++++++ .../configuration/DruidConfiguration.java | 56 +++++ .../MyBatisPlusConfiguration.java | 34 +++ .../configuration/ProjectProperty.java | 40 ++++ .../RedisClientConfiguration.java | 39 +++ .../configuration/ServerConfiguration.java | 117 +++++++++ .../configuration/Swagger2Configuration.java | 68 ++++++ .../configuration/WebMvcConfiguration.java | 45 ++++ .../controller/CommonController.java | 44 ++++ .../universal/controller/ErrorController.java | 44 ++++ .../controller/RedirectController.java | 55 +++++ .../domain/ValidationTestPayload.java | 24 ++ .../universal/filter/RequestFilter.java | 34 +++ .../universal/service/CommonService.java | 29 +++ .../universal/service/RedisService.java | 107 +++++++++ .../service/impl/CommonServiceImpl.java | 124 ++++++++++ .../service/impl/RedisServiceImpl.java | 110 +++++++++ .../application-development-docker.yml | 15 ++ .../application-development-local.yml | 42 ++++ .../main/resources/application-production.yml | 15 ++ .../src/main/resources/application-stage.yml | 15 ++ .../src/main/resources/application-test.yml | 15 ++ .../src/main/resources/application.yml | 87 +++++++ auth-center/src/main/resources/banner.txt | 26 ++ .../src/main/resources/configuration/.empty | 0 .../configuration/logback/logback-base.xml | 69 ++++++ .../logback/logback-development-docker.xml | 10 + .../logback/logback-development-local.xml | 10 + .../logback/logback-production.xml | 10 + .../configuration/logback/logback-stage.xml | 10 + .../configuration/logback/logback-test.xml | 10 + .../src/main/resources/static/home.html | 78 ++++++ .../main/resources/static/icon/favicon.ico | Bin 0 -> 7185 bytes ...muscle-and-fitness-server-social-image.png | Bin 0 -> 70913 bytes .../src/main/resources/static/script/404.js | 12 + .../src/main/resources/static/script/home.js | 72 ++++++ .../src/main/resources/static/script/video.js | 17 ++ .../src/main/resources/static/styles/404.css | 89 +++++++ .../src/main/resources/static/styles/home.css | 222 ++++++++++++++++++ .../main/resources/static/styles/video.css | 136 +++++++++++ .../src/main/resources/static/video.html | 25 ++ .../AuthCenterApplicationTests.java | 13 + docker-compose.development-docker.yml | 41 +++- exercise-mis/pom.xml | 3 +- gateway/pom.xml | 4 + .../configuration/SecurityConfiguration.java | 42 ++++ .../application-development-local.yml | 6 + muscle-mis/pom.xml | 2 +- pom.xml | 6 + 55 files changed, 2672 insertions(+), 12 deletions(-) create mode 100644 auth-center/README.md create mode 100644 auth-center/pom.xml create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/AuthCenterApplication.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/aspect/ExceptionControllerAdvice.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/aspect/MethodArgumentValidationAspect.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/aspect/ValidateArgument.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/aspect/WebRequestLogAspect.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/DruidConfiguration.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/MyBatisPlusConfiguration.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/ProjectProperty.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/RedisClientConfiguration.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/ServerConfiguration.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/Swagger2Configuration.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/WebMvcConfiguration.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/controller/CommonController.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/controller/ErrorController.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/controller/RedirectController.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/domain/ValidationTestPayload.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/filter/RequestFilter.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/service/CommonService.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/service/RedisService.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/service/impl/CommonServiceImpl.java create mode 100644 auth-center/src/main/java/com/jmsoftware/authcenter/universal/service/impl/RedisServiceImpl.java create mode 100644 auth-center/src/main/resources/application-development-docker.yml create mode 100644 auth-center/src/main/resources/application-development-local.yml create mode 100644 auth-center/src/main/resources/application-production.yml create mode 100644 auth-center/src/main/resources/application-stage.yml create mode 100644 auth-center/src/main/resources/application-test.yml create mode 100644 auth-center/src/main/resources/application.yml create mode 100644 auth-center/src/main/resources/banner.txt create mode 100644 auth-center/src/main/resources/configuration/.empty create mode 100644 auth-center/src/main/resources/configuration/logback/logback-base.xml create mode 100644 auth-center/src/main/resources/configuration/logback/logback-development-docker.xml create mode 100644 auth-center/src/main/resources/configuration/logback/logback-development-local.xml create mode 100644 auth-center/src/main/resources/configuration/logback/logback-production.xml create mode 100644 auth-center/src/main/resources/configuration/logback/logback-stage.xml create mode 100644 auth-center/src/main/resources/configuration/logback/logback-test.xml create mode 100644 auth-center/src/main/resources/static/home.html create mode 100644 auth-center/src/main/resources/static/icon/favicon.ico create mode 100644 auth-center/src/main/resources/static/image/muscle-and-fitness-server-social-image.png create mode 100644 auth-center/src/main/resources/static/script/404.js create mode 100644 auth-center/src/main/resources/static/script/home.js create mode 100644 auth-center/src/main/resources/static/script/video.js create mode 100644 auth-center/src/main/resources/static/styles/404.css create mode 100644 auth-center/src/main/resources/static/styles/home.css create mode 100644 auth-center/src/main/resources/static/styles/video.css create mode 100644 auth-center/src/main/resources/static/video.html create mode 100644 auth-center/src/test/java/com/jmsoftware/authcenter/AuthCenterApplicationTests.java create mode 100644 gateway/src/main/java/com/jmsoftware/gateway/universal/configuration/SecurityConfiguration.java diff --git a/auth-center/README.md b/auth-center/README.md new file mode 100644 index 00000000..97150fe9 --- /dev/null +++ b/auth-center/README.md @@ -0,0 +1,8 @@ +# Getting Started + +### Reference Documentation +For further reference, please consider the following sections: + +* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) +* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/maven-plugin/) + diff --git a/auth-center/pom.xml b/auth-center/pom.xml new file mode 100644 index 00000000..77fe6bb5 --- /dev/null +++ b/auth-center/pom.xml @@ -0,0 +1,132 @@ + + + 4.0.0 + + + auth-center + Auth Center + Authentication and Authorization Center (AAC) for incoming requests from clients. + + 11 + 8770 + + + com.jmsoftware + muscle-and-fitness-server + 0.0.1-SNAPSHOT + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + com.google.cloud.tools + jib-maven-plugin + 2.0.0 + + + + compilingPhaseJib + compile + + dockerBuild + + + + + packagingPhaseJib + package + + build + + + + + + openjdk:11.0.5-slim + + + docker.io/ijohnnymiller/${project.artifactId}-${envAlias} + + ${project.version} + + + + + /${project.artifactId}-${envAlias} + + -Xmx256m + + + ${port} + + USE_CURRENT_TIMESTAMP + + + + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-starter-netflix-eureka-client + + + de.codecentric + spring-boot-admin-starter-client + ${spring-boot-admin.version} + + + org.springframework.cloud + spring-cloud-starter-zipkin + + + org.springframework.boot + spring-boot-starter-data-redis + + + + com.jmsoftware + common + 0.0.1-SNAPSHOT + + + + + com.baomidou + mybatis-plus-boot-starter + 3.3.1 + + + mysql + mysql-connector-java + runtime + + + com.alibaba + druid + 1.1.21 + + + + redis.clients + jedis + + + diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/AuthCenterApplication.java b/auth-center/src/main/java/com/jmsoftware/authcenter/AuthCenterApplication.java new file mode 100644 index 00000000..9197d313 --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/AuthCenterApplication.java @@ -0,0 +1,51 @@ +package com.jmsoftware.authcenter; + +import com.jmsoftware.authcenter.universal.configuration.ProjectProperty; +import com.jmsoftware.authcenter.universal.configuration.ServerConfiguration; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; + +import java.time.Duration; +import java.time.Instant; +import java.util.TimeZone; + +/** + *

AuthCenterApplication

+ *

+ * Change description here. + * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 3/12/20 9:57 AM + **/ +@Slf4j +@EnableFeignClients +@EnableDiscoveryClient +@SpringBootApplication +public class AuthCenterApplication { + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static ProjectProperty projectProperty; + private static ServerConfiguration serverConfiguration; + + public AuthCenterApplication(ProjectProperty projectProperty, ServerConfiguration serverConfiguration) { + AuthCenterApplication.projectProperty = projectProperty; + AuthCenterApplication.serverConfiguration = serverConfiguration; + } + + public static void main(String[] args) { + var startInstant = Instant.now(); + SpringApplication.run(AuthCenterApplication.class, args); + var endInstant = Instant.now(); + var duration = Duration.between(startInstant, endInstant); + log.info("🥳 Congratulations! 🎉"); + log.info("🖥 {}@{} started!", projectProperty.getProjectArtifactId(), projectProperty.getVersion()); + log.info("⚙️ Environment: {} ({})", projectProperty.getEnvironment(), projectProperty.getEnvironmentAlias()); + log.info("⏳ Deployment duration: {} seconds ({} ms)", duration.getSeconds(), duration.toMillis()); + log.info("⏰ App started at {} (timezone - {})", endInstant, TimeZone.getDefault().getDisplayName()); + log.info("{} App running at{} - Local: http://localhost:{}{}/{} - Network: {}/", + LINE_SEPARATOR, LINE_SEPARATOR, serverConfiguration.getServerPort(), projectProperty.getContextPath(), + LINE_SEPARATOR, serverConfiguration.getBaseUrl()); + } +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/aspect/ExceptionControllerAdvice.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/aspect/ExceptionControllerAdvice.java new file mode 100644 index 00000000..878088b3 --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/aspect/ExceptionControllerAdvice.java @@ -0,0 +1,138 @@ +package com.jmsoftware.authcenter.universal.aspect; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.json.JSONUtil; +import com.jmsoftware.common.bean.ResponseBodyBean; +import com.jmsoftware.common.constant.HttpStatus; +import com.jmsoftware.common.exception.BaseException; +import com.jmsoftware.common.util.RequestUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.NoHandlerFoundException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.ConstraintViolationException; +import java.util.Objects; + +/** + *

ExceptionControllerAdvice

+ *

+ * Exception advice for global controllers. + * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 2019-03-02 17:39 + **/ +@Slf4j +@ControllerAdvice +public class ExceptionControllerAdvice { + /** + *

Exception handler.

+ *

ATTENTION: In this method, cannot throw any exception.

+ * + * @param request HTTP request + * @param exception any kinds of exception occurred in controller + * @return custom exception info + */ + @ResponseBody + @ExceptionHandler(value = Exception.class) + public ResponseBodyBean handleException(HttpServletRequest request, + HttpServletResponse response, + Exception exception) { + log.error("Exception occurred when [{}] requested access. URL: {}", + RequestUtil.getRequestIpAndPort(request), + request.getServletPath()); + + // FIXME: THIS IS NOT A PROBLEM + // ATTENTION: Use only ResponseBodyBean.ofStatus() in handleException() method and DON'T throw any exception + if (exception instanceof NoHandlerFoundException) { + log.error("[GlobalExceptionCapture] NoHandlerFoundException: Request URL = {}, HTTP method = {}", + ((NoHandlerFoundException) exception).getRequestURL(), + ((NoHandlerFoundException) exception).getHttpMethod()); + response.setStatus(HttpStatus.NOT_FOUND.getCode()); + return ResponseBodyBean.ofStatus(HttpStatus.NOT_FOUND); + } else if (exception instanceof HttpRequestMethodNotSupportedException) { + log.error("[GlobalExceptionCapture] HttpRequestMethodNotSupportedException: " + + "Current method is {}, Support HTTP method = {}", + ((HttpRequestMethodNotSupportedException) exception).getMethod(), + JSONUtil.toJsonStr( + ((HttpRequestMethodNotSupportedException) exception).getSupportedHttpMethods())); + response.setStatus(HttpStatus.METHOD_NOT_ALLOWED.getCode()); + return ResponseBodyBean.ofStatus(HttpStatus.METHOD_NOT_ALLOWED); + } else if (exception instanceof MethodArgumentNotValidException) { + log.error("[GlobalExceptionCapture] MethodArgumentNotValidException: {}", exception.getMessage()); + response.setStatus(HttpStatus.BAD_REQUEST.getCode()); + return ResponseBodyBean.ofStatus(HttpStatus.BAD_REQUEST.getCode(), + getFieldErrorMessageFromException((MethodArgumentNotValidException) exception), + null); + } else if (exception instanceof ConstraintViolationException) { + log.error("[GlobalExceptionCapture] ConstraintViolationException: {}", exception.getMessage()); + response.setStatus(HttpStatus.BAD_REQUEST.getCode()); + return ResponseBodyBean.ofStatus(HttpStatus.BAD_REQUEST.getCode(), + CollUtil.getFirst(((ConstraintViolationException) exception) + .getConstraintViolations()) + .getMessage(), null); + } else if (exception instanceof MethodArgumentTypeMismatchException) { + log.error("[GlobalExceptionCapture] MethodArgumentTypeMismatchException: " + + "Parameter name = {}, Exception information: {}", + ((MethodArgumentTypeMismatchException) exception).getName(), + ((MethodArgumentTypeMismatchException) exception).getMessage()); + response.setStatus(HttpStatus.PARAM_NOT_MATCH.getCode()); + return ResponseBodyBean.ofStatus(HttpStatus.PARAM_NOT_MATCH); + } else if (exception instanceof HttpMessageNotReadableException) { + log.error("[GlobalExceptionCapture] HttpMessageNotReadableException: {}", + ((HttpMessageNotReadableException) exception).getMessage()); + response.setStatus(HttpStatus.PARAM_NOT_NULL.getCode()); + return ResponseBodyBean.ofStatus(HttpStatus.PARAM_NOT_NULL); + } else if (exception instanceof BaseException) { + log.error("[GlobalExceptionCapture] BaseException: Status code: {}, message: {}, data: {}", + ((BaseException) exception).getCode(), + exception.getMessage(), + ((BaseException) exception).getData()); + response.setStatus(((BaseException) exception).getCode()); + return ResponseBodyBean.ofStatus(((BaseException) exception).getCode(), + exception.getMessage(), + ((BaseException) exception).getData()); + } else if (exception instanceof BindException) { + log.error("[GlobalExceptionCapture]: Exception information: {} ", exception.getMessage()); + response.setStatus(HttpStatus.INVALID_PARAM.getCode()); + return ResponseBodyBean.ofStatus(HttpStatus.INVALID_PARAM); + } + log.error("[GlobalExceptionCapture]: Exception information: {} ", exception.getMessage(), exception); + response.setStatus(HttpStatus.ERROR.getCode()); + return ResponseBodyBean.ofStatus(HttpStatus.ERROR.getCode(), HttpStatus.ERROR.getMessage(), null); + } + + /** + * Get field error message from exception. If two or more fields do not pass Spring Validation check, then will + * return the 1st error message of the error field. + * + * @param exception MethodArgumentNotValidException + * @return field error message + */ + private String getFieldErrorMessageFromException(MethodArgumentNotValidException exception) { + try { + DefaultMessageSourceResolvable firstErrorField = + (DefaultMessageSourceResolvable) Objects.requireNonNull(exception.getBindingResult() + .getAllErrors() + .get(0) + .getArguments())[0]; + String firstErrorFieldName = firstErrorField.getDefaultMessage(); + String firstErrorFieldMessage = exception.getBindingResult().getAllErrors().get(0).getDefaultMessage(); + return firstErrorFieldName + " " + firstErrorFieldMessage; + } catch (Exception e) { + log.error("Exception occurred when get field error message from exception. Exception message: {}", + e.getMessage(), + e); + return HttpStatus.INVALID_PARAM.getMessage(); + } + } +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/aspect/MethodArgumentValidationAspect.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/aspect/MethodArgumentValidationAspect.java new file mode 100644 index 00000000..c8acb071 --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/aspect/MethodArgumentValidationAspect.java @@ -0,0 +1,138 @@ +package com.jmsoftware.authcenter.universal.aspect; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ArrayUtil; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.*; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; + +import javax.validation.*; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + *

MethodArgumentValidationAspect

+ *

This class is an aspect class that validates method's argument(s).

+ *

USAGE (Must Do's)

+ *
    + *
  1. Annotate the method which argument we need to validate by @ValidateArgument
  2. + *
  3. Annotate the argument(s) that we need to validate by @javax.validation.Valid
  4. + *
  5. The field(s) of the argument(s) could be annotated by the constraint annotation provided by Spring + * Security
  6. + *
+ *

ATTENTION

+ *

If the argument doesn't pass validation, an IllegalArgumentException will be thrown, and not proceed + * the target method.

+ * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 2019-07-06 12:17 + **/ +@Slf4j +@Aspect +@Component +public class MethodArgumentValidationAspect { + private final Validator validator; + + public MethodArgumentValidationAspect() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + this.validator = factory.getValidator(); + } + + /** + * Define pointcut. Pointcut is a predicate or expression that matches join points. In + * ValidateMethodArgumentAspect, we need to cut any method annotated with `@ValidateArgument` only. + *

+ * More detail at: Spring aop aspectJ + * pointcut expression examples + */ + @Pointcut("@annotation(com.jmsoftware.authcenter.universal.aspect.ValidateArgument)") + public void validateMethodArgumentPointcut() { + } + + /** + * Before handle method's argument. This method will be executed after called `proceedingJoinPoint.proceed()`. In + * this phrase, we're going to take some logs. + *

+ * `@Before` annotated methods run exactly before the all methods matching with pointcut expression. + * + * @param joinPoint a point of execution of the program + */ + @Before("validateMethodArgumentPointcut()") + public void beforeMethodHandleArgument(JoinPoint joinPoint) { + log.info("Method : {}#{}", + joinPoint.getSignature().getDeclaringTypeName(), + joinPoint.getSignature().getName()); + log.info("Argument : {}", joinPoint.getArgs()); + } + + /** + * Around annotated method processes argument. Around advice can perform custom behavior before and after the + * method invocation. + * + * @param proceedingJoinPoint the object can perform method invocation + * @return any value (may be void) that annotated method returned + */ + @Around("validateMethodArgumentPointcut()") + public Object aroundMethodHandleArgument(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { + log.info("======= METHOD'S ARGUMENT VALIDATION START ======="); + Object[] args = proceedingJoinPoint.getArgs(); + MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature(); + Annotation[][] parameterAnnotations = signature.getMethod().getParameterAnnotations(); + // argumentIndexes is the array list that stores the index of argument we need to validate (the argument + // annotated by `@Valid`) + List argumentIndexes = new ArrayList<>(); + for (Annotation[] parameterAnnotation : parameterAnnotations) { + int paramIndex = ArrayUtil.indexOf(parameterAnnotations, parameterAnnotation); + for (Annotation annotation : parameterAnnotation) { + if (annotation instanceof Valid) { + argumentIndexes.add(paramIndex); + } + } + } + + for (Integer index : argumentIndexes) { + Set> validates = validator.validate(args[index]); + if (CollectionUtil.isNotEmpty(validates)) { + String message = String.format("Argument validation failed: %s", validates); + log.info("Method : {}#{}", + proceedingJoinPoint.getSignature().getDeclaringTypeName(), + proceedingJoinPoint.getSignature().getName()); + log.info("Argument : {}", args); + log.error("Validation result: {}", message); + // If the argument doesn't pass validation, an IllegalArgumentException will be thrown, and not + // proceed the target method + throw new IllegalArgumentException(message); + } + } + + log.info("Validation result: Validation passed"); + return proceedingJoinPoint.proceed(); + } + + /** + * `@After` annotated methods run exactly after the all methods matching with pointcut expression. + */ + @After("validateMethodArgumentPointcut()") + public void afterMethodHandleArgument() { + log.info("======== METHOD'S ARGUMENT VALIDATION END ========"); + } + + /** + * `@AfterThrowing` annotated methods run after the method (matching with pointcut expression) exits by throwing an + * exception. + * + * @param joinPoint a point of execution of the program + * @param e exception that controller's method throws + */ + @AfterThrowing(pointcut = "validateMethodArgumentPointcut()", throwing = "e") + public void afterThrowingException(JoinPoint joinPoint, Exception e) { + log.info("Signature : {}", joinPoint.getSignature().toShortString()); + log.error("Exception message: {}", e.toString()); + log.error("== METHOD'S ARGUMENT VALIDATION END WITH EXCEPTION =="); + } +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/aspect/ValidateArgument.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/aspect/ValidateArgument.java new file mode 100644 index 00000000..9c9a5122 --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/aspect/ValidateArgument.java @@ -0,0 +1,19 @@ +package com.jmsoftware.authcenter.universal.aspect; + +import java.lang.annotation.*; + +/** + *

ValidateArgument

+ *

Annotation for validating method's argument.

+ *

ATTENTION

+ *

If the argument doesn't pass validation, an IllegalArgumentException will be thrown, and not proceed + * the target method.

+ * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 2019-07-06 12:08 + **/ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +@Documented +public @interface ValidateArgument { +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/aspect/WebRequestLogAspect.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/aspect/WebRequestLogAspect.java new file mode 100644 index 00000000..ded4ca37 --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/aspect/WebRequestLogAspect.java @@ -0,0 +1,126 @@ +package com.jmsoftware.authcenter.universal.aspect; + +import cn.hutool.core.util.NumberUtil; +import cn.hutool.json.JSONUtil; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jmsoftware.common.util.RequestUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.*; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; + +/** + *

RequestLogAspect

+ *

Description:

+ *

RequestLogAspect is an AOP for logging URL, HTTP method, client IP and other information when web resource was + * accessed.

+ *

Feature:

+ *

No methods in controller need to be decorated with annotation. This aspect would automatically cut the method + * decorated with `@GetMapping` or `@PostMapping`.

+ * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 2019-05-05 19:55 + **/ +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class WebRequestLogAspect { + private static final String LINE_SEPARATOR = System.lineSeparator(); + private final ObjectMapper mapper = new ObjectMapper(); + + /** + * Define pointcut. Pointcut is a predicate or expression that matches join points. In WebRequestLogAspect, we need + * to cut any method annotated with `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`, `@PatchMapping`, `@RequestMapping`. + *

+ * More detail at: Spring aop aspectJ + * pointcut expression examples + */ + @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)" + + " || @annotation(org.springframework.web.bind.annotation.PostMapping)" + + " || @annotation(org.springframework.web.bind.annotation.PutMapping)" + + " || @annotation(org.springframework.web.bind.annotation.DeleteMapping)" + + " || @annotation(org.springframework.web.bind.annotation.PatchMapping)" + + " || @annotation(org.springframework.web.bind.annotation.RequestMapping)") + public void requestLogPointcut() { + } + + /** + * Before controller handle client request (on client sent a request). + *

+ * `@Before` annotated methods run exactly before the all methods matching with pointcut expression. + * + * @param joinPoint a point of execution of the program + */ + @Before("requestLogPointcut()") + public void beforeHandleRequest(JoinPoint joinPoint) { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + assert attributes != null; + HttpServletRequest request = attributes.getRequest(); + log.info("============ WEB REQUEST LOG START ============"); + log.info("URL : {}", request.getRequestURL().toString()); + log.info("HTTP Method : {}", request.getMethod()); + log.info("Client IP:Port : {}", RequestUtil.getRequestIpAndPort(request)); + log.info("Class Method : {}#{}", + joinPoint.getSignature().getDeclaringTypeName(), + joinPoint.getSignature().getName()); + log.info("Request Params :{}{}", LINE_SEPARATOR, JSONUtil.toJsonPrettyStr(joinPoint.getArgs())); + } + + /** + * Around controller's method processes client request. Around advice can perform custom behavior before and after + * the method invocation. + * + * @param proceedingJoinPoint the object can perform method invocation + * @return any value (may be void) that controller's method returned + * @throws Throwable any exceptions that controller's method may throw + */ + @Around("requestLogPointcut()") + public Object aroundHandleRequest(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { + long startTime = System.currentTimeMillis(); + Object result = proceedingJoinPoint.proceed(); + long elapsedTime = System.currentTimeMillis() - startTime; + try { + var formattedStringifiedJson = JSONUtil.formatJsonStr(mapper.writeValueAsString(result)); + if (formattedStringifiedJson.length() > 500) { + formattedStringifiedJson = formattedStringifiedJson.substring(0, 499).concat("…"); + } + log.info("Response :{}{}", LINE_SEPARATOR, formattedStringifiedJson); + } catch (JsonProcessingException e) { + log.info("Response (non-JSON): {}", result); + } + log.info("Elapsed time : {} s ({} ms)", + NumberUtil.decimalFormat("0.00", elapsedTime / 1000D), + elapsedTime); + return result; + } + + /** + * `@After` annotated methods run exactly after the all methods matching with pointcut expression. + */ + @After("requestLogPointcut()") + public void afterHandleRequest() { + log.info("============= WEB REQUEST LOG END ============="); + } + + /** + * `@AfterThrowing` annotated methods run after the method (matching with pointcut expression) exits by throwing an + * exception. + * + * @param joinPoint a point of execution of the program + * @param e exception that controller's method throws + */ + @AfterThrowing(pointcut = "requestLogPointcut()", throwing = "e") + public void afterThrowingException(JoinPoint joinPoint, Exception e) { + log.info("Signature : {}", joinPoint.getSignature().toShortString()); + log.error("Exception message : {}, message: {}", e.toString(), e.getMessage()); + log.error("====== WEB REQUEST LOG END WITH EXCEPTION ====="); + } +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/DruidConfiguration.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/DruidConfiguration.java new file mode 100644 index 00000000..c513fed4 --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/DruidConfiguration.java @@ -0,0 +1,56 @@ +package com.jmsoftware.authcenter.universal.configuration; + +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.support.http.StatViewServlet; +import com.alibaba.druid.support.http.WebStatFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.sql.DataSource; + +/** + *

DruidConfiguration

+ *

+ * Druid Configuration + * Click me to view Druid Monitor + * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 2019-03-24 13:31 + **/ +@Configuration +@RequiredArgsConstructor +public class DruidConfiguration { + @Bean + public ServletRegistrationBean druidServlet() { + ServletRegistrationBean servletRegistrationBean = + new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*"); + servletRegistrationBean.addInitParameter("loginUsername", "admin"); + servletRegistrationBean.addInitParameter("loginPassword", "admin"); + servletRegistrationBean.addInitParameter("resetEnable", "false"); + return servletRegistrationBean; + } + + @Bean + public FilterRegistrationBean filterRegistrationBean() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); + filterRegistrationBean.setFilter(new WebStatFilter()); + filterRegistrationBean.addUrlPatterns("/*"); + // Ignored resources + filterRegistrationBean.addInitParameter("exclusions", + "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); + filterRegistrationBean.addInitParameter("profileEnable", "true"); + filterRegistrationBean.addInitParameter("principalCookieName", "USER_COOKIE"); + filterRegistrationBean.addInitParameter("principalSessionName", "USER_SESSION"); + return filterRegistrationBean; + } + + @Bean + @ConfigurationProperties(prefix = "spring.datasource") + public DataSource druidDataSource() { + return new DruidDataSource(); + } +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/MyBatisPlusConfiguration.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/MyBatisPlusConfiguration.java new file mode 100644 index 00000000..413b1b28 --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/MyBatisPlusConfiguration.java @@ -0,0 +1,34 @@ +package com.jmsoftware.authcenter.universal.configuration; + +import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; +import com.baomidou.mybatisplus.extension.plugins.pagination.optimize.JsqlParserCountOptimize; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + *

MyBatisPlusConfiguration

+ *

+ * Change description here. + * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 2019-05-02 11:57 + **/ +@Configuration +@EnableTransactionManagement +public class MyBatisPlusConfiguration { + /** + * Inject bean to enable pagination. + * + * @return bean for pagination + */ + @Bean + public PaginationInterceptor paginationInterceptor() { + var paginationInterceptor = new PaginationInterceptor(); + // Set maximum query record count + paginationInterceptor.setLimit(100L); + // Enable JSQL Parser Count Optimizing (for left join) + paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true)); + return paginationInterceptor; + } +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/ProjectProperty.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/ProjectProperty.java new file mode 100644 index 00000000..9175f94a --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/ProjectProperty.java @@ -0,0 +1,40 @@ +package com.jmsoftware.authcenter.universal.configuration; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + *

ProjectProperty

+ *

+ * Change description here. + * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 2019-04-18 13:01 + **/ +@Slf4j +@Data +@Component +@ConfigurationProperties(prefix = "project.property") +public class ProjectProperty { + private String basePackage; + private String contextPath; + private String groupId; + private String artifactId; + private String projectArtifactId; + private String version; + private String description; + private String jdkVersion; + private String environment; + private String environmentAlias; + private String url; + private String inceptionYear; + private String organizationName; + private String organizationUrl; + private String issueManagementSystem; + private String issueManagementUrl; + private String developerName; + private String developerEmail; + private String developerUrl; +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/RedisClientConfiguration.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/RedisClientConfiguration.java new file mode 100644 index 00000000..2b212068 --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/RedisClientConfiguration.java @@ -0,0 +1,39 @@ +package com.jmsoftware.authcenter.universal.configuration; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.io.Serializable; + +/** + *

RedisClientConfiguration

+ *

+ * Redis configuration. + * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 2019-03-23 14:27 + **/ +@Configuration +@EnableCaching +@AutoConfigureAfter(RedisAutoConfiguration.class) +public class RedisClientConfiguration extends CachingConfigurerSupport { + /** + * Redis template. Support for <String, Serializable> + */ + @Bean + public RedisTemplate redisFactory(LettuceConnectionFactory lettuceConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + template.setConnectionFactory(lettuceConnectionFactory); + return template; + } +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/ServerConfiguration.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/ServerConfiguration.java new file mode 100644 index 00000000..21ea990d --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/ServerConfiguration.java @@ -0,0 +1,117 @@ +package com.jmsoftware.authcenter.universal.configuration; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.context.WebServerInitializedEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.URL; +import java.util.Enumeration; + +/** + *

ServerConfiguration

+ *

+ * Change description here. + * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 2019-04-26 16:02 + **/ +@Slf4j +@Getter +@Component +@RequiredArgsConstructor +public class ServerConfiguration implements ApplicationListener { + public static final String DEVELOPMENT_ENVIRONMENT_ALIAS = "dev"; + private final ProjectProperty projectProperty; + private int serverPort; + + @Override + public void onApplicationEvent(WebServerInitializedEvent event) { + this.serverPort = event.getWebServer().getPort(); + } + + /** + *

Get base URL of backend server.

+ *

The result will be like:

+ *
    + *
  1. http://[serverIp]:[serverPort]/[contextPath]
  2. + *
  3. https://[serverIp]/[contextPath]
  4. + *
+ * + * @return base URL + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 2019-05-03 16:05 + */ + public String getBaseUrl() { + return "http://" + this.getPublicIp() + ":" + serverPort + projectProperty.getContextPath(); + } + + /** + * Find public IP address. + * + * @return public IP + */ + public String getPublicIp() { + if (projectProperty.getEnvironmentAlias().contains(DEVELOPMENT_ENVIRONMENT_ALIAS)) { + return this.getInternetIp(); + } + try { + // An API provided by https://whatismyipaddress.com/api + URL url = new URL("https://ipv4bot.whatismyipaddress.com/"); + BufferedReader sc = new BufferedReader(new InputStreamReader(url.openStream())); + // Read system IP Address + return sc.readLine().trim(); + } catch (Exception e) { + log.error("Cannot execute properly to get IP address from https://whatismyipaddress.com/api", e); + } + return this.getInternetIp(); + } + + /** + * Get internet IP. + * + * @return internet IP + */ + private String getInternetIp() { + String intranetIp = this.getIntranetIp(); + try { + Enumeration networks = NetworkInterface.getNetworkInterfaces(); + InetAddress ip; + Enumeration addresses; + while (networks.hasMoreElements()) { + addresses = networks.nextElement().getInetAddresses(); + while (addresses.hasMoreElements()) { + ip = addresses.nextElement(); + if (ip instanceof Inet4Address + && ip.isSiteLocalAddress() + && !ip.getHostAddress().equals(intranetIp)) { + return ip.getHostAddress(); + } + } + } + return intranetIp; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Get intranet IP. + * + * @return intranet IP + */ + private String getIntranetIp() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/Swagger2Configuration.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/Swagger2Configuration.java new file mode 100644 index 00000000..259713c3 --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/Swagger2Configuration.java @@ -0,0 +1,68 @@ +package com.jmsoftware.authcenter.universal.configuration; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +/** + *

Swagger2Configuration

+ *

+ * Swagger 2 Configuration + * Click me to view + * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 2019-02-07 16:15 + **/ +@Configuration +@EnableSwagger2 +@RequiredArgsConstructor +public class Swagger2Configuration { + private final ProjectProperty projectProperty; + private static final String LINE_SEPARATOR = System.lineSeparator(); + + @Bean + public Docket createRestApi() { + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .select() + .apis(RequestHandlerSelectors.basePackage(projectProperty.getBasePackage())) + .paths(PathSelectors.any()) + .build(); + } + + private ApiInfo apiInfo() { + var projectArtifactId = projectProperty.getProjectArtifactId(); + var version = projectProperty.getVersion(); + var developerEmail = projectProperty.getDeveloperEmail(); + var developerUrl = projectProperty.getDeveloperUrl(); + var environmentAlias = projectProperty.getEnvironmentAlias(); + return new ApiInfoBuilder() + .title(String.format("API for %s@%s (%s)", + projectArtifactId, + version, + environmentAlias)) + .description(String.format("%s %sArtifact ID: %s%sEnvironment: %s (%s)", + projectProperty.getDescription(), + LINE_SEPARATOR, + projectArtifactId, + LINE_SEPARATOR, + projectProperty.getEnvironment(), + environmentAlias)) + .contact(new Contact(String.format("%s, email: %s%sHome page: %s", + projectProperty.getDeveloperName(), + developerEmail, + LINE_SEPARATOR, + developerUrl), + developerUrl, developerEmail)) + .version(version) + .build(); + } +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/WebMvcConfiguration.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/WebMvcConfiguration.java new file mode 100644 index 00000000..76deb6c9 --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/configuration/WebMvcConfiguration.java @@ -0,0 +1,45 @@ +package com.jmsoftware.authcenter.universal.configuration; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + *

WebMvcConfiguration

+ *

+ * Spring MVC Configurations. + * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 1/23/20 9:02 AM + **/ +@Configuration +@RequiredArgsConstructor +public class WebMvcConfiguration implements WebMvcConfigurer { + private static final long MAX_AGE_SECS = 3600; + + /** + * 1. Config static path pattern + * 2. Config static resource location + * + * @param registry static resources register + */ + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/"); + } + + /** + * Configure cross origin requests processing. + * + * @param registry CORS registry + */ + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowedMethods("HEAD", "OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE") + .maxAge(MAX_AGE_SECS); + } +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/controller/CommonController.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/controller/CommonController.java new file mode 100644 index 00000000..8ed0a7e1 --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/controller/CommonController.java @@ -0,0 +1,44 @@ +package com.jmsoftware.authcenter.universal.controller; + +import com.jmsoftware.authcenter.universal.domain.ValidationTestPayload; +import com.jmsoftware.authcenter.universal.service.CommonService; +import com.jmsoftware.authcenter.universal.service.RedisService; +import com.jmsoftware.common.bean.ResponseBodyBean; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + *

CommonController

+ *

+ * Change description here. + * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 2/4/20 10:29 AM + **/ +@RestController +@RequiredArgsConstructor +@RequestMapping("/common") +@Api(tags = {"Common Controller"}) +public class CommonController { + private final CommonService commonService; + private final RedisService redisService; + + @GetMapping("/app-info") + @ApiOperation(value = "/app-info", notes = "Retrieve application information") + public ResponseBodyBean> applicationInformation() { + var data = commonService.getApplicationInfo(); + redisService.set("appInfo", data.toString()); + return ResponseBodyBean.ofDataAndMessage(data, "Succeed to retrieve app info."); + } + + @PostMapping("/validation-test") + @ApiOperation(value = "/validation-test", notes = "Validation of request payload test") + public ResponseBodyBean validationTest(@RequestBody ValidationTestPayload payload) { + commonService.validateObject(payload); + return ResponseBodyBean.ofDataAndMessage(payload.getName(), "validationTest()"); + } +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/controller/ErrorController.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/controller/ErrorController.java new file mode 100644 index 00000000..05b33384 --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/controller/ErrorController.java @@ -0,0 +1,44 @@ +package com.jmsoftware.authcenter.universal.controller; + +import io.swagger.annotations.Api; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController; +import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver; +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Map; + +/** + *

ErrorController

+ *

+ * Error controller. + * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 2019-03-02 16:56 + **/ +@Slf4j +@RestController +@Api(tags = {"Error Controller"}) +public class ErrorController extends BasicErrorController { + public ErrorController(ErrorAttributes errorAttributes, + ServerProperties serverProperties, + List errorViewResolvers) { + super(errorAttributes, serverProperties.getError(), errorViewResolvers); + } + + @Override + public ResponseEntity> error(HttpServletRequest request) { + HttpStatus httpStatus = getStatus(request); + Map body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); + body.put("message", httpStatus.getReasonPhrase()); + log.error("Captured HTTP request error. Response body = {}", body); + return new ResponseEntity<>(body, httpStatus); + } +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/controller/RedirectController.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/controller/RedirectController.java new file mode 100644 index 00000000..55302068 --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/controller/RedirectController.java @@ -0,0 +1,55 @@ +package com.jmsoftware.authcenter.universal.controller; + +import com.jmsoftware.authcenter.universal.configuration.ProjectProperty; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + *

RedirectController

+ *

+ * HTTP Redirect Controller + * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 1/21/20 1:18 PM + **/ +@Slf4j +@RestController +@RequiredArgsConstructor +@Api(tags = {"Redirect Controller"}) +public class RedirectController { + private final ProjectProperty projectProperty; + + @PostConstruct + public void postConstruct() { + log.info("URL redirect service initialized."); + } + + @GetMapping("/home") + @ApiOperation(value = "/home", notes = "Home page") + public void handleHomeRequest(HttpServletResponse response) throws IOException { + // Redirect to home page + response.sendRedirect(projectProperty.getContextPath() + "static/home.html"); + } + + @GetMapping("/doc") + @ApiOperation(value = "/doc", notes = "Swagger API Documentation") + public void handleDocRequest(HttpServletResponse response) throws IOException { + // Redirect to Bootstrap Swagger API documentation + response.sendRedirect(projectProperty.getContextPath() + "/doc.html?cache=1&lang=en"); + } + + @GetMapping("/webjars/bycdao-ui/images/api.ico") + @ApiOperation(value = "/webjars/bycdao-ui/images/api.ico", notes = "Favicon redirection") + public void handleFaviconRequest(HttpServletResponse response) throws IOException { + // Redirect to a customized favicon + response.sendRedirect(projectProperty.getContextPath() + "/static/icon/favicon.ico"); + } +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/domain/ValidationTestPayload.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/domain/ValidationTestPayload.java new file mode 100644 index 00000000..f0b9a56b --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/domain/ValidationTestPayload.java @@ -0,0 +1,24 @@ +package com.jmsoftware.authcenter.universal.domain; + +import lombok.Data; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + *

ValidationTestPayload

+ *

+ * Change description here. + * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 2/14/20 11:34 AM + **/ +@Data +public class ValidationTestPayload { + @NotNull + @Min(value = 1L) + private Long id; + @NotEmpty + private String name; +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/filter/RequestFilter.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/filter/RequestFilter.java new file mode 100644 index 00000000..d3d0bce1 --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/filter/RequestFilter.java @@ -0,0 +1,34 @@ +package com.jmsoftware.authcenter.universal.filter; + +import com.jmsoftware.common.util.RequestUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + *

RequestFilter

+ *

Request filter.

+ * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 2019-03-23 14:24 + **/ +@Slf4j +@Component +public class RequestFilter extends OncePerRequestFilter { + @Override + @SuppressWarnings("NullableProblems") + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws IOException, ServletException { + log.info("[{}] Client requested access. Method: {}, URL: {}", + RequestUtil.getRequestIpAndPort(request), + request.getMethod(), + request.getRequestURL()); + filterChain.doFilter(request, response); + } +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/service/CommonService.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/service/CommonService.java new file mode 100644 index 00000000..8ba49847 --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/service/CommonService.java @@ -0,0 +1,29 @@ +package com.jmsoftware.authcenter.universal.service; + +import com.jmsoftware.authcenter.universal.domain.ValidationTestPayload; + +import java.util.Map; + +/** + *

CommonService

+ *

+ * Change description here. + * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 2/4/20 11:15 AM + */ +public interface CommonService { + /** + * Gets application info. + * + * @return the application info. + */ + Map getApplicationInfo(); + + /** + * Validate object. + * + * @param payload the payload + */ + void validateObject(ValidationTestPayload payload); +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/service/RedisService.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/service/RedisService.java new file mode 100644 index 00000000..f3744111 --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/service/RedisService.java @@ -0,0 +1,107 @@ +package com.jmsoftware.authcenter.universal.service; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + *

RedisService

+ *

Redis service interface for Redis useful operations

+ * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 2019-07-05 09:38 + **/ +public interface RedisService { + /** + * Set key and value in Redis with expiration + * + * @param key key + * @param value value + * @param expirationTime expiration time + * @param timeUnit time unit + * @return true - operation done; false - operation failure + */ + Boolean set(String key, String value, Long expirationTime, TimeUnit timeUnit); + + /** + * Set key and list in Redis with expiration + * + * @param key key + * @param list list + * @param expirationTime expiration time + * @param timeUnit time unit + * @return true - operation done; false - operation failure + */ + Boolean set(String key, List list, Long expirationTime, TimeUnit timeUnit); + + /** + * Set key and value in Redis + * + * @param key key + * @param value value + * @return true - operation done; false - operation failure + */ + Boolean set(String key, String value); + + /** + * Set key and list in Redis + * + * @param key key + * @param list list + * @return true - operation done; false - operation failure + */ + Boolean set(String key, List list); + + /** + * Make key expire + * + * @param key key + * @param expirationTime expiration time + * @param timeUnit time unit + * @return true - operation done; false - operation failure + */ + Boolean expire(String key, long expirationTime, TimeUnit timeUnit); + + /** + * Get expiration time by key + * + * @param key key + * @param timeUnit time unit + * @return expiration time. Null when used in pipeline / transaction. + */ + Long getExpire(String key, TimeUnit timeUnit); + + /** + * Get value by key + * + * @param key key + * @return null when key does not exist or used in pipeline / transaction. + */ + String get(String key); + + /** + * Get list by key + * + * @param key key + * @param clazz type + * @return null when key does not exist or used in pipeline / transaction. + */ + List get(String key, Class clazz); + + /** + * Delete by key + * + * @param key key + * @return the number of keys that were removed. Null when used in pipeline / transaction. + */ + Long delete(String key); + + /** + * Delete by keys + * + * @param keys key list + * @return the number of keys that were removed. Null when used in pipeline / transaction. + */ + Long delete(Collection keys); +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/service/impl/CommonServiceImpl.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/service/impl/CommonServiceImpl.java new file mode 100644 index 00000000..41669edd --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/service/impl/CommonServiceImpl.java @@ -0,0 +1,124 @@ +package com.jmsoftware.authcenter.universal.service.impl; + +import com.jmsoftware.authcenter.universal.aspect.ValidateArgument; +import com.jmsoftware.authcenter.universal.configuration.ProjectProperty; +import com.jmsoftware.authcenter.universal.domain.ValidationTestPayload; +import com.jmsoftware.authcenter.universal.service.CommonService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.validation.Valid; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + *

CommonServiceImpl

+ *

+ * Change description here. + * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 2/4/20 11:16 AM + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CommonServiceImpl implements CommonService { + private final ProjectProperty projectProperty; + + @Override + public Map getApplicationInfo() { + var map = new HashMap(16); + var fieldsInfo = getFieldsInfo(projectProperty); + fieldsInfo.forEach(fieldInfo -> { + var type = fieldInfo.get("type"); + if ("class java.lang.String".equals(type)) { + map.put((String) fieldInfo.get("name"), fieldInfo.get("value")); + } + }); + return map; + } + + @Override + @ValidateArgument + public void validateObject(@Valid ValidationTestPayload payload) { + log.info("Validation passed! {}", payload); + } + + /** + * Gets field value by name. + * + * @param fieldName the field name + * @param object the object + * @return the field value by name + * @see Java 中遍历一个对象的所有属性 + */ + private Object getFieldValueByName(String fieldName, Object object) { + try { + String firstLetter = fieldName.substring(0, 1).toUpperCase(); + String getter = "get" + firstLetter + fieldName.substring(1); + Method method = object.getClass().getMethod(getter); + return method.invoke(object); + } catch (Exception e) { + log.error("Can't get field's value by name! Cause: {}", e.getMessage()); + return null; + } + } + + /** + * Get filed name string [ ]. + * + * @param o the o + * @return the string [ ] + * @see Java 中遍历一个对象的所有属性 + */ + private String[] getFiledName(Object o) { + Field[] fields = o.getClass().getDeclaredFields(); + String[] fieldNames = new String[fields.length]; + for (int i = 0; i < fields.length; i++) { + log.info("fields[i].getType(): {}", fields[i].getType()); + fieldNames[i] = fields[i].getName(); + } + return fieldNames; + } + + /** + * Get Fields Info + * + * @param o the o + * @return the fields info + * @see Java 中遍历一个对象的所有属性 + */ + private List> getFieldsInfo(Object o) { + Field[] fields = o.getClass().getDeclaredFields(); + var arrayList = new ArrayList>(); + for (Field field : fields) { + var infoMap = new HashMap(16); + infoMap.put("type", field.getType().toString()); + infoMap.put("name", field.getName()); + infoMap.put("value", getFieldValueByName(field.getName(), o)); + arrayList.add(infoMap); + } + return arrayList; + } + + /** + * Get Filed Values + * + * @param o the o + * @return the object [ ] + * @see Java 中遍历一个对象的所有属性 + */ + public Object[] getFiledValues(Object o) { + String[] fieldNames = this.getFiledName(o); + Object[] value = new Object[fieldNames.length]; + for (int i = 0; i < fieldNames.length; i++) { + value[i] = this.getFieldValueByName(fieldNames[i], o); + } + return value; + } +} diff --git a/auth-center/src/main/java/com/jmsoftware/authcenter/universal/service/impl/RedisServiceImpl.java b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/service/impl/RedisServiceImpl.java new file mode 100644 index 00000000..561a5298 --- /dev/null +++ b/auth-center/src/main/java/com/jmsoftware/authcenter/universal/service/impl/RedisServiceImpl.java @@ -0,0 +1,110 @@ +package com.jmsoftware.authcenter.universal.service.impl; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.json.JSONUtil; +import com.jmsoftware.authcenter.universal.service.RedisService; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.redis.connection.RedisStringCommands; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.stereotype.Service; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + *

RedisServiceImpl

+ *

Redis service implementation for Redis useful operations

+ * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 2019-07-05 09:39 + **/ +@Service +public class RedisServiceImpl implements RedisService { + private final RedisTemplate redisTemplate; + + public RedisServiceImpl(@Qualifier("redisFactory") RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + @Override + public Boolean set(String key, String value, Long expirationTime, TimeUnit timeUnit) { + return redisTemplate.execute((RedisCallback) connection -> { + var serializer = redisTemplate.getStringSerializer(); + var result = connection.set(Objects.requireNonNull(serializer.serialize(key)), + Objects.requireNonNull(serializer.serialize(value)), + Expiration.from(expirationTime, timeUnit), + RedisStringCommands.SetOption.upsert()); + return ObjectUtil.isNotNull(result) ? result : false; + }); + } + + @Override + public Boolean set(String key, List list, Long expirationTime, TimeUnit timeUnit) { + var value = JSONUtil.toJsonStr(list); + return set(key, value, expirationTime, timeUnit); + } + + @Override + public Boolean set(String key, String value) { + return redisTemplate.execute((RedisCallback) connection -> { + var serializer = redisTemplate.getStringSerializer(); + var result = connection.set(Objects.requireNonNull(serializer.serialize(key)), + Objects.requireNonNull(serializer.serialize(value))); + return ObjectUtil.isNotNull(result) ? result : false; + }); + } + + @Override + public Boolean set(String key, List list) { + var value = JSONUtil.toJsonStr(list); + return set(key, value); + } + + @Override + public Boolean expire(String key, long expirationTime, TimeUnit timeUnit) { + var result = redisTemplate.expire(key, expirationTime, timeUnit); + return ObjectUtil.isNotNull(result) ? result : false; + } + + @Override + public Long getExpire(String key, TimeUnit timeUnit) { + return redisTemplate.getExpire(key, timeUnit); + } + + @Override + public String get(String key) { + return redisTemplate.execute((RedisCallback) connection -> { + var serializer = redisTemplate.getStringSerializer(); + var bytes = connection.get(Objects.requireNonNull(serializer.serialize(key))); + return serializer.deserialize(bytes); + }); + } + + @Override + public List get(String key, Class clazz) { + var json = get(key); + if (StringUtils.isNotBlank(json)) { + return JSONUtil.toList(JSONUtil.parseArray(json), clazz); + } + return null; + } + + @Override + public Long delete(String key) { + return redisTemplate.execute((RedisCallback) connection -> { + var serializer = redisTemplate.getStringSerializer(); + return connection.del(serializer.serialize(key)); + }); + } + + @Override + public Long delete(Collection keys) { + return redisTemplate.delete(keys); + } +} diff --git a/auth-center/src/main/resources/application-development-docker.yml b/auth-center/src/main/resources/application-development-docker.yml new file mode 100644 index 00000000..f513aad3 --- /dev/null +++ b/auth-center/src/main/resources/application-development-docker.yml @@ -0,0 +1,15 @@ +eureka: + instance: + leaseRenewalIntervalInSeconds: 10 + health-check-url-path: /actuator/health + metadata-map: + # needed to trigger info and endpoint update after restart + startup: ${random.int} + client: + registryFetchIntervalSeconds: 5 + serviceUrl: + defaultZone: http://localhost:8760/eureka/ + +spring: + zipkin: + base-url: http://localhost:9411 diff --git a/auth-center/src/main/resources/application-development-local.yml b/auth-center/src/main/resources/application-development-local.yml new file mode 100644 index 00000000..c0035892 --- /dev/null +++ b/auth-center/src/main/resources/application-development-local.yml @@ -0,0 +1,42 @@ +eureka: + instance: + leaseRenewalIntervalInSeconds: 10 + health-check-url-path: /actuator/health + metadata-map: + # needed to trigger info and endpoint update after restart + startup: ${random.int} + client: + registryFetchIntervalSeconds: 5 + serviceUrl: + defaultZone: http://localhost:8760/eureka/ + +spring: + zipkin: + base-url: http://localhost:9411 + datasource: + name: exercise_dictionary + driver-class-name: com.mysql.cj.jdbc.Driver + type: com.alibaba.druid.pool.DruidDataSource + url: jdbc:mysql://localhost:3306/exercise_dictionary?useSSL=true&useUnicode=true + username: root + password: jm@mysql + devtools: + restart: + enabled: true + redis: + database: 0 + host: localhost + port: 6379 + password: 123456 + timeout: 10000ms + lettuce: + pool: + max-active: 20 + max-idle: 10 + max-wait: -1ms + min-idle: 0 + +logging: + # Configure logging level for SFTP/JSCH + level: + com.jcraft.jsch: INFO diff --git a/auth-center/src/main/resources/application-production.yml b/auth-center/src/main/resources/application-production.yml new file mode 100644 index 00000000..f513aad3 --- /dev/null +++ b/auth-center/src/main/resources/application-production.yml @@ -0,0 +1,15 @@ +eureka: + instance: + leaseRenewalIntervalInSeconds: 10 + health-check-url-path: /actuator/health + metadata-map: + # needed to trigger info and endpoint update after restart + startup: ${random.int} + client: + registryFetchIntervalSeconds: 5 + serviceUrl: + defaultZone: http://localhost:8760/eureka/ + +spring: + zipkin: + base-url: http://localhost:9411 diff --git a/auth-center/src/main/resources/application-stage.yml b/auth-center/src/main/resources/application-stage.yml new file mode 100644 index 00000000..f513aad3 --- /dev/null +++ b/auth-center/src/main/resources/application-stage.yml @@ -0,0 +1,15 @@ +eureka: + instance: + leaseRenewalIntervalInSeconds: 10 + health-check-url-path: /actuator/health + metadata-map: + # needed to trigger info and endpoint update after restart + startup: ${random.int} + client: + registryFetchIntervalSeconds: 5 + serviceUrl: + defaultZone: http://localhost:8760/eureka/ + +spring: + zipkin: + base-url: http://localhost:9411 diff --git a/auth-center/src/main/resources/application-test.yml b/auth-center/src/main/resources/application-test.yml new file mode 100644 index 00000000..f513aad3 --- /dev/null +++ b/auth-center/src/main/resources/application-test.yml @@ -0,0 +1,15 @@ +eureka: + instance: + leaseRenewalIntervalInSeconds: 10 + health-check-url-path: /actuator/health + metadata-map: + # needed to trigger info and endpoint update after restart + startup: ${random.int} + client: + registryFetchIntervalSeconds: 5 + serviceUrl: + defaultZone: http://localhost:8760/eureka/ + +spring: + zipkin: + base-url: http://localhost:9411 diff --git a/auth-center/src/main/resources/application.yml b/auth-center/src/main/resources/application.yml new file mode 100644 index 00000000..f6e80952 --- /dev/null +++ b/auth-center/src/main/resources/application.yml @@ -0,0 +1,87 @@ +server: + port: @port@ + # servlet: + # context-path: /@project.artifactId@-@envAlias@ + tomcat: + uri-encoding: @project.build.sourceEncoding@ + +spring: + application: + name: @project.artifactId@ + profiles: + active: @env@ + mvc: + throw-exception-if-no-handler-found: true + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + sleuth: + sampler: + probability: 1.0 + servlet: + multipart: + # `location` specifies the directory where uploaded files will be stored. When not specified, + # a temporary directory will be used. ATTENTION: it may differ due to OS. + location: /Users/johnny/Documents/@project.artifactId@/temprary-file + # `max-file-size` specifies the maximum size permitted for uploaded files. The default is 1MB. We set it as 64 MB. + max-file-size: 64MB + # `max-request-size` specifies the maximum size allowed for multipart/form-data requests. The default is 10MB. + max-request-size: 70MB + # `file-size-threshold` specifies the size threshold after which files will be written to disk. + # The default is 0. We set it as 0 too. + file-size-threshold: 0 + +feign: + client: + config: + default: + connectTimeout: 5000 + readTimeout: 10000 + +management: + endpoints: + web: + exposure: + include: "*" + endpoint: + health: + show-details: ALWAYS + +mybatis-plus: + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl + # mapper-locations should start with `classpath*` prefix + # when project is based on Maven multi-module to load XML mapper in different jar + mapper-locations: classpath*:/mapper/**/*Mapper.xml + global-config: + db-config: + table-prefix: t_ + +logging: + config: classpath:configuration/logback/logback-@env@.xml + # Configure logging level of project as DEBUG to enable SQL logging and other logging. + level: + com.jmsoftware: DEBUG + +project: + property: + base-package: com.jmsoftware.authcenter + context-path: + group-id: @project.groupId@ + artifact-id: @project.artifactId@ + project-artifact-id: @project.artifactId@ + version: @project.version@ + description: @project.description@ + jdk-version: @java.version@ + environment: @env@ + environment-alias: @envAlias@ + url: @project.url@ + inception-year: @inceptionYear@ + organization-name: @project.organization.name@ + organization-url: @project.organization.url@ + issue-management-system: @project.issueManagement.system@ + issue-management-url: @project.issueManagement.url@ + developer-name: @developerName@ + developer-email: @developerEmail@ + developer-url: @developerUrl@ diff --git a/auth-center/src/main/resources/banner.txt b/auth-center/src/main/resources/banner.txt new file mode 100644 index 00000000..d0832b7a --- /dev/null +++ b/auth-center/src/main/resources/banner.txt @@ -0,0 +1,26 @@ +${AnsiStyle.BOLD}${AnsiColor.BRIGHT_GREEN} + ___ __ __ ______ __ + / | __ __/ /_/ /_ / ____/__ ____ / /____ _____ + / /| |/ / / / __/ __ \ / / / _ \/ __ \/ __/ _ \/ ___/ + / ___ / /_/ / /_/ / / / / /___/ __/ / / / /_/ __/ / +/_/ |_\__,_/\__/_/ /_/ \____/\___/_/ /_/\__/\___/_/ +${AnsiStyle.BOLD}Exercise MIS :: Powered by Spring Boot ::${spring-boot.formatted-version} +${AnsiColor.CYAN}Author: Johnny Miller (鍾俊), email: johnnysviva@outlook.com +${AnsiStyle.NORMAL}${AnsiColor.MAGENTA}http://patorjk.com/software/taag/#p=display&f=Slant&t=Auth%20Center +${AnsiColor.BRIGHT_BLACK} +-------------------------------------------Font Info------------------------------------------- +Slant by Glenn Chappell 3/93 -- based on Standard +Includes ISO Latin-1 +figlet release 2.1 -- 12 Aug 1994 +Permission is hereby given to modify this font, as long as the +modifier's name is placed on a comment line. + +Modified by Paul Burton 12/96 to include new parameter +supported by FIGlet and FIGWin. May also be slightly modified for better use +of new full-width/kern/smush alternatives, but default output is NOT changed. + +------------------------------------------Banner Info------------------------------------------ +Banner generated by Text ASCII Art Generator. +A web app that lets you type in large ASCII Art text lettering. +This can create art you can put in your email signature, on your webpage, etc etc. +More at http://patorjk.com/software/taag/ diff --git a/auth-center/src/main/resources/configuration/.empty b/auth-center/src/main/resources/configuration/.empty new file mode 100644 index 00000000..e69de29b diff --git a/auth-center/src/main/resources/configuration/logback/logback-base.xml b/auth-center/src/main/resources/configuration/logback/logback-base.xml new file mode 100644 index 00000000..fcf44261 --- /dev/null +++ b/auth-center/src/main/resources/configuration/logback/logback-base.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + UTF-8 + + + + + + + ALL + ACCEPT + ACCEPT + + + + ${FILE_LOG_PATTERN} + UTF-8 + + + + + + + ${LOG_HOME}/${PROJECT_ARTIFACT_Id}.%d{yyyy-MM-dd}.%i.log.gz + + 5MB + 7 + 1GB + + + diff --git a/auth-center/src/main/resources/configuration/logback/logback-development-docker.xml b/auth-center/src/main/resources/configuration/logback/logback-development-docker.xml new file mode 100644 index 00000000..f38e6c2d --- /dev/null +++ b/auth-center/src/main/resources/configuration/logback/logback-development-docker.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/auth-center/src/main/resources/configuration/logback/logback-development-local.xml b/auth-center/src/main/resources/configuration/logback/logback-development-local.xml new file mode 100644 index 00000000..f38e6c2d --- /dev/null +++ b/auth-center/src/main/resources/configuration/logback/logback-development-local.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/auth-center/src/main/resources/configuration/logback/logback-production.xml b/auth-center/src/main/resources/configuration/logback/logback-production.xml new file mode 100644 index 00000000..f38e6c2d --- /dev/null +++ b/auth-center/src/main/resources/configuration/logback/logback-production.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/auth-center/src/main/resources/configuration/logback/logback-stage.xml b/auth-center/src/main/resources/configuration/logback/logback-stage.xml new file mode 100644 index 00000000..f38e6c2d --- /dev/null +++ b/auth-center/src/main/resources/configuration/logback/logback-stage.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/auth-center/src/main/resources/configuration/logback/logback-test.xml b/auth-center/src/main/resources/configuration/logback/logback-test.xml new file mode 100644 index 00000000..f1916a0f --- /dev/null +++ b/auth-center/src/main/resources/configuration/logback/logback-test.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/auth-center/src/main/resources/static/home.html b/auth-center/src/main/resources/static/home.html new file mode 100644 index 00000000..e7599316 --- /dev/null +++ b/auth-center/src/main/resources/static/home.html @@ -0,0 +1,78 @@ + + + + + + UNSET + + + + +
+
+ Hello, World! + + Welcome to {{ projectArtifactId }}@{{ version }}! + + + + {{ applicationName }} + +
+
+ Create New Person +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + + + + + + + + + + + + + +
NameAgeSexDelete
{{ person.name }}{{ person.age }}{{ person.sex }} + +
+ This page is provided by + keepfool. + +
+ + + + diff --git a/auth-center/src/main/resources/static/icon/favicon.ico b/auth-center/src/main/resources/static/icon/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0798d38f0591873e5dce2ee2a9ae3c0a27b23682 GIT binary patch literal 7185 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

Cgoi&0Op~bNt8DuafknShTQnC|~IRX&giubFs z3E9x*roKsWB0!vgd_PeF=`~b-d zFc;uQii-3?5bgAHJEMil?-43LH8`4(sm+Q;(-9q%2V)wEJVK&Q7Xq|K=|`fH)>_3%)}y39`O@ z;PW2`_4{7aM4+J$If+C;#06(X!CC58K--tUJeQn)K+6F&{)2%3S+Pf?AO>%U|DT6f zPEpXSFk|~JY94wZ5A2`YmPqu}Q5SR#DnCuI`>;v=03a@Y6V-sHc%6k|SiNq}|4Gq< z^+1Z-jwDY(MCzL#GHHoD3!BXRPnkmQ{4_s!9qi0#3Z@49@_^wc9h$V9IO3Q^0O{QS e(@ijSMm&|{g<#Tr)Y{o4u-<(mtp-i!`2PbAOMgcI literal 0 HcmV?d00001 diff --git a/auth-center/src/main/resources/static/image/muscle-and-fitness-server-social-image.png b/auth-center/src/main/resources/static/image/muscle-and-fitness-server-social-image.png new file mode 100644 index 0000000000000000000000000000000000000000..83786ad82e1796d69b4070642ef82f8fe5635f2b GIT binary patch literal 70913 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%0D+n7pcq=JwxbCAn5JsjgvfbHb%`|} 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� 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 j<@hP-|NW label { + display: inline-block; + width: 10rem; + text-align: right; +} + +.form-group > input, +.form-group > select { + display: inline-block; + height: 2.5rem; + line-height: 2.5rem; +} + +.text-center { + text-align: center; +} + +.pagination { + display: inline-block; + padding-left: 0; + margin: 21px 0; + border-radius: 3px; +} + +.pagination > li { + display: inline; +} + +.pagination > li > a { + position: relative; + float: left; + padding: 6px 12px; + line-height: 1.5; + text-decoration: none; + color: #009a61; + background-color: #fff; + border: 1px solid #ddd; + margin-left: -1px; + list-style: none; +} + +.pagination > li > a:hover { + background-color: #eee; +} + +.pagination .active { + color: #fff; + background-color: #009a61; + border-left: none; + border-right: none; +} + +.pagination .active:hover { + background: #009a61; + cursor: default; +} + +.pagination > li:first-child > a .p { + border-bottom-left-radius: 3px; + border-top-left-radius: 3px; +} + +.pagination > li:last-child > a { + border-bottom-right-radius: 3px; + border-top-right-radius: 3px; +} + +.greeting-box { + background-color: firebrick; + max-width: 700px; + padding: 20px; + border-radius: 8px; + margin: 48px auto 20px; +} + +.slide-in-bck-center { + animation: slide-in-bck-center 1.2s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; +} + +@keyframes slide-in-bck-center { + 0% { + transform: translateZ(600px); + opacity: 0; + } + 100% { + transform: translateZ(0); + opacity: 1; + } +} + +.greeting-text { + font-size: xx-large; + text-align: center; + color: ivory; + display: block; +} + +.application-name { + display: inline-block; + font-weight: bold; + width: auto; + font-size: 48px; + color: rgb(7, 156, 58); + position: relative; + left: 60px; + top: 200px; + cursor: default; +} + +.social-image { + width: 600px; + border-radius: 8px; +} + +.greeting-title { + text-align: center; + display: block; + font-size: 48px; + font-weight: bold; + color: ivory; + text-shadow: black 2px 2px 6px; +} + +.server-name { + color: gold; +} + +.go-to { + display: inline-block; + margin-bottom: 6px; +} diff --git a/auth-center/src/main/resources/static/styles/video.css b/auth-center/src/main/resources/static/styles/video.css new file mode 100644 index 00000000..341a63f5 --- /dev/null +++ b/auth-center/src/main/resources/static/styles/video.css @@ -0,0 +1,136 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box +} + +html { + font-size: 12px; + font-family: Ubuntu, simHei, sans-serif; + font-weight: 400 +} + +body { + font-size: 1rem +} + +table, +td, +th { + border-collapse: collapse; + border-spacing: 0 +} + +table { + width: 100% +} + +td, +th { + border: 1px solid #bcbcbc; + padding: 5px 10px +} + +th { + background: #42b983; + font-size: 1.2rem; + font-weight: 400; + color: #fff; + cursor: pointer +} + +tr:nth-of-type(odd) { + background: #fff +} + +tr:nth-of-type(even) { + background: #eee +} + +fieldset { + border: 1px solid #BCBCBC; + padding: 15px; +} + +input { + outline: none +} + +input[type=text] { + border: 1px solid #ccc; + padding: .5rem .3rem; +} + +input[type=text]:focus { + border-color: #42b983; +} + +button { + outline: none; + padding: 5px 8px; + color: #fff; + border: 1px solid #BCBCBC; + border-radius: 3px; + background-color: #009A61; + cursor: pointer; +} + +button:hover { + opacity: 0.8; +} + +#app { + margin: 0 auto; + max-width: 640px; + -webkit-perspective: 500px; +} + +.greeting-box { + background-color: firebrick; + max-width: 700px; + padding: 20px; + border-radius: 8px; + margin: 48px auto 20px; +} + +.slide-in-bck-center { + animation: slide-in-bck-center 1.2s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; +} + +@keyframes slide-in-bck-center { + 0% { + transform: translateZ(600px); + opacity: 0; + } + 100% { + transform: translateZ(0); + opacity: 1; + } +} + +.greeting-text { + font-size: xx-large; + text-align: center; + color: ivory; +} + +.social-image { + width: 600px; + border-radius: 8px; +} + +.greeting-title { + text-align: center; + display: block; + font-size: 48px; + text-shadow: black 2px 2px 6px; +} + +.server-name { + color: gold; +} + +.go-to { + display: inline-block; + margin-bottom: 6px; +} diff --git a/auth-center/src/main/resources/static/video.html b/auth-center/src/main/resources/static/video.html new file mode 100644 index 00000000..9a186caa --- /dev/null +++ b/auth-center/src/main/resources/static/video.html @@ -0,0 +1,25 @@ + + + + + + ExRx.net Crawler Server + + + + +
+
+ + Hello, World! + Welcome to {{ appInfo.projectArtifactId }}@{{ appInfo.version }}! + + +
+
+ + + + diff --git a/auth-center/src/test/java/com/jmsoftware/authcenter/AuthCenterApplicationTests.java b/auth-center/src/test/java/com/jmsoftware/authcenter/AuthCenterApplicationTests.java new file mode 100644 index 00000000..796eac29 --- /dev/null +++ b/auth-center/src/test/java/com/jmsoftware/authcenter/AuthCenterApplicationTests.java @@ -0,0 +1,13 @@ +package com.jmsoftware.authcenter; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AuthCenterApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/docker-compose.development-docker.yml b/docker-compose.development-docker.yml index 9306ea70..c678450b 100644 --- a/docker-compose.development-docker.yml +++ b/docker-compose.development-docker.yml @@ -9,12 +9,24 @@ services: ports: - "3306:3306" volumes: - - /Users/johnny/docker-file-mapping/mysql-8.0.19-1debian9:/var/lib/mysql + - /Users/johnny/docker-file-mapping/mysql-m&f:/var/lib/mysql command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci networks: exrx_net_crawler_network: ipv4_address: 172.16.238.10 + redis-server: + container_name: redis-server-m&f-dev_dkr + image: redis:latest + ports: + - "6379:6379" + restart: always + volumes: + - /Users/johnny/docker-file-mapping/redis-m&f/data:/data + - /Users/johnny/docker-file-mapping/redis-m&f/redis.conf:/usr/local/etc/redis/redis.conf + command: "redis-server /usr/local/etc/redis/redis.conf" + ipv4_address: 172.16.238.11 + atmoz-sftp-server: container_name: atmoz-sftp-server-m&f-dev_dkr image: atmoz/sftp:latest @@ -26,7 +38,7 @@ services: command: "johnny:atmoz@sftp:::upload" networks: exrx_net_crawler_network: - ipv4_address: 172.16.238.11 + ipv4_address: 172.16.238.12 service-registry: container_name: service-registry-dev_dkr @@ -37,7 +49,7 @@ services: - /Users/johnny/docker-file-mapping/service-registry:/logs-dev_dkr networks: exrx_net_crawler_network: - ipv4_address: 172.11.239.12 + ipv4_address: 172.11.239.13 spring-boot-admin: container_name: spring-boot-admin-dev_dkr @@ -48,7 +60,7 @@ services: - /Users/johnny/docker-file-mapping/spring-boot-admin:/logs-dev_dkr networks: exrx_net_crawler_network: - ipv4_address: 172.11.239.13 + ipv4_address: 172.11.239.14 zipkin: container_name: openzipkin/zipkin @@ -71,29 +83,40 @@ services: # - 9410:9410 networks: exrx_net_crawler_network: - ipv4_address: 172.11.239.14 + ipv4_address: 172.11.239.15 + + auth-center: + container_name: auth-center-dev_dkr + image: "ijohnnymiller/auth-center-dev_dkr:${TAG}" + ports: + - "8770:8770" + volumes: + - /Users/johnny/docker-file-mapping/auth-center:/logs-dev_dkr + networks: + exrx_net_crawler_network: + ipv4_address: 172.11.239.16 exercise-mis: container_name: exercise-mis-dev_dkr image: "ijohnnymiller/exercise-mis-dev_dkr:${TAG}" ports: - - "8770:8770" + - "8771:8771" volumes: - /Users/johnny/docker-file-mapping/exercise-mis:/logs-dev_dkr networks: exrx_net_crawler_network: - ipv4_address: 172.11.239.15 + ipv4_address: 172.11.239.16 muscle-mis: container_name: muscle-mis-dev_dkr image: "ijohnnymiller/muscle-mis-dev_dkr:${TAG}" ports: - - "8771:8771" + - "8772:8772" volumes: - /Users/johnny/docker-file-mapping/muscle-mis:/logs-dev_dkr networks: exrx_net_crawler_network: - ipv4_address: 172.11.239.16 + ipv4_address: 172.11.239.17 networks: exrx_net_crawler_network: diff --git a/exercise-mis/pom.xml b/exercise-mis/pom.xml index 95b8e3b6..7f97e403 100644 --- a/exercise-mis/pom.xml +++ b/exercise-mis/pom.xml @@ -5,12 +5,11 @@ exercise-mis - 0.0.1-SNAPSHOT Exercise MIS Exercise Management Information Service. 11 - 8770 + 8771 com.jmsoftware diff --git a/gateway/pom.xml b/gateway/pom.xml index f4a1ac0a..01452192 100644 --- a/gateway/pom.xml +++ b/gateway/pom.xml @@ -90,6 +90,10 @@ org.springframework.cloud spring-cloud-starter-zipkin + + org.springframework.cloud + spring-cloud-security + com.jmsoftware diff --git a/gateway/src/main/java/com/jmsoftware/gateway/universal/configuration/SecurityConfiguration.java b/gateway/src/main/java/com/jmsoftware/gateway/universal/configuration/SecurityConfiguration.java new file mode 100644 index 00000000..ea8efa97 --- /dev/null +++ b/gateway/src/main/java/com/jmsoftware/gateway/universal/configuration/SecurityConfiguration.java @@ -0,0 +1,42 @@ +package com.jmsoftware.gateway.universal.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; + +/** + *

SecurityConfiguration

+ *

+ * Change description here. + * + * @author Johnny Miller (鍾俊), email: johnnysviva@outlook.com + * @date 3/12/20 10:13 AM + **/ +@EnableWebFluxSecurity +public class SecurityConfiguration { + private static final String[] excludedAuth = { + "/auth/login", + "/auth/logout", + "/health", + "/api/socket/**" + }; + + @Bean + SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception { + http.authorizeExchange() + .pathMatchers(excludedAuth).permitAll() + .pathMatchers(HttpMethod.OPTIONS).permitAll() + .anyExchange().authenticated() + .and() + .httpBasic() + .and() + // 启动页面表单登陆,spring security 内置了一个登陆页面/login + .formLogin() + // 必须支持跨域 + .and().csrf().disable() + .logout().disable(); + return http.build(); + } +} diff --git a/gateway/src/main/resources/application-development-local.yml b/gateway/src/main/resources/application-development-local.yml index 55095458..c9b179b6 100644 --- a/gateway/src/main/resources/application-development-local.yml +++ b/gateway/src/main/resources/application-development-local.yml @@ -9,3 +9,9 @@ eureka: registryFetchIntervalSeconds: 5 serviceUrl: defaultZone: http://localhost:8760/eureka/ + +spring: + security: + user: + name: admin + password: admin diff --git a/muscle-mis/pom.xml b/muscle-mis/pom.xml index 2b62d277..e4f00549 100644 --- a/muscle-mis/pom.xml +++ b/muscle-mis/pom.xml @@ -9,7 +9,7 @@ Muscle Management Information Service. 11 - 8771 + 8772 com.jmsoftware diff --git a/pom.xml b/pom.xml index 55bf4437..28d4d5e8 100644 --- a/pom.xml +++ b/pom.xml @@ -32,6 +32,7 @@ service-registry spring-boot-admin gateway + auth-center exercise-mis muscle-mis @@ -67,6 +68,11 @@ gateway 0.0.1-SNAPSHOT + + com.jmsoftware + auth-center + 0.0.1-SNAPSHOT + com.jmsoftware exercise-mis