Enable Null checking in spring-security-web via JSpecify

Closes gh-16882
This commit is contained in:
Rob Winch 2025-08-21 14:24:08 -05:00
parent a58f3282d9
commit be64c67af5
No known key found for this signature in database
27 changed files with 237 additions and 36 deletions

View File

@ -1,3 +1,7 @@
plugins {
id 'security-nullability'
}
apply plugin: 'io.spring.convention.spring-module'
dependencies {

View File

@ -20,6 +20,7 @@ import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.springframework.expression.BeanResolver;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.messaging.Message;
@ -49,12 +50,16 @@ public class DefaultMessageSecurityExpressionHandler<T> extends AbstractSecurity
Message<T> message) {
MessageSecurityExpressionRoot root = createSecurityExpressionRoot(authentication, message);
StandardEvaluationContext ctx = new StandardEvaluationContext(root);
ctx.setBeanResolver(getBeanResolver());
BeanResolver beanResolver = getBeanResolver();
if (beanResolver != null) {
// https://github.com/spring-projects/spring-framework/issues/35371
ctx.setBeanResolver(beanResolver);
}
return ctx;
}
@Override
protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication,
protected SecurityExpressionOperations createSecurityExpressionRoot(@Nullable Authentication authentication,
Message<T> invocation) {
return createSecurityExpressionRoot(() -> authentication, invocation);
}

View File

@ -55,7 +55,7 @@ public final class MessageAuthorizationContextSecurityExpressionHandler
}
@Override
public EvaluationContext createEvaluationContext(Authentication authentication,
public EvaluationContext createEvaluationContext(@Nullable Authentication authentication,
MessageAuthorizationContext<?> message) {
return createEvaluationContext(() -> authentication, message);
}

View File

@ -18,6 +18,8 @@ package org.springframework.security.messaging.access.expression;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.messaging.Message;
@ -60,7 +62,7 @@ class MessageExpressionConfigAttribute implements ConfigAttribute, EvaluationCon
}
@Override
public String getAttribute() {
public @Nullable String getAttribute() {
return null;
}

View File

@ -18,6 +18,8 @@ package org.springframework.security.messaging.access.expression;
import java.util.Collection;
import org.jspecify.annotations.Nullable;
import org.springframework.expression.EvaluationContext;
import org.springframework.messaging.Message;
import org.springframework.security.access.AccessDecisionVoter;
@ -60,7 +62,7 @@ public class MessageExpressionVoter<T> implements AccessDecisionVoter<Message<T>
return ExpressionUtils.evaluateAsBoolean(attr.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED : ACCESS_DENIED;
}
private MessageExpressionConfigAttribute findConfigAttribute(Collection<ConfigAttribute> attributes) {
private @Nullable MessageExpressionConfigAttribute findConfigAttribute(Collection<ConfigAttribute> attributes) {
for (ConfigAttribute attribute : attributes) {
if (attribute instanceof MessageExpressionConfigAttribute) {
return (MessageExpressionConfigAttribute) attribute;

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Security expression support for {@link org.springframework.messaging.Message}.
*/
@NullMarked
package org.springframework.security.messaging.access.expression;
import org.jspecify.annotations.NullMarked;

View File

@ -20,6 +20,7 @@ import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.core.log.LogMessage;
import org.springframework.messaging.Message;
@ -110,7 +111,7 @@ public final class AuthorizationChannelInterceptor implements ChannelInterceptor
@Override
public <T> void publishAuthorizationEvent(Supplier<Authentication> authentication, T object,
AuthorizationResult result) {
@Nullable AuthorizationResult result) {
}

View File

@ -16,6 +16,8 @@
package org.springframework.security.messaging.access.intercept;
import org.jspecify.annotations.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.ChannelInterceptor;
@ -83,7 +85,7 @@ public final class ChannelSecurityInterceptor extends AbstractSecurityIntercepto
}
@Override
public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, Exception ex) {
public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, @Nullable Exception ex) {
InterceptorStatusToken token = clearToken();
finallyInvocation(token);
}
@ -99,7 +101,7 @@ public final class ChannelSecurityInterceptor extends AbstractSecurityIntercepto
}
@Override
public void afterReceiveCompletion(Message<?> message, MessageChannel channel, Exception ex) {
public void afterReceiveCompletion(@Nullable Message<?> message, MessageChannel channel, @Nullable Exception ex) {
}
private InterceptorStatusToken clearToken() {

View File

@ -17,6 +17,7 @@
package org.springframework.security.messaging.access.intercept;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
@ -61,7 +62,7 @@ public final class DefaultMessageSecurityMetadataSource implements MessageSecuri
return entry.getValue();
}
}
return null;
return Collections.emptyList();
}
@Override

View File

@ -55,7 +55,7 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
}
@Override
public AuthorizationResult authorize(Supplier<? extends @Nullable Authentication> authentication,
public @Nullable AuthorizationResult authorize(Supplier<? extends @Nullable Authentication> authentication,
Message<?> message) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Authorizing message"));
@ -75,7 +75,8 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
return null;
}
private MessageAuthorizationContext<?> authorizationContext(MessageMatcher<?> matcher, Message<?> message) {
private @Nullable MessageAuthorizationContext<?> authorizationContext(MessageMatcher<?> matcher,
Message<?> message) {
MessageMatcher.MatchResult matchResult = matcher.matcher((Message) message);
if (!matchResult.isMatch()) {
return null;
@ -179,7 +180,7 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
* @return the {@link Builder.Constraint} that is associated to the
* {@link MessageMatcher}
*/
private Builder.Constraint simpDestMatchers(SimpMessageType type, String... patterns) {
private Builder.Constraint simpDestMatchers(@Nullable SimpMessageType type, String... patterns) {
List<MessageMatcher<?>> matchers = new ArrayList<>(patterns.length);
for (String pattern : patterns) {
MessageMatcher<Object> matcher = this.messageMatcherBuilder.matcher(type, pattern);

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Authorization support for {@link org.springframework.messaging.Message}.
*/
@NullMarked
package org.springframework.security.messaging.access.intercept;
import org.jspecify.annotations.NullMarked;

View File

@ -18,6 +18,8 @@ package org.springframework.security.messaging.context;
import java.lang.annotation.Annotation;
import org.jspecify.annotations.Nullable;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.MergedAnnotations;
@ -110,13 +112,14 @@ public final class AuthenticationPrincipalArgumentResolver implements HandlerMet
}
@Override
public Object resolveArgument(MethodParameter parameter, Message<?> message) {
public @Nullable Object resolveArgument(MethodParameter parameter, Message<?> message) {
Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
if (authentication == null) {
return null;
}
Object principal = authentication.getPrincipal();
AuthenticationPrincipal authPrincipal = findMethodAnnotation(parameter);
Assert.notNull(authPrincipal, "AuthenticationPrincipal must not be null. Run supports first");
String expressionToParse = authPrincipal.expression();
if (StringUtils.hasLength(expressionToParse)) {
StandardEvaluationContext context = new StandardEvaluationContext();
@ -165,7 +168,7 @@ public final class AuthenticationPrincipalArgumentResolver implements HandlerMet
* @param parameter the {@link MethodParameter} to search for an {@link Annotation}
* @return the {@link Annotation} that was found or null.
*/
private AuthenticationPrincipal findMethodAnnotation(MethodParameter parameter) {
private @Nullable AuthenticationPrincipal findMethodAnnotation(MethodParameter parameter) {
if (this.useAnnotationTemplate) {
return this.scanner.scan(parameter.getParameter());
}

View File

@ -18,6 +18,8 @@ package org.springframework.security.messaging.context;
import java.util.Stack;
import org.jspecify.annotations.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
@ -96,7 +98,7 @@ public final class SecurityContextChannelInterceptor implements ExecutorChannelI
}
@Override
public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, Exception ex) {
public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, @Nullable Exception ex) {
cleanup();
}
@ -107,7 +109,8 @@ public final class SecurityContextChannelInterceptor implements ExecutorChannelI
}
@Override
public void afterMessageHandled(Message<?> message, MessageChannel channel, MessageHandler handler, Exception ex) {
public void afterMessageHandled(Message<?> message, MessageChannel channel, MessageHandler handler,
@Nullable Exception ex) {
cleanup();
}
@ -131,7 +134,7 @@ public final class SecurityContextChannelInterceptor implements ExecutorChannelI
this.securityContextHolderStrategy.setContext(context);
}
private Authentication getAuthentication(Object user) {
private Authentication getAuthentication(@Nullable Object user) {
if ((user instanceof Authentication)) {
return (Authentication) user;
}

View File

@ -18,6 +18,8 @@ package org.springframework.security.messaging.context;
import java.util.Stack;
import org.jspecify.annotations.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
@ -121,7 +123,8 @@ public final class SecurityContextPropagationChannelInterceptor implements Execu
}
@Override
public void afterMessageHandled(Message<?> message, MessageChannel channel, MessageHandler handler, Exception ex) {
public void afterMessageHandled(Message<?> message, MessageChannel channel, MessageHandler handler,
@Nullable Exception ex) {
cleanup();
}

View File

@ -0,0 +1,24 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Support for establishing the
* {@link org.springframework.security.core.context.SecurityContext} within messaging.
*/
@NullMarked
package org.springframework.security.messaging.context;
import org.jspecify.annotations.NullMarked;

View File

@ -18,6 +18,8 @@ package org.springframework.security.messaging.handler.invocation.reactive;
import java.lang.annotation.Annotation;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
@ -108,7 +110,7 @@ public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArg
private boolean useAnnotationTemplate = false;
private BeanResolver beanResolver;
private @Nullable BeanResolver beanResolver;
private ReactiveAdapterRegistry adapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
@ -149,7 +151,8 @@ public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArg
// @formatter:on
}
private Object resolvePrincipal(MethodParameter parameter, Object principal) {
@NullUnmarked
private @Nullable Object resolvePrincipal(MethodParameter parameter, @Nullable Object principal) {
AuthenticationPrincipal authPrincipal = findMethodAnnotation(parameter);
String expressionToParse = authPrincipal.expression();
if (StringUtils.hasLength(expressionToParse)) {
@ -169,7 +172,7 @@ public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArg
return principal;
}
private boolean isInvalidType(MethodParameter parameter, Object principal) {
private boolean isInvalidType(MethodParameter parameter, @Nullable Object principal) {
if (principal == null) {
return false;
}
@ -206,7 +209,7 @@ public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArg
* @param parameter the {@link MethodParameter} to search for an {@link Annotation}
* @return the {@link Annotation} that was found or null.
*/
private AuthenticationPrincipal findMethodAnnotation(MethodParameter parameter) {
private @Nullable AuthenticationPrincipal findMethodAnnotation(MethodParameter parameter) {
if (this.useAnnotationTemplate) {
return this.scanner.scan(parameter.getParameter());
}

View File

@ -18,6 +18,7 @@ package org.springframework.security.messaging.handler.invocation.reactive;
import java.lang.annotation.Annotation;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
@ -106,7 +107,7 @@ public class CurrentSecurityContextArgumentResolver implements HandlerMethodArgu
private boolean useAnnotationTemplate = false;
private BeanResolver beanResolver;
private @Nullable BeanResolver beanResolver;
private ReactiveAdapterRegistry adapterRegistry = ReactiveAdapterRegistry.getSharedInstance();
@ -159,7 +160,7 @@ public class CurrentSecurityContextArgumentResolver implements HandlerMethodArgu
// @formatter:on
}
private Object resolveSecurityContext(MethodParameter parameter, Object securityContext) {
private @Nullable Object resolveSecurityContext(MethodParameter parameter, Object securityContext) {
CurrentSecurityContext contextAnno = findMethodAnnotation(parameter);
if (contextAnno != null) {
return resolveSecurityContextFromAnnotation(contextAnno, parameter, securityContext);
@ -167,14 +168,17 @@ public class CurrentSecurityContextArgumentResolver implements HandlerMethodArgu
return securityContext;
}
private Object resolveSecurityContextFromAnnotation(CurrentSecurityContext contextAnno, MethodParameter parameter,
Object securityContext) {
private @Nullable Object resolveSecurityContextFromAnnotation(CurrentSecurityContext contextAnno,
MethodParameter parameter, Object securityContext) {
String expressionToParse = contextAnno.expression();
if (StringUtils.hasLength(expressionToParse)) {
StandardEvaluationContext context = new StandardEvaluationContext();
context.setRootObject(securityContext);
context.setVariable("this", securityContext);
context.setBeanResolver(this.beanResolver);
if (this.beanResolver != null) {
// https://github.com/spring-projects/spring-framework/issues/35371
context.setBeanResolver(this.beanResolver);
}
Expression expression = this.parser.parseExpression(expressionToParse);
securityContext = expression.getValue(context);
}
@ -187,7 +191,7 @@ public class CurrentSecurityContextArgumentResolver implements HandlerMethodArgu
return securityContext;
}
private boolean isInvalidType(MethodParameter parameter, Object value) {
private boolean isInvalidType(MethodParameter parameter, @Nullable Object value) {
if (value == null) {
return false;
}
@ -223,7 +227,7 @@ public class CurrentSecurityContextArgumentResolver implements HandlerMethodArgu
* @param parameter the {@link MethodParameter} to search for an {@link Annotation}
* @return the {@link Annotation} that was found or null.
*/
private CurrentSecurityContext findMethodAnnotation(MethodParameter parameter) {
private @Nullable CurrentSecurityContext findMethodAnnotation(MethodParameter parameter) {
if (this.useAnnotationTemplate) {
return this.scanner.scan(parameter.getParameter());
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Reactive support for resolving security related arguments.
*/
@NullMarked
package org.springframework.security.messaging.handler.invocation.reactive;
import org.jspecify.annotations.NullMarked;

View File

@ -107,7 +107,7 @@ public final class PathPatternMessageMatcher implements MessageMatcher<Object> {
return (pathMatchInfo != null) ? MatchResult.match(pathMatchInfo.getUriVariables()) : MatchResult.notMatch();
}
private static String getDestination(Message<?> message) {
private static @Nullable String getDestination(Message<?> message) {
return SimpMessageHeaderAccessor.getDestination(message.getHeaders());
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Support for matching messages.
*/
@NullMarked
package org.springframework.security.messaging.util.matcher;
import org.jspecify.annotations.NullMarked;

View File

@ -19,6 +19,8 @@ package org.springframework.security.messaging.web.csrf;
import java.security.MessageDigest;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
@ -70,7 +72,7 @@ public final class XorCsrfChannelInterceptor implements ChannelInterceptor {
* @param actual
* @return
*/
private static boolean equalsConstantTime(String expected, String actual) {
private static boolean equalsConstantTime(String expected, @Nullable String actual) {
if (expected == actual) {
return true;
}

View File

@ -18,6 +18,8 @@ package org.springframework.security.messaging.web.csrf;
import java.util.Base64;
import org.jspecify.annotations.Nullable;
import org.springframework.security.crypto.codec.Utf8;
import org.springframework.util.Assert;
@ -33,7 +35,7 @@ final class XorCsrfTokenUtils {
private XorCsrfTokenUtils() {
}
static String getTokenValue(String actualToken, String token) {
static @Nullable String getTokenValue(@Nullable String actualToken, String token) {
byte[] actualBytes;
try {
actualBytes = Base64.getUrlDecoder().decode(actualToken);

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Support CSRF protection in messages.
*/
@NullMarked
package org.springframework.security.messaging.web.csrf;
import org.jspecify.annotations.NullMarked;

View File

@ -19,6 +19,7 @@ package org.springframework.security.messaging.web.socket.server;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import org.jspecify.annotations.Nullable;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
@ -62,7 +63,7 @@ public final class CsrfTokenHandshakeInterceptor implements HandshakeInterceptor
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
@Nullable Exception exception) {
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Reactive Security CSRF protection.
*/
@NullMarked
package org.springframework.security.messaging.web.socket.server;
import org.jspecify.annotations.NullMarked;

View File

@ -74,7 +74,7 @@ public class ExpressionBasedMessageSecurityMetadataSourceFactoryTests {
@Test
public void createExpressionMessageMetadataSourceNoMatch() {
Collection<ConfigAttribute> attrs = this.source.getAttributes(this.message);
assertThat(attrs).isNull();
assertThat(attrs).isEmpty();
}
@Test

View File

@ -67,8 +67,8 @@ public class DefaultMessageSecurityMetadataSourceTests {
}
@Test
public void getAttributesNull() {
assertThat(this.source.getAttributes(this.message)).isNull();
public void getAttributesEmpty() {
assertThat(this.source.getAttributes(this.message)).isEmpty();
}
@Test