https://docs.spring.io/spring-security/reference/5.8/migration/servlet/config.html
最近在做SpringBoot2.x到3.0的升级。其中最主要的一部分是javax -> jakarta
packageName的变更,另外一部分是对一些废弃/删除的类进行替换。大部分升级都比较顺利,但是在SpringSecurity上遇到了不少坑。
先看一下下面的代码
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.authorizeRequests().expressionHandler(webExpressionHandler());
registry
.antMatchers("/servlet1/**").permitAll()
.antMatchers("/servlet2/**").permitAll()
.antMatchers("/**").access("isLoggedIn()");
return http.build();
}
这段代码在6.0有两个问题,一是authorizeRequests
标记为废弃,二是antMatchers
方法被移除,这两个问题我们一个个看。
首先看一下antMatchers
方法被移除的问题,随便搜一下就可以找到答案,使用requestMatchers
来进行替代,看起来非常简单,替换之后我们启动server之后随便访问一下API,结果
随后又切换了几个发现全都是403,看log里也没有任何Error,怀疑是Spring进行了拦截,随后我们打开Trace级别的log,随后发现了如下信息
2023-03-22 15:44:32,746] [TRACE] 61259 [http-nio-8183-exec-2] edFilterInvocationSecurityMetadataSource - 59C004B425A34C0B96B01682C54B8B2C_1679471072599 - Did not match request to Mvc [pattern='/servlet1/**'] - [permitAll] (11/22)
我们的请求是GET localhost:8080/context/servlet1/test
,怎么会不匹配呢?最后通过debug发现,在SpringSecurity6.0中,默认使用的是MvcRequestMatcher
,
它在匹配的时候会将context-path
和servlet-path
都去掉之后再进行匹配,拿上面的例子来说是用/test
和/servlet1/**
进行匹配,匹配不上就会返回unauthorized 403。
由于我们项目中是多servlet的形式(历史原因)且API数量非常多,现在要去修改匹配路径非常容易漏掉某些API导致产线问题。然后我们仔细看了requestMatchers
的方法内部
public C requestMatchers(String... patterns) {
return requestMatchers(null, patterns);
}
public C requestMatchers(HttpMethod method, String... patterns) {
List<RequestMatcher> matchers = new ArrayList<>();
if (mvcPresent) {
matchers.addAll(createMvcMatchers(method, patterns));
}
else {
matchers.addAll(RequestMatchers.antMatchers(method, patterns));
}
return requestMatchers(matchers.toArray(new RequestMatcher[0]));
}
mvcPresent
这个boolean是根据AbstractRequestMatcherRegistry.class
存不存在来设值,这还是一个final变量想要通过修改mvcPresent
的值来生成antMatchers
似乎不现实,不过很快我们找到了另外一个方法
public C requestMatchers(RequestMatcher... requestMatchers) {
Assert.state(!this.anyRequestConfigured, "Can't configure requestMatchers after anyRequest");
return chainRequestMatchers(Arrays.asList(requestMatchers));
}
这个方法允许我们传入任意一种RequestMatcher,RequestMatchers.antMatchers这个方法也可以为我们产生一个antMatchers
,两者结合一下
```java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.authorizeRequests().expressionHandler(webExpressionHandler());
registry
.requestMatchers(antMatchers("/servlet1/**")).permitAll()
.requestMatchers.(antMatchers("/servlet2/**")).permitAll()
.requestMatchers("/**").access("isLoggedIn()");
return http.build();
}
最后测试通过,问题解决!
authorizeRequests
在6.0中被标记为废弃理论上暂时可以不进行处理,但是考虑到以后真正删除之后还要花时间重新再研究一遍,不如一鼓作气都处理掉了。我们先看看spring留下的注释
Deprecated
Use authorizeHttpRequests() instead
看起来非常简单,直接用authorizeHttpRequests()
方法进行替代就行了。当我们使用新的方法时遇到了两个error
expressionHandler()
接受一个SecurityExpressionHandler
作为参数,这个handler中有一个方法createSecurityExpressionRoot
@Override
protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication,
FilterInvocation fi) {
WebSecurityExpressionRoot root = new WebSecurityExpressionRoot(authentication, fi);
root.setPermissionEvaluator(getPermissionEvaluator());
root.setTrustResolver(this.trustResolver);
root.setRoleHierarchy(getRoleHierarchy());
root.setDefaultRolePrefix(this.defaultRolePrefix);
return root;
}
createSecurityExpressionRoot
的第一行需要创建一个WebSecurityExpressionRoot
,这个root负责解析access(expression)
中的expression
表达式,在本例中我们自定义的WebSecurityExpressionRoot
中有一个isLoggedIn()
方法,该方法返回一个boolean用于authorization判断。看到这里大概就可以明白 expressionHandler()
和access()
是配套使用的,一个负责设置expression
表达式,一个负责解析表达式,主要的作用是判断request是否被授权。
然后我们看一下AuthorizationManagerRequestMatcherRegistry的access方法
public AuthorizationManagerRequestMatcherRegistry access(
AuthorizationManager<RequestAuthorizationContext> manager) {
Assert.notNull(manager, "manager cannot be null");
return AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager);
}
该方法接收AuthorizationManager作为参数,我们再看一下AuthorizationManager的定义
/**
* An Authorization manager which can determine if an {@link Authentication} has access to
* a specific object.
*
* @param the type of object that the authorization check is being done one.
* @author Evgeniy Cheban
*/
@FunctionalInterface
public interface AuthorizationManager<T> {
/**
* Determines if access should be granted for a specific authentication and object.
* @param authentication the {@link Supplier} of the {@link Authentication} to check
* @param object the {@link T} object to check
* @throws AccessDeniedException if access is not granted
*/
default void verify(Supplier<Authentication> authentication, T object) {
AuthorizationDecision decision = check(authentication, object);
if (decision != null && !decision.isGranted()) {
throw new AccessDeniedException("Access Denied");
}
}
/**
* Determines if access is granted for a specific authentication and object.
* @param authentication the {@link Supplier} of the {@link Authentication} to check
* @param object the {@link T} object to check
* @return an {@link AuthorizationDecision} or null if no decision could be made
*/
@Nullable
AuthorizationDecision check(Supplier<Authentication> authentication, T object);
}
AuthorizationManager其实很简单,主要就是实现check方法来返回对应的AuthorizationDecision。至此我们似乎已经有了解决方案,我们可以自定一个AuthorizationManager,将isLoggedIn()
的逻辑放入其中即可。不过在查看Spring Migration的文档的过程中,我们发现了一个有意思的类WebExpressionAuthorizationManager
,
/**
* Creates an instance.
* @param expressionString the raw expression string to parse
*/
public WebExpressionAuthorizationManager(String expressionString) {
Assert.hasText(expressionString, "expressionString cannot be empty");
this.expression = this.expressionHandler.getExpressionParser().parseExpression(expressionString);
}
/**
* Sets the {@link SecurityExpressionHandler} to be used. The default is
* {@link DefaultHttpSecurityExpressionHandler}.
* @param expressionHandler the {@link SecurityExpressionHandler} to use
*/
public void setExpressionHandler(SecurityExpressionHandler<RequestAuthorizationContext> expressionHandler) {
Assert.notNull(expressionHandler, "expressionHandler cannot be null");
this.expressionHandler = expressionHandler;
this.expression = expressionHandler.getExpressionParser()
.parseExpression(this.expression.getExpressionString());
}
WebExpressionAuthorizationManager的构造函数需要传入一个expression
表达式,并提供了一个方法setExpressionHandler
,这一切不正是我们所需要的!
需要注意的是,虽然都是SecurityExpressionHandler,但是泛型参数不同需要我们做些调整
最后,代码如下
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
var authorizationManager = new WebExpressionAuthorizationManager("isLoggedIn()");
authorizationManager.setExpressionHandler(webExpressionHandler());
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.authorizeHttpRequests();
registry
registry
.requestMatchers(antMatchers("/servlet1/**")).permitAll()
.requestMatchers.(antMatchers("/servlet2/**")).permitAll()
.requestMatchers("/**").access(authorizationManager);
return http.build();
}