package com.iailab.framework.security.config; import cn.hutool.core.collection.CollUtil; import com.iailab.framework.security.core.filter.TokenAuthenticationFilter; import com.iailab.framework.web.config.WebProperties; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureOrder; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.util.pattern.PathPattern; import javax.annotation.Resource; import javax.annotation.security.PermitAll; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import static com.iailab.framework.common.util.collection.CollectionUtils.convertList; /** * 自定义的 Spring Security 配置适配器实现 * * @author iailab */ @AutoConfiguration @AutoConfigureOrder(-1) // 目的:先于 Spring Security 自动配置,避免一键改包后,org.* 基础包无法生效 @EnableMethodSecurity(securedEnabled = true) public class IailabWebSecurityConfigurerAdapter { @Resource private WebProperties webProperties; @Resource private SecurityProperties securityProperties; /** * 认证失败处理类 Bean */ @Resource private AuthenticationEntryPoint authenticationEntryPoint; /** * 权限不够处理器 Bean */ @Resource private AccessDeniedHandler accessDeniedHandler; /** * Token 认证过滤器 Bean */ @Resource private TokenAuthenticationFilter authenticationTokenFilter; /** * 自定义的权限映射 Bean 们 * * @see #filterChain(HttpSecurity) */ @Resource private List authorizeRequestsCustomizers; @Resource private ApplicationContext applicationContext; /** * 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入 * 通过覆写父类的该方法,添加 @Bean 注解,解决该问题 */ @Bean public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } /** * 配置 URL 的安全配置 * * anyRequest | 匹配所有请求路径 * access | SpringEl表达式结果为true时可以访问 * anonymous | 匿名可以访问 * denyAll | 用户不能访问 * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 * hasRole | 如果有参数,参数表示角色,则其角色可以访问 * permitAll | 用户可以任意访问 * rememberMe | 允许通过remember-me登录的用户访问 * authenticated | 用户登录后可访问 */ @Bean protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { // 登出 httpSecurity // 开启跨域 .cors(Customizer.withDefaults()) // CSRF 禁用,因为不使用 Session .csrf(AbstractHttpConfigurer::disable) // 基于 token 机制,所以不需要 Session .sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .headers(c -> c.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) // 一堆自定义的 Spring Security 处理器 .exceptionHandling(c -> c.authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler)); // 登录、登录暂时不使用 Spring Security 的拓展点,主要考虑一方面拓展多用户、多种登录方式相对复杂,一方面用户的学习成本较高 // 获得 @PermitAll 带来的 URL 列表,免登录 Multimap permitAllUrls = getPermitAllUrlsFromAnnotations(); // 设置每个请求的权限 httpSecurity // ①:全局共享规则 .authorizeHttpRequests(c -> c // 1.1 静态资源,可匿名访问 .requestMatchers(HttpMethod.GET, "/*.html", "/*.html", "/*.css", "/*.js").permitAll() // 1.2 设置 @PermitAll 无需认证 .requestMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll() .requestMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll() .requestMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll() .requestMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll() .requestMatchers(HttpMethod.HEAD, permitAllUrls.get(HttpMethod.HEAD).toArray(new String[0])).permitAll() .requestMatchers(HttpMethod.PATCH, permitAllUrls.get(HttpMethod.PATCH).toArray(new String[0])).permitAll() // 1.3 基于 yudao.security.permit-all-urls 无需认证 .requestMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll() ) // ②:每个项目的自定义规则 .authorizeHttpRequests(c -> authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(c))) // ③:兜底规则,必须认证 .authorizeHttpRequests(c -> c.anyRequest().authenticated()); // 添加 Token Filter httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); return httpSecurity.build(); } private String buildAppApi(String url) { return webProperties.getAppApi().getPrefix() + url; } private Multimap getPermitAllUrlsFromAnnotations() { Multimap result = HashMultimap.create(); // 获得接口对应的 HandlerMethod 集合 RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping"); Map handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods(); // 获得有 @PermitAll 注解的接口 for (Map.Entry entry : handlerMethodMap.entrySet()) { HandlerMethod handlerMethod = entry.getValue(); if (!handlerMethod.hasMethodAnnotation(PermitAll.class)) { continue; } Set urls = new HashSet<>(); if (entry.getKey().getPatternsCondition() != null) { urls.addAll(entry.getKey().getPatternsCondition().getPatterns()); } if (entry.getKey().getPathPatternsCondition() != null) { urls.addAll(convertList(entry.getKey().getPathPatternsCondition().getPatterns(), PathPattern::getPatternString)); } if (urls.isEmpty()) { continue; } // 特殊:使用 @RequestMapping 注解,并且未写 method 属性,此时认为都需要免登录 Set methods = entry.getKey().getMethodsCondition().getMethods(); if (CollUtil.isEmpty(methods)) { result.putAll(HttpMethod.GET, urls); result.putAll(HttpMethod.POST, urls); result.putAll(HttpMethod.PUT, urls); result.putAll(HttpMethod.DELETE, urls); result.putAll(HttpMethod.HEAD, urls); result.putAll(HttpMethod.PATCH, urls); continue; } // 根据请求方法,添加到 result 结果 entry.getKey().getMethodsCondition().getMethods().forEach(requestMethod -> { switch (requestMethod) { case GET: result.putAll(HttpMethod.GET, urls); break; case POST: result.putAll(HttpMethod.POST, urls); break; case PUT: result.putAll(HttpMethod.PUT, urls); break; case DELETE: result.putAll(HttpMethod.DELETE, urls); break; case HEAD: result.putAll(HttpMethod.HEAD, urls); break; case PATCH: result.putAll(HttpMethod.PATCH, urls); break; } }); } return result; } }