Prechádzať zdrojové kódy

增加 @PreAuthenticated 注解,实现登陆的拦截

YunaiV 3 rokov pred
rodič
commit
689171c18d

+ 1 - 1
pom.xml

@@ -19,7 +19,7 @@
     <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
 
     <properties>
-        <revision>1.0.0-snapshot</revision>
+        <revision>1.1.0-snapshot</revision>
         <!-- Maven 相关 -->
         <java.version>1.8</java.version>
         <maven.compiler.source>${java.version}</maven.compiler.source>

+ 39 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/framework/security/SecurityConfiguration.java

@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.adminserver.framework.security;
+
+import cn.iocoder.yudao.framework.web.config.WebProperties;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
+
+import javax.annotation.Resource;
+
+@Configuration
+public class SecurityConfiguration {
+
+    @Resource
+    private WebProperties webProperties;
+
+    @Value("${spring.boot.admin.context-path:''}")
+    private String adminSeverContextPath;
+
+    @Bean
+    public Customizer<ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry> authorizeRequestsCustomizer() {
+        return registry -> {
+            // 通用的接口,可匿名访问 TODO 芋艿:需要抽象出去
+            registry.antMatchers(api("/system/captcha/**")).anonymous();
+            // Spring Boot Admin Server 的安全配置 TODO 芋艿:需要抽象出去
+            registry.antMatchers(adminSeverContextPath).anonymous()
+                    .antMatchers(adminSeverContextPath + "/**").anonymous();
+            // 短信回调 API
+            registry.antMatchers(api("/system/sms/callback/**")).anonymous();
+        };
+    }
+
+    private String api(String url) {
+        return webProperties.getApiPrefix() + url;
+    }
+
+}

+ 6 - 1
yudao-framework/yudao-spring-boot-starter-security/pom.xml

@@ -19,7 +19,12 @@
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-common</artifactId>
-            <version>${revision}</version>
+        </dependency>
+
+        <!-- Spring 核心 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
         </dependency>
 
         <!-- Web 相关 -->

+ 9 - 0
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoSecurityAutoConfiguration.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.framework.security.config;
 
+import cn.iocoder.yudao.framework.security.core.aop.PreAuthenticatedAspect;
 import cn.iocoder.yudao.framework.security.core.filter.JwtAuthenticationTokenFilter;
 import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl;
 import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPointImpl;
@@ -33,6 +34,14 @@ public class YudaoSecurityAutoConfiguration {
     private SecurityProperties securityProperties;
 
     /**
+     * 处理用户未登陆拦截的切面的 Bean
+     */
+    @Bean
+    public PreAuthenticatedAspect preAuthenticatedAspect() {
+        return new PreAuthenticatedAspect();
+    }
+
+    /**
      * 认证失败处理类 Bean
      */
     @Bean

+ 19 - 19
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java

@@ -14,10 +14,12 @@ import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.http.HttpMethod;
 import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
 import org.springframework.security.config.http.SessionCreationPolicy;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@@ -41,9 +43,6 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
     @Resource
     private WebProperties webProperties;
 
-    @Value("${spring.boot.admin.context-path:''}")
-    private String adminSeverContextPath;
-
     /**
      * 自定义用户【认证】逻辑
      */
@@ -74,6 +73,13 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
      */
     @Resource
     private JwtAuthenticationTokenFilter authenticationTokenFilter;
+    /**
+     * 自定义的权限映射 Bean
+     *
+     * @see #configure(HttpSecurity)
+     */
+    @Resource
+    private Customizer<ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry> authorizeRequestsCustomizer;
 
     /**
      * 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入
@@ -121,15 +127,16 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
                 .csrf().disable()
                 // 基于 token 机制,所以不需要 Session
                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
+                .headers().frameOptions().disable().and()
                 // 一堆自定义的 Spring Security 处理器
                 .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
                     .accessDeniedHandler(accessDeniedHandler).and()
-                // 设置每个请求的权限
-                .authorizeRequests()
+                .logout().logoutUrl(api("/logout")).logoutSuccessHandler(logoutSuccessHandler); // 登出
+
+        // 设置每个请求的权限 ①:全局共享规则
+        httpSecurity.authorizeRequests()
                     // 登陆的接口,可匿名访问
                     .antMatchers(api("/login")).anonymous()
-                    // 通用的接口,可匿名访问 TODO 芋艿:需要抽象出去
-                    .antMatchers(api("/system/captcha/**")).anonymous()
                     // 静态资源,可匿名访问
                     .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
                     // 文件的获取接口,可匿名访问
@@ -139,22 +146,15 @@ public class YudaoWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdap
                     .antMatchers("/swagger-resources/**").anonymous()
                     .antMatchers("/webjars/**").anonymous()
                     .antMatchers("/*/api-docs").anonymous()
-                    // Spring Boot Admin Server 的安全配置 TODO 芋艿:需要抽象出去
-                    .antMatchers(adminSeverContextPath).anonymous()
-                    .antMatchers(adminSeverContextPath + "/**").anonymous()
                     // Spring Boot Actuator 的安全配置
                     .antMatchers("/actuator").anonymous()
                     .antMatchers("/actuator/**").anonymous()
-                    // Druid 监控 TODO 芋艿:需要抽象出去
+                    // Druid 监控 TODO 芋艿:等对接了 druid admin 后,在调整下。
                     .antMatchers("/druid/**").anonymous()
-                    // 短信回调 API TODO 芋艿:需要抽象出去
-                    .antMatchers(api("/system/sms/callback/**")).anonymous()
-                    .antMatchers(api("/user/**")).anonymous()
-                    // 除上面外的所有请求全部需要鉴权认证
-                    .anyRequest().authenticated()
-                .and()
-                .headers().frameOptions().disable();
-        httpSecurity.logout().logoutUrl(api("/logout")).logoutSuccessHandler(logoutSuccessHandler);
+                // 设置每个请求的权限 ②:每个项目的自定义规则
+                .and().authorizeRequests(authorizeRequestsCustomizer)
+                // 设置每个请求的权限 ③:兜底规则,必须认证
+                .authorizeRequests().anyRequest().authenticated();
         // 添加 JWT Filter
         httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
     }

+ 17 - 0
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/annotations/PreAuthenticated.java

@@ -0,0 +1,17 @@
+package cn.iocoder.yudao.framework.security.core.annotations;
+
+import java.lang.annotation.*;
+
+/**
+ * 声明用户需要登陆
+ *
+ * 为什么不使用 {@link org.springframework.security.access.prepost.PreAuthorize} 注解,原因是不通过时,抛出的是认证不通过,而不是未登陆
+ *
+ * @author 芋道源码
+ */
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+@Documented
+public @interface PreAuthenticated {
+}

+ 25 - 0
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/aop/PreAuthenticatedAspect.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.framework.security.core.aop;
+
+import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+
+import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED;
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+
+@Aspect
+@Slf4j
+public class PreAuthenticatedAspect {
+
+    @Around("@annotation(preAuthenticated)")
+    public Object around(ProceedingJoinPoint joinPoint, PreAuthenticated preAuthenticated) throws Throwable {
+        if (SecurityFrameworkUtils.getLoginUser() == null) {
+            throw exception(UNAUTHORIZED);
+        }
+        return joinPoint.proceed();
+    }
+
+}

+ 29 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/framework/security/SecurityConfiguration.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.userserver.framework.security;
+
+import cn.iocoder.yudao.framework.web.config.WebProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
+
+import javax.annotation.Resource;
+
+@Configuration
+public class SecurityConfiguration {
+
+    @Resource
+    private WebProperties webProperties;
+
+    @Bean
+    public Customizer<ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry> authorizeRequestsCustomizer() {
+        return registry -> {
+            registry.antMatchers(api("/**")).anonymous(); // 默认 API 都是用户可访问
+        };
+    }
+
+    private String api(String url) {
+        return webProperties.getApiPrefix() + url;
+    }
+
+}

+ 10 - 0
yudao-user-server/src/main/java/cn/iocoder/yudao/userserver/modules/infra/controller/HelloController.java

@@ -1,6 +1,9 @@
 package cn.iocoder.yudao.userserver.modules.infra.controller;
 
+import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.access.annotation.Secured;
+import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
@@ -15,4 +18,11 @@ public class HelloController {
     public String hello(String hello) {
         return "echo + " + hello;
     }
+
+    @RequestMapping("/user/info")
+    @PreAuthenticated
+    public String xx() {
+        return "none";
+    }
+
 }