diff --git a/common/mall-spring-boot-starter-security/pom.xml b/common/mall-spring-boot-starter-security/pom.xml index 42c3d4bf7..89cf3eb32 100644 --- a/common/mall-spring-boot-starter-security/pom.xml +++ b/common/mall-spring-boot-starter-security/pom.xml @@ -17,7 +17,6 @@ cn.iocoder.mall system-rpc-api 1.0-SNAPSHOT - true @@ -38,7 +37,6 @@ org.apache.dubbo dubbo - true diff --git a/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/config/CommonSecurityAutoConfiguration.java b/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/config/CommonSecurityAutoConfiguration.java index 9bf902068..b34c5b468 100644 --- a/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/config/CommonSecurityAutoConfiguration.java +++ b/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/config/CommonSecurityAutoConfiguration.java @@ -1,16 +1,38 @@ package cn.iocoder.mall.security.config; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import cn.iocoder.mall.security.core.interceptor.AccountAuthInterceptor; +import cn.iocoder.mall.web.config.CommonWebAutoConfiguration; +import cn.iocoder.mall.web.core.constant.CommonMallConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration +@AutoConfigureAfter(CommonWebAutoConfiguration.class) // 在 CommonWebAutoConfiguration 之后自动配置,保证过滤器的顺序 @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) -@ConditionalOnClass(name = {"cn.iocoder.mall.system.rpc.api.systemlog.SystemLogRPC", "org.apache.dubbo.config.annotation.Reference"}) public class CommonSecurityAutoConfiguration implements WebMvcConfigurer { - // ========== 拦截器相关 ========== + private Logger logger = LoggerFactory.getLogger(getClass()); + // ========== 拦截器相关 ========== + @Bean + @ConditionalOnMissingBean(AccountAuthInterceptor.class) + public AccountAuthInterceptor accountAuthInterceptor() { + return new AccountAuthInterceptor(); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + // AccountAuthInterceptor 拦截器 + registry.addInterceptor(this.accountAuthInterceptor()) + .addPathPatterns(CommonMallConstants.ROOT_PATH_ADMIN + "/**", CommonMallConstants.ROOT_PATH_USER + "/**"); + logger.info("[addInterceptors][加载 AccountAuthInterceptor 拦截器完成]"); + } } diff --git a/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/annotation/RequiresLogin.java b/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/annotation/RequiresLogin.java new file mode 100644 index 000000000..a85b10b6c --- /dev/null +++ b/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/annotation/RequiresLogin.java @@ -0,0 +1,16 @@ +package cn.iocoder.mall.security.core.annotation; + +import java.lang.annotation.*; + +/** + * 要求用户登录注解。通过将该注解添加到 Controller 上,会自动校验用户是否登陆。 + * + * 默认请求下,用户访问的 API 接口,无需登陆。主要的考虑是, + * 1. 需要用户登陆的接口,本身会获取在线用户的编号。如果不添加 @RequiresLogin 注解就会报错。 + * 2. 大多数情况下,用户的 API 接口无需登陆。 + */ +@Documented +@Target({ElementType.METHOD}) // 暂时不支持 ElementType.TYPE ,因为没有场景 +@Retention(RetentionPolicy.RUNTIME) +public @interface RequiresLogin { +} diff --git a/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/annotation/RequiresPermissions.java b/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/annotation/RequiresPermissions.java new file mode 100644 index 000000000..ae304a2e4 --- /dev/null +++ b/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/annotation/RequiresPermissions.java @@ -0,0 +1,22 @@ +package cn.iocoder.mall.security.core.annotation; + +import java.lang.annotation.*; + +/** + * 参考 Shiro @RequiresPermissions 设计 http://shiro.apache.org/static/1.3.2/apidocs/org/apache/shiro/authz/annotation/RequiresPermissions.html + * + * 通过将该注解添加到 Controller 的方法上,进行授权鉴定 + */ +@Documented +@Target({ElementType.METHOD}) // 暂时不支持 ElementType.TYPE ,因为没有场景 +@Retention(RetentionPolicy.RUNTIME) +public @interface RequiresPermissions { + + /** + * 当有多个标识时,必须全部拥有权限,才可以操作 + * + * @return 权限标识数组 + */ + String[] value(); + +} diff --git a/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/context/AdminSecurityContext.java b/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/context/AdminSecurityContext.java new file mode 100644 index 000000000..625832fbd --- /dev/null +++ b/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/context/AdminSecurityContext.java @@ -0,0 +1,28 @@ +package cn.iocoder.mall.security.core.context; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.Set; + +/** + * Security 上下文 + */ +@Data +@Accessors(chain = true) +public class AdminSecurityContext { + + /** + * 管理员编号 + */ + private Integer adminId; + /** + * 管理员账号 + */ + private String username; + /** + * 拥有的角色编号 + */ + private Set roleIds; + +} diff --git a/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/context/AdminSecurityContextHolder.java b/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/context/AdminSecurityContextHolder.java new file mode 100644 index 000000000..17ca066a5 --- /dev/null +++ b/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/context/AdminSecurityContextHolder.java @@ -0,0 +1,30 @@ +package cn.iocoder.mall.security.core.context; + +/** + * {@link AdminSecurityContext} Holder + * + * 参考 spring security 的 ThreadLocalSecurityContextHolderStrategy 类,简单实现。 + */ +public class AdminSecurityContextHolder { + + private static final ThreadLocal SECURITY_CONTEXT = new ThreadLocal<>(); + + public static void setContext(AdminSecurityContext context) { + SECURITY_CONTEXT.set(context); + } + + public static AdminSecurityContext getContext() { + AdminSecurityContext ctx = SECURITY_CONTEXT.get(); + // 为空时,设置一个空的进去 + if (ctx == null) { + ctx = new AdminSecurityContext(); + SECURITY_CONTEXT.set(ctx); + } + return ctx; + } + + public static void clear() { + SECURITY_CONTEXT.remove(); + } + +} diff --git a/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/context/UserSecurityContext.java b/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/context/UserSecurityContext.java new file mode 100644 index 000000000..ad7a79040 --- /dev/null +++ b/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/context/UserSecurityContext.java @@ -0,0 +1,18 @@ +package cn.iocoder.mall.security.core.context; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * User Security 上下文 + */ +@Data +@Accessors(chain = true) +public class UserSecurityContext { + + /** + * 用户编号 + */ + private Integer userId; + +} diff --git a/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/context/UserSecurityContextHolder.java b/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/context/UserSecurityContextHolder.java new file mode 100644 index 000000000..7c6d9e92a --- /dev/null +++ b/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/context/UserSecurityContextHolder.java @@ -0,0 +1,30 @@ +package cn.iocoder.mall.security.core.context; + +/** + * {@link UserSecurityContext} Holder + * + * 参考 spring security 的 ThreadLocalSecurityContextHolderStrategy 类,简单实现。 + */ +public class UserSecurityContextHolder { + + private static final ThreadLocal SECURITY_CONTEXT = new ThreadLocal(); + + public static void setContext(UserSecurityContext context) { + SECURITY_CONTEXT.set(context); + } + + public static UserSecurityContext getContext() { + UserSecurityContext ctx = SECURITY_CONTEXT.get(); + // 为空时,设置一个空的进去 + if (ctx == null) { + ctx = new UserSecurityContext(); + SECURITY_CONTEXT.set(ctx); + } + return ctx; + } + + public static void clear() { + SECURITY_CONTEXT.remove(); + } + +} diff --git a/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/account/AccountAuthInterceptor.java b/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/interceptor/AccountAuthInterceptor.java similarity index 88% rename from common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/account/AccountAuthInterceptor.java rename to common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/interceptor/AccountAuthInterceptor.java index 538114688..040ed1a97 100644 --- a/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/account/AccountAuthInterceptor.java +++ b/common/mall-spring-boot-starter-security/src/main/java/cn/iocoder/mall/security/core/interceptor/AccountAuthInterceptor.java @@ -1,4 +1,4 @@ -package cn.iocoder.mall.security.core.account; +package cn.iocoder.mall.security.core.interceptor; import cn.iocoder.common.framework.util.HttpUtil; import cn.iocoder.common.framework.util.ServiceExceptionUtil; @@ -10,6 +10,7 @@ import cn.iocoder.mall.web.core.util.CommonWebUtil; import org.apache.dubbo.config.annotation.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; @@ -24,8 +25,12 @@ public class AccountAuthInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - // 执行认证 + // 获得访问令牌 String accessToken = HttpUtil.obtainAuthorization(request); + if (StringUtils.hasText(accessToken)) { // 如果未传递,则不进行认证 + return true; + } + // 执行认证 OAuth2AccessTokenAuthenticateRequest oauth2AccessTokenAuthenticateRequest = new OAuth2AccessTokenAuthenticateRequest() .setAccessToken(accessToken).setIp(HttpUtil.getIp(request)); CommonResult oauth2AccessTokenResponseResult = oauth2RPC.authenticate(oauth2AccessTokenAuthenticateRequest); diff --git a/common/mall-spring-boot-starter-security/src/main/resources/META-INF/spring.factories b/common/mall-spring-boot-starter-security/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..198e0fb11 --- /dev/null +++ b/common/mall-spring-boot-starter-security/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.mall.security.config.CommonSecurityAutoConfiguration diff --git a/common/mall-spring-boot-starter-web/src/main/java/cn/iocoder/mall/web/config/CommonWebAutoConfiguration.java b/common/mall-spring-boot-starter-web/src/main/java/cn/iocoder/mall/web/config/CommonWebAutoConfiguration.java index 676a02bed..ff4bbdef3 100644 --- a/common/mall-spring-boot-starter-web/src/main/java/cn/iocoder/mall/web/config/CommonWebAutoConfiguration.java +++ b/common/mall-spring-boot-starter-web/src/main/java/cn/iocoder/mall/web/config/CommonWebAutoConfiguration.java @@ -1,5 +1,6 @@ package cn.iocoder.mall.web.config; +import cn.iocoder.common.framework.servlet.CorsFilter; import cn.iocoder.mall.web.core.constant.CommonMallConstants; import cn.iocoder.mall.web.core.handler.GlobalExceptionHandler; import cn.iocoder.mall.web.core.handler.GlobalResponseBodyHandler; @@ -10,6 +11,7 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; @@ -47,14 +49,23 @@ public class CommonWebAutoConfiguration implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { try { - AccessLogInterceptor accessLogInterceptor = this.accessLogInterceptor(); - if (accessLogInterceptor != null) { - registry.addInterceptor(accessLogInterceptor) - .addPathPatterns(CommonMallConstants.ROOT_PATH_ADMIN + "/**", CommonMallConstants.ROOT_PATH_USER + "/**"); - } + registry.addInterceptor(this.accessLogInterceptor()) + .addPathPatterns(CommonMallConstants.ROOT_PATH_ADMIN + "/**", CommonMallConstants.ROOT_PATH_USER + "/**"); + logger.info("[addInterceptors][加载 AccessLogInterceptor 拦截器完成]"); } catch (NoSuchBeanDefinitionException e) { logger.warn("[addInterceptors][无法获取 AccessLogInterceptor 拦截器,因此不启动 AccessLog 的记录]"); } } + // ========== 过滤器相关 ========== + + @Bean + @ConditionalOnMissingBean + public FilterRegistrationBean corsFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new CorsFilter()); + registrationBean.addUrlPatterns("/*"); + return registrationBean; + } + } diff --git a/common/mall-spring-boot/src/main/java/cn/iocoder/mall/spring/boot/web/UserMVCAutoConfiguration.java b/common/mall-spring-boot/src/main/java/cn/iocoder/mall/spring/boot/web/UserMVCAutoConfiguration.java index e64b73ba2..74ca07987 100644 --- a/common/mall-spring-boot/src/main/java/cn/iocoder/mall/spring/boot/web/UserMVCAutoConfiguration.java +++ b/common/mall-spring-boot/src/main/java/cn/iocoder/mall/spring/boot/web/UserMVCAutoConfiguration.java @@ -53,13 +53,6 @@ public class UserMVCAutoConfiguration implements WebMvcConfigurer { registry.addInterceptor(userSecurityInterceptor()).addPathPatterns(MallConstants.ROOT_PATH_USER + "/**"); } - @Bean - @ConditionalOnMissingBean - public FilterRegistrationBean corsFilter() { - FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); - registrationBean.setFilter(new CorsFilter()); - registrationBean.addUrlPatterns("/*"); - return registrationBean; - } + } diff --git a/system/system-rest/pom.xml b/system/system-rest/pom.xml index c38f41f11..dae17eda7 100644 --- a/system/system-rest/pom.xml +++ b/system/system-rest/pom.xml @@ -26,6 +26,11 @@ mall-spring-boot-starter-web 1.0-SNAPSHOT + + cn.iocoder.mall + mall-spring-boot-starter-security + 1.0-SNAPSHOT + cn.iocoder.mall mall-spring-boot-starter-swagger diff --git a/system/system-rpc/src/main/resources/rpc.yaml b/system/system-rpc/src/main/resources/rpc.yaml index ffce4d6e8..e0b942d2b 100644 --- a/system/system-rpc/src/main/resources/rpc.yaml +++ b/system/system-rpc/src/main/resources/rpc.yaml @@ -21,3 +21,5 @@ dubbo: consumer: SystemLogRPC: # 用于 AccessLogInterceptor 等拦截器,记录 HTTP API 请求的访问日志 version: 1.0.0 + OAuth2RPC: + version: 1.0.0