配置问题说明
如下所示,代码配置了两个,过滤器,一个是资源保护,一个是不保护。
/** @Description: 配置需要保护的资源* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年5月23日 下午2:28:20*/@Bean@Order(2)public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http) throws Exception {http.securityMatcher("/sysOrg/info/page").csrf(csrf -> csrf.disable()).authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated()).oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())).formLogin(form -> form.disable());return http.build();}/** @Description: 配置不需要保护的资源* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年5月23日 下午2:28:20*/@Bean@Order(3)public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(authorize ->authorize.requestMatchers("/sysOrg/info/list", //测试该接口公开访问,不需要保护 "/login", // 登录页面"/logout", // 登出端点"/swagger-ui/**", // Swagger UI"/v3/api-docs/**", // OpenAPI文档"/webjars/**", // WebJars资源"/swagger-resources/**", // Swagger资源"/doc.html" // Knife4j文档页面).permitAll().anyRequest().authenticated()).formLogin(Customizer.withDefaults()).logout(Customizer.withDefaults());return http.build();}
先说上述代码的结果
访问http://127.0.0.1:18080/doc.html是正常的,这是期望的
访问http://127.0.0.1:18080/sysOrg/info/page,会被401,这也是期望的,因为我携带无效token
但是我访问http://127.0.0.1:18080/sysOrg/info/list,给我从定向到登录页面了
什么意思嗯?就是说,下面的配置中,配置的其他静态资源,例如swagger页面,是能访问的,但是里面的/sysOrg/info/list这个接口不能访问。
这个破问题,百度,找官方资料,GPT、各类AI大模型,一点卵用没得。配置文件,开一下debug,看看
debug: true
logging.level.org.springframework.security: DEBUG
接下来当我访问swagger的时候
通过debug的日志,发现正常的。
接下来访问http://127.0.0.1:18080/sysOrg/info/page
这也是符合我预期的,因为我的JWT token确实无效
o.s.security.web.FilterChainProxy : Securing POST /sysOrg/info/page
o.s.s.o.s.r.a.JwtAuthenticationProvider : Failed to authenticate since the JWT was invalid
但是我访问http://127.0.0.1:18080/sysOrg/info/list(也就是配置的公开API接口)
根据日志报错,提示无效的CSRF token。然后从定向到登录了。
o.s.security.web.FilterChainProxy : Securing POST /sysOrg/info/list
o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for http://127.0.0.1:18080/sysOrg/info/list
o.s.s.w.access.AccessDeniedHandlerImpl : Responding with 403 status code
o.s.security.web.FilterChainProxy : Securing POST /error
o.s.s.w.a.AnonymousAuthenticationFilter : Set SecurityContextHolder to anonymous SecurityContext
o.s.s.web.DefaultRedirectStrategy : Redirecting to http://127.0.0.1:18080/login
o.s.security.web.FilterChainProxy : Securing GET /login
通过查阅到相关资料,得知
Spring Security 默认对 POST、PUT、DELETE 等非 GET 请求启用 CSRF 校验,要求请求中包含有效的 CSRF 令牌
于是加了一个get方法的测试接口,果然,不进行验证了。说明,该版本是需要对CSRF进行验证
接下来,我们对/sysOrg/info/list进行CSRF忽略。
@Bean@Order(3)public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {http.csrf(csrf -> csrf.ignoringRequestMatchers("/sysOrg/info/list" //测试该接口公开访问,不需要保护 )).authorizeHttpRequests(authorize ->authorize.requestMatchers("/sysOrg/info/list", //测试该接口公开访问,不需要保护 "/sysOrg/info/test/get", //测试该接口公开访问,不需要保护 "/login", // 登录页面"/logout", // 登出端点"/swagger-ui/**", // Swagger UI"/v3/api-docs/**", // OpenAPI文档"/webjars/**", // WebJars资源"/swagger-resources/**", // Swagger资源"/doc.html" // Knife4j文档页面).permitAll().anyRequest().authenticated()).formLogin(Customizer.withDefaults()).logout(Customizer.withDefaults());return http.build();}
现在终于能访问了。而不是重新返回登录页面。
接下来,我使用rest ful地址参数,如下所示,很好,又返回登录页面了。
@PostMapping("/info/list/{id}")@ApiOperationSupport(order = 6)public R<List<SysOrg>> ssssssssss(@PathVariable String id) {return R.ok();}
Spring Security 的路径匹配支持 Ant 模式,需使用 ** 或 * 通配符匹配动态路径
因此我们修改下配置,修改为/sysOrg/info/list/**
@Bean@Order(3)public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {http.csrf(csrf -> csrf.ignoringRequestMatchers("/sysOrg/info/list/**" //测试该接口公开访问,不需要保护 )).authorizeHttpRequests(authorize ->authorize.requestMatchers("/sysOrg/info/list/**", //测试该接口公开访问,不需要保护 "/sysOrg/info/test/get", //测试该接口公开访问,不需要保护 "/login", // 登录页面"/logout", // 登出端点"/swagger-ui/**", // Swagger UI"/v3/api-docs/**", // OpenAPI文档"/webjars/**", // WebJars资源"/swagger-resources/**", // Swagger资源"/doc.html" // Knife4j文档页面).permitAll().anyRequest().authenticated()).formLogin(Customizer.withDefaults()).logout(Customizer.withDefaults());return http.build();}
当然现在在地址后面拼接参数也可以
配置思路
截止目前技术路线就打通了,接下来,就是怎么设计了来帮助我们更好的配置接口地址了,总不能在代码里面挨个的去写
思路
1.ignore-url: yml配置不需要进行拦截的地址,例如静态资源文件、swagger、公发API接口
2. allUrl:获取指定包路径下的所有的接口地址
3. 配置资源保护API接口:保护的API接口 = allUrl 减 ignore-url
4. 配置公共开发API接口:ignore-url
最后完整的示例代码如下
配置信息
package.path: com.example.demoignore-url: "/login;/logout;/swagger-ui/**;/v3/api-docs/**;/webjars/**;/swagger-resources/**;/doc.html;/code/generate;/table/download;/sysUser/**"
oauth2.1配置
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;import com.example.demo.UrlsUtils;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;import lombok.extern.log4j.Log4j2;@Configuration
@EnableWebSecurity
@Log4j2
public class SecurityConfig {@Autowiredprivate WebApplicationContext applicationContext;@Value(value = "${ignore-url}")private String ignoreUrl;@Value(value = "${package.path}")private String packagePath;/** @Description: 配置授权服务器(用于登录操作)* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年5月23日 下午3:15:35*/@Bean@Order(1)public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =OAuth2AuthorizationServerConfigurer.authorizationServer();http.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher()).with(authorizationServerConfigurer, (authorizationServer) ->authorizationServer.oidc(Customizer.withDefaults())).authorizeHttpRequests((authorize) ->authorize.anyRequest().authenticated()).exceptionHandling((exceptions) -> exceptions.defaultAuthenticationEntryPointFor(new LoginUrlAuthenticationEntryPoint("/login"),new MediaTypeRequestMatcher(MediaType.TEXT_HTML)));return http.build();}/** @Description: 配置需要保护的资源* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年5月23日 下午2:28:20*/@Bean@Order(2)public SecurityFilterChain resourceServerSecurityFilterChain(HttpSecurity http) throws Exception {//获取所有的接口地址Map<String, Object> filteredControllers = UrlsUtils.getControllerClass(packagePath, applicationContext);// 使用过滤后的控制器获取 URLList<String> allUrl = UrlsUtils.getControllerUrls(filteredControllers);String[] split = ignoreUrl.split(";");for (String url : split) {url = url.replaceAll(" ", "");UrlsUtils.removeUrl(allUrl, url);UrlsUtils.removeUrl(allUrl, url);}log.info("受保护的API地址:{}",allUrl);http.securityMatcher(allUrl.toArray(new String[0])).csrf(csrf -> csrf.disable()).authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated()).oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())).formLogin(form -> form.disable());return http.build();}/** @Description: 配置不需要保护的资源* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年5月23日 下午2:28:20*/@Bean@Order(3)public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {String[] split = ignoreUrl.split(";");for (int i = 0; i < split.length; i++) {split[i] = split[i].replaceAll(" ", "");}log.info("受保护的API地址:{}",Arrays.asList(split));http.csrf(csrf -> csrf.ignoringRequestMatchers(split)).authorizeHttpRequests(authorize ->authorize.requestMatchers(split).permitAll().anyRequest().authenticated()).formLogin(Customizer.withDefaults()).logout(Customizer.withDefaults());return http.build();}@Beanpublic UserDetailsService userDetailsService() {UserDetails userDetails = User.withDefaultPasswordEncoder().username("hutao").password("2025.com").roles("USER").build();return new InMemoryUserDetailsManager(userDetails);}@Beanpublic RegisteredClientRepository registeredClientRepository() {RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString()).clientId("oidc-client").clientSecret("{noop}secret").clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC).authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN).authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).redirectUri("http://www.baidu.com").postLogoutRedirectUri("http://127.0.0.1:8080/").scope(OidcScopes.OPENID).scope(OidcScopes.PROFILE).clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()).build();return new InMemoryRegisteredClientRepository(oidcClient);}@Beanpublic JWKSource<SecurityContext> jwkSource() {KeyPair keyPair = generateRsaKey();RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();RSAKey rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey).keyID(UUID.randomUUID().toString()).build();JWKSet jwkSet = new JWKSet(rsaKey);return new ImmutableJWKSet<>(jwkSet);}private static KeyPair generateRsaKey() {KeyPair keyPair;try {KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");keyPairGenerator.initialize(2048);keyPair = keyPairGenerator.generateKeyPair();}catch (Exception ex) {throw new IllegalStateException(ex);}return keyPair;}@Beanpublic JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);}@Beanpublic AuthorizationServerSettings authorizationServerSettings() {return AuthorizationServerSettings.builder().build();}}
工具类
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;import org.springframework.aop.support.AopUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;/*** @Description:获取项目的所有接口地址* @author:hutao* @mail:hutao_2017@aliyun.com* @date:2021年5月6日*/
public class UrlsUtils {/** @Description: 获取controller class* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年5月23日 下午4:18:54*/public static Map<String, Object> getControllerClass(String packagePath, WebApplicationContext applicationContext) {Map<String, Object> restControllers = applicationContext.getBeansWithAnnotation(RestController.class);Map<String, Object> controllers = applicationContext.getBeansWithAnnotation(Controller.class);restControllers.putAll(controllers);// 过滤出指定包路径下的控制器Map<String, Object> filteredControllers = restControllers.entrySet().stream().filter(entry -> {// 处理 CGLIB 代理类(获取原始类)Class<?> targetClass = AopUtils.getTargetClass(entry.getValue());return targetClass.getPackageName().startsWith(packagePath);}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));return filteredControllers;}/*** @description:获取Controller下各方法的接口地址* @author:hutao* @mail:hutao_2017@aliyun.com* @date:2022年3月22日 上午8:54:29*/public static List<String> getControllerUrls(Map<String,Object> controller) {List<String> urls = new ArrayList<>();for (Map.Entry<String, Object> entry : controller.entrySet()) {Object classValue = entry.getValue();Class<?> targetClass = AopUtils.getTargetClass(classValue);String baseUrl = getControllerPath(targetClass);List<Method> declaredMethods = Arrays.asList(targetClass.getDeclaredMethods());for (Method method : declaredMethods) {getMethodPath(urls, baseUrl, method);}}return urls;}/*** @description:移除不需要进行拦截的地址* @author:hutao* @mail:hutao_2017@aliyun.com* @date:2022年3月22日 上午8:54:20*/public static void removeUrl(List<String> urls,String removeUrl) {if(removeUrl.contains("/*")) {Iterator<String> iterator = urls.iterator();while(iterator.hasNext()){ String url = iterator.next(); if(url.contains(removeUrl.replace("*", ""))){ iterator.remove(); } } }else {urls.remove(removeUrl);}}/*** @description:获取所有方法的path* @author:hutao* @mail:hutao_2017@aliyun.com* @date:2022年3月22日 上午8:54:10*/private static void getMethodPath(List<String> urls, String baseUrl, Method method) {String[] mappingValues = null;if(method.getAnnotation(RequestMapping.class) != null) {RequestMapping annotation = method.getAnnotation(RequestMapping.class);mappingValues = annotation.value();}else if (method.getAnnotation(GetMapping.class) != null) {GetMapping annotation = method.getAnnotation(GetMapping.class);mappingValues = annotation.value();}else if (method.getAnnotation(PostMapping.class) != null) {PostMapping annotation = method.getAnnotation(PostMapping.class);mappingValues = annotation.value();}else if (method.getAnnotation(DeleteMapping.class) != null) {DeleteMapping annotation = method.getAnnotation(DeleteMapping.class);mappingValues = annotation.value();}else if (method.getAnnotation(PutMapping.class) != null) {PutMapping annotation = method.getAnnotation(PutMapping.class);mappingValues = annotation.value();}if(mappingValues != null) {for (String mappingValue : mappingValues) {String url = baseUrl+mappingValue;//处理某些controller中requestMapping中没有添加根路径if(!url.startsWith("/")) {url = "/"+url;}urls.add(url);}}}/*** @description:获取controller上面的path地址* @author:hutao* @mail:hutao_2017@aliyun.com* @date:2022年3月22日 上午8:53:59*/private static String getControllerPath(Class<?> targetClass) {String baseUrl = "";RequestMapping controllerMapping = targetClass.getAnnotation(RequestMapping.class);if(controllerMapping != null) {String[] values = controllerMapping.value();if (values.length>0) {baseUrl = values[0];}}return baseUrl;}
}
测试验证
启动项目,根据日志,查看相关接口是否需要进行保护
SecurityConfig : 受保护的API地址:[/sysOrg/info/list, /sysOrg/info/test/get, /sysOrg/info/update,
/sysOrg/info/remove/{id}, /sysOrg/info/save, /sysOrg/info/{id}, /sysOrg/info/list/{id},
/sysOrg/info/page, /sysOrg/info/list/xxxxx]SecurityConfig : 公共开发的API地址:[/login, /logout, /swagger-ui/**, /v3/api-docs/**,
/webjars/**, /swagger-resources/**,
/doc.html, /code/generate, /table/download, /sysUser/**]
以上述日志为例:我公开了:/doc.html, /code/generate, /table/download, /sysUser/**。但sysOrg下的接口,均为开放
公开的API不需要token
未公开的API,返回401