liriming
2024-12-03 97f3c16ac595493405e8cf76399dc703c088ef6f
提交 | 用户 | 时间
e7c126 1 package com.iailab.framework.security.config;
H 2
3 import cn.hutool.core.collection.CollUtil;
4 import com.iailab.framework.security.core.filter.TokenAuthenticationFilter;
5 import com.iailab.framework.web.config.WebProperties;
6 import com.google.common.collect.HashMultimap;
7 import com.google.common.collect.Multimap;
8 import org.springframework.boot.autoconfigure.AutoConfiguration;
9 import org.springframework.boot.autoconfigure.AutoConfigureOrder;
10 import org.springframework.context.ApplicationContext;
11 import org.springframework.context.annotation.Bean;
12 import org.springframework.http.HttpMethod;
13 import org.springframework.security.authentication.AuthenticationManager;
14 import org.springframework.security.config.Customizer;
15 import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
16 import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
17 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
18 import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
19 import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
20 import org.springframework.security.config.http.SessionCreationPolicy;
21 import org.springframework.security.web.AuthenticationEntryPoint;
22 import org.springframework.security.web.SecurityFilterChain;
23 import org.springframework.security.web.access.AccessDeniedHandler;
24 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
25 import org.springframework.web.bind.annotation.RequestMethod;
26 import org.springframework.web.method.HandlerMethod;
27 import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
28 import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
29 import org.springframework.web.util.pattern.PathPattern;
30
31 import javax.annotation.Resource;
32 import javax.annotation.security.PermitAll;
33 import java.util.HashSet;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Set;
37
38 import static com.iailab.framework.common.util.collection.CollectionUtils.convertList;
39
40 /**
41  * 自定义的 Spring Security 配置适配器实现
42  *
43  * @author iailab
44  */
45 @AutoConfiguration
46 @AutoConfigureOrder(-1) // 目的:先于 Spring Security 自动配置,避免一键改包后,org.* 基础包无法生效
47 @EnableMethodSecurity(securedEnabled = true)
48 public class IailabWebSecurityConfigurerAdapter {
49
50     @Resource
51     private WebProperties webProperties;
52     @Resource
53     private SecurityProperties securityProperties;
54
55     /**
56      * 认证失败处理类 Bean
57      */
58     @Resource
59     private AuthenticationEntryPoint authenticationEntryPoint;
60     /**
61      * 权限不够处理器 Bean
62      */
63     @Resource
64     private AccessDeniedHandler accessDeniedHandler;
65     /**
66      * Token 认证过滤器 Bean
67      */
68     @Resource
69     private TokenAuthenticationFilter authenticationTokenFilter;
70
71     /**
72      * 自定义的权限映射 Bean 们
73      *
74      * @see #filterChain(HttpSecurity)
75      */
76     @Resource
77     private List<AuthorizeRequestsCustomizer> authorizeRequestsCustomizers;
78
79     @Resource
80     private ApplicationContext applicationContext;
81
82     /**
83      * 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入
84      * 通过覆写父类的该方法,添加 @Bean 注解,解决该问题
85      */
86     @Bean
87     public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception {
88         return authenticationConfiguration.getAuthenticationManager();
89     }
90
91     /**
92      * 配置 URL 的安全配置
93      *
94      * anyRequest          |   匹配所有请求路径
95      * access              |   SpringEl表达式结果为true时可以访问
96      * anonymous           |   匿名可以访问
97      * denyAll             |   用户不能访问
98      * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
99      * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
100      * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
101      * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
102      * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
103      * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
104      * permitAll           |   用户可以任意访问
105      * rememberMe          |   允许通过remember-me登录的用户访问
106      * authenticated       |   用户登录后可访问
107      */
108     @Bean
109     protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
110         // 登出
111         httpSecurity
112                 // 开启跨域
113                 .cors(Customizer.withDefaults())
114                 // CSRF 禁用,因为不使用 Session
115                 .csrf(AbstractHttpConfigurer::disable)
116                 // 基于 token 机制,所以不需要 Session
117                 .sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
118                 .headers(c -> c.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
119                 // 一堆自定义的 Spring Security 处理器
120                 .exceptionHandling(c -> c.authenticationEntryPoint(authenticationEntryPoint)
121                         .accessDeniedHandler(accessDeniedHandler));
122         // 登录、登录暂时不使用 Spring Security 的拓展点,主要考虑一方面拓展多用户、多种登录方式相对复杂,一方面用户的学习成本较高
123
124         // 获得 @PermitAll 带来的 URL 列表,免登录
125         Multimap<HttpMethod, String> permitAllUrls = getPermitAllUrlsFromAnnotations();
126         // 设置每个请求的权限
127         httpSecurity
128                 // ①:全局共享规则
874287 129                 .authorizeHttpRequests(c -> c
H 130                         // 1.1 静态资源,可匿名访问
131                         .requestMatchers(HttpMethod.GET, "/*.html", "/*.html", "/*.css", "/*.js").permitAll()
132                         // 1.2 设置 @PermitAll 无需认证
133                         .requestMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll()
134                         .requestMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll()
135                         .requestMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll()
136                         .requestMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll()
137                         .requestMatchers(HttpMethod.HEAD, permitAllUrls.get(HttpMethod.HEAD).toArray(new String[0])).permitAll()
138                         .requestMatchers(HttpMethod.PATCH, permitAllUrls.get(HttpMethod.PATCH).toArray(new String[0])).permitAll()
139                         // 1.3 基于 yudao.security.permit-all-urls 无需认证
140                         .requestMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll()
141                 )
e7c126 142                 // ②:每个项目的自定义规则
874287 143                 .authorizeHttpRequests(c -> authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(c)))
e7c126 144                 // ③:兜底规则,必须认证
874287 145                 .authorizeHttpRequests(c -> c.anyRequest().authenticated());
e7c126 146
H 147         // 添加 Token Filter
148         httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
149         return httpSecurity.build();
150     }
151
152     private String buildAppApi(String url) {
153         return webProperties.getAppApi().getPrefix() + url;
154     }
155
156     private Multimap<HttpMethod, String> getPermitAllUrlsFromAnnotations() {
157         Multimap<HttpMethod, String> result = HashMultimap.create();
158         // 获得接口对应的 HandlerMethod 集合
159         RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping)
160                 applicationContext.getBean("requestMappingHandlerMapping");
161         Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
162         // 获得有 @PermitAll 注解的接口
163         for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethodMap.entrySet()) {
164             HandlerMethod handlerMethod = entry.getValue();
165             if (!handlerMethod.hasMethodAnnotation(PermitAll.class)) {
166                 continue;
167             }
168             Set<String> urls = new HashSet<>();
169             if (entry.getKey().getPatternsCondition() != null) {
170                 urls.addAll(entry.getKey().getPatternsCondition().getPatterns());
171             }
172             if (entry.getKey().getPathPatternsCondition() != null) {
173                 urls.addAll(convertList(entry.getKey().getPathPatternsCondition().getPatterns(), PathPattern::getPatternString));
174             }
175             if (urls.isEmpty()) {
176                 continue;
177             }
178
179             // 特殊:使用 @RequestMapping 注解,并且未写 method 属性,此时认为都需要免登录
180             Set<RequestMethod> methods = entry.getKey().getMethodsCondition().getMethods();
181             if (CollUtil.isEmpty(methods)) {
182                 result.putAll(HttpMethod.GET, urls);
183                 result.putAll(HttpMethod.POST, urls);
184                 result.putAll(HttpMethod.PUT, urls);
185                 result.putAll(HttpMethod.DELETE, urls);
186                 result.putAll(HttpMethod.HEAD, urls);
187                 result.putAll(HttpMethod.PATCH, urls);
188                 continue;
189             }
190             // 根据请求方法,添加到 result 结果
191             entry.getKey().getMethodsCondition().getMethods().forEach(requestMethod -> {
192                 switch (requestMethod) {
193                     case GET:
194                         result.putAll(HttpMethod.GET, urls);
195                         break;
196                     case POST:
197                         result.putAll(HttpMethod.POST, urls);
198                         break;
199                     case PUT:
200                         result.putAll(HttpMethod.PUT, urls);
201                         break;
202                     case DELETE:
203                         result.putAll(HttpMethod.DELETE, urls);
204                         break;
205                     case HEAD:
206                         result.putAll(HttpMethod.HEAD, urls);
207                         break;
208                     case PATCH:
209                         result.putAll(HttpMethod.PATCH, urls);
210                         break;
211                 }
212             });
213         }
214         return result;
215     }
216
217 }