Make Spring Security work in a Tomcat app

This commit is contained in:
Dave Syer
2025-01-06 08:48:04 +00:00
parent c936d6a876
commit abcf83e8cc
19 changed files with 735 additions and 2 deletions

View File

@@ -0,0 +1,61 @@
# Spring Boot gRPC Sample
This project is a copy one of the samples from the [gRPC Spring Boot Starter](https://github.com/yidongnan/grpc-spring-boot-starter/blob/master/examples/local-grpc-server/build.gradle). Build and run any way you like to run Spring Boot. E.g:
```
$ ./mvnw spring-boot:run
...
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.0.0)
2022-12-08T05:32:24.934-08:00 INFO 551632 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication using Java 17.0.5 with PID 551632 (/home/dsyer/dev/scratch/demo/target/classes started by dsyer in /home/dsyer/dev/scratch/demo)
2022-12-08T05:32:24.938-08:00 INFO 551632 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default"
2022-12-08T05:32:25.377-08:00 WARN 551632 --- [ main] ocalVariableTableParameterNameDiscoverer : Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: net.devh.boot.grpc.server.autoconfigure.GrpcHealthServiceAutoConfiguration
2022-12-08T05:32:25.416-08:00 WARN 551632 --- [ main] ocalVariableTableParameterNameDiscoverer : Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: net.devh.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration
2022-12-08T05:32:25.425-08:00 WARN 551632 --- [ main] ocalVariableTableParameterNameDiscoverer : Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: net.devh.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration
2022-12-08T05:32:25.427-08:00 INFO 551632 --- [ main] g.s.a.GrpcServerFactoryAutoConfiguration : Detected grpc-netty: Creating NettyGrpcServerFactory
2022-12-08T05:32:25.712-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: Simple, bean: grpcServerService, class: com.example.demo.GrpcServerService
2022-12-08T05:32:25.712-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: grpc.health.v1.Health, bean: grpcHealthService, class: io.grpc.protobuf.services.HealthServiceImpl
2022-12-08T05:32:25.712-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: grpc.reflection.v1alpha.ServerReflection, bean: protoReflectionService, class: io.grpc.protobuf.services.ProtoReflectionService
2022-12-08T05:32:25.820-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.GrpcServerLifecycle : gRPC Server started, listening on address: *, port: 9090
2022-12-08T05:32:25.831-08:00 INFO 551632 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 1.264 seconds (process running for 1.623)
```
The server starts by default on port 9090. Test with [gRPCurl](https://github.com/fullstorydev/grpcurl):
```
$ grpcurl -d '{"name":"Hi"}' -plaintext localhost:9090 Simple.SayHello
{
"message": "Hello ==\u003e Hi"
}
```
## Native Image
The app compiles to a native image if the JVM is GraalVM:
```
$ ./mvnw -Pnative native:compile
$ ./target/demo
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.0.0)
2022-12-08T05:36:54.365-08:00 INFO 554359 --- [ main] com.example.demo.DemoApplication : Starting AOT-processed DemoApplication using Java 17.0.5 with PID 554359 (/home/dsyer/dev/scratch/demo/target/demo started by dsyer in /home/dsyer/dev/scratch/demo)
2022-12-08T05:36:54.366-08:00 INFO 554359 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default"
2022-12-08T05:36:54.377-08:00 INFO 554359 --- [ main] g.s.a.GrpcServerFactoryAutoConfiguration : Detected grpc-netty: Creating NettyGrpcServerFactory
2022-12-08T05:36:54.392-08:00 INFO 554359 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: Simple, bean: grpcServerService, class: com.example.demo.GrpcServerService
2022-12-08T05:36:54.392-08:00 INFO 554359 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: grpc.health.v1.Health, bean: grpcHealthService, class: io.grpc.protobuf.services.HealthServiceImpl
2022-12-08T05:36:54.392-08:00 INFO 554359 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: grpc.reflection.v1alpha.ServerReflection, bean: protoReflectionService, class: io.grpc.protobuf.services.ProtoReflectionService
2022-12-08T05:36:54.396-08:00 INFO 554359 --- [ main] n.d.b.g.s.s.GrpcServerLifecycle : gRPC Server started, listening on address: *, port: 9090
2022-12-08T05:36:54.396-08:00 INFO 554359 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 0.046 seconds (process running for 0.052)
```

View File

@@ -0,0 +1,183 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>org.springframework.grpc</groupId>
<artifactId>grpc-tomcat-secure-sample</artifactId>
<version>0.3.0-SNAPSHOT</version>
<name>Spring gRPC Server Sample</name>
<description>Demo project for Spring gRPC</description>
<url />
<licenses>
<license />
</licenses>
<developers>
<developer />
</developers>
<scm>
<connection />
<developerConnection />
<tag />
<url />
</scm>
<properties>
<java.version>17</java.version>
<spring-javaformat-maven-plugin.version>0.0.39</spring-javaformat-maven-plugin.version>
<protobuf-java.version>3.25.5</protobuf-java.version>
<grpc.version>1.63.2</grpc.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.grpc</groupId>
<artifactId>spring-grpc-dependencies</artifactId>
<version>0.3.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.grpc</groupId>
<artifactId>spring-grpc-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-services</artifactId>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-servlet-jakarta</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.grpc</groupId>
<artifactId>spring-grpc-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.1</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<buildArgs>
<buildArg>--verbose</buildArg>
</buildArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>io.spring.javaformat</groupId>
<artifactId>spring-javaformat-maven-plugin</artifactId>
<version>${spring-javaformat-maven-plugin.version}</version>
<executions>
<execution>
<?m2e ignore?>
<phase>validate</phase>
<inherited>true</inherited>
<goals>
<goal>validate</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
<configuration>
<pluginParameter>
jakarta_omit,@generated=omit
</pluginParameter>
<protocArtifact>
com.google.protobuf:protoc:${protobuf-java.version}:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>
io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories>
</project>

View File

@@ -0,0 +1,25 @@
package org.springframework.grpc.sample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@SpringBootApplication
public class GrpcServerApplication {
public static void main(String[] args) {
SpringApplication.run(GrpcServerApplication.class, args);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.httpBasic(Customizer.withDefaults())
.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated())
.csrf(csrf -> csrf.disable())
.build();
}
}

View File

@@ -0,0 +1,48 @@
package org.springframework.grpc.sample;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.grpc.sample.proto.HelloReply;
import org.springframework.grpc.sample.proto.HelloRequest;
import org.springframework.grpc.sample.proto.SimpleGrpc;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import io.grpc.stub.StreamObserver;
@Service
public class GrpcServerService extends SimpleGrpc.SimpleImplBase {
private static Log log = LogFactory.getLog(GrpcServerService.class);
@Override
public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
log.info("Hello " + req.getName());
log.info("Security " + SecurityContextHolder.getContext().getAuthentication());
HelloReply reply = HelloReply.newBuilder().setMessage("Hello ==> " + req.getName()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
@Override
public void streamHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
log.info("Hello " + req.getName());
int count = 0;
while (count < 10) {
HelloReply reply = HelloReply.newBuilder().setMessage("Hello(" + count + ") ==> " + req.getName()).build();
responseObserver.onNext(reply);
count++;
try {
Thread.sleep(1000L);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
responseObserver.onError(e);
return;
}
}
responseObserver.onCompleted();
}
}

View File

@@ -0,0 +1,23 @@
syntax = "proto3";
option java_multiple_files = true;
option java_package = "org.springframework.grpc.sample.proto";
option java_outer_classname = "HelloWorldProto";
// The greeting service definition.
service Simple {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {
}
rpc StreamHello(HelloRequest) returns (stream HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}

View File

@@ -0,0 +1,2 @@
# Ignored unless building in Nix (https://github.com/oracle/graal/issues/8639)
Args = -ENIX_LDFLAGS -ENIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu

View File

@@ -0,0 +1,5 @@
spring.application.name=grpc-tomcat-secure
server.port=9090
server.http2.enabled=true
spring.security.user.name=user
spring.security.user.password=user

View File

@@ -0,0 +1,108 @@
package org.springframework.grpc.sample;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.Base64;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Lazy;
import org.springframework.grpc.client.ChannelBuilderOptions;
import org.springframework.grpc.client.GrpcChannelFactory;
import org.springframework.grpc.sample.proto.HelloReply;
import org.springframework.grpc.sample.proto.HelloRequest;
import org.springframework.grpc.sample.proto.SimpleGrpc;
import org.springframework.grpc.server.security.GrpcSecurity;
import org.springframework.grpc.test.LocalGrpcPort;
import org.springframework.test.annotation.DirtiesContext;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
import io.grpc.Status.Code;
import io.grpc.MethodDescriptor;
import io.grpc.StatusRuntimeException;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class GrpcServerApplicationTests {
private static Log log = LogFactory.getLog(GrpcServerApplicationTests.class);
public static void main(String[] args) {
new SpringApplicationBuilder(GrpcServerApplication.class, ExtraConfiguration.class).run(args);
}
@Autowired
@Qualifier("stub")
private SimpleGrpc.SimpleBlockingStub stub;
@Autowired
@Qualifier("basic")
private SimpleGrpc.SimpleBlockingStub basic;
@Test
@DirtiesContext
void contextLoads() {
}
@Test
@DirtiesContext
void unauthenticated() {
StatusRuntimeException exception = assertThrows(StatusRuntimeException.class,
() -> stub.sayHello(HelloRequest.newBuilder().setName("Alien").build()));
assertEquals(Code.UNAUTHENTICATED, exception.getStatus().getCode());
}
@Test
@DirtiesContext
void authenticated() {
log.info("Testing");
HelloReply response = basic.sayHello(HelloRequest.newBuilder().setName("Alien").build());
assertEquals("Hello ==> Alien", response.getMessage());
}
@TestConfiguration
static class ExtraConfiguration {
@Bean
@Lazy
SimpleGrpc.SimpleBlockingStub basic(GrpcChannelFactory channels, @LocalServerPort int port) {
return SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:" + port,
ChannelBuilderOptions.defaults().withCustomizer((__, channel) -> {
channel.intercept(new ClientInterceptor() {
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
return new SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {
public void start(ClientCall.Listener<RespT> responseListener,
io.grpc.Metadata headers) {
headers.put(GrpcSecurity.AUTHORIZATION_KEY,
"Basic " + Base64.getEncoder().encodeToString("user:user".getBytes()));
super.start(responseListener, headers);
};
};
}
});
})));
}
@Bean
@Lazy
SimpleGrpc.SimpleBlockingStub stub(GrpcChannelFactory channels, @LocalServerPort int port) {
return SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:" + port));
}
}
}

View File

@@ -2,4 +2,5 @@ spring.application.name=grpc-tomcat
server.port=9090
server.http2.enabled=true
logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]
logging.level.org.springframework.security=DEBUG
management.endpoints.web.exposure.include=*

View File

@@ -19,6 +19,7 @@
<module>grpc-server</module>
<module>grpc-server-netty-shaded</module>
<module>grpc-tomcat</module>
<module>grpc-tomcat-secure</module>
<module>grpc-webflux</module>
</modules>

View File

@@ -27,6 +27,11 @@
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2024-2024 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.
*/
package org.springframework.grpc.server.security;
import io.grpc.Metadata;
/**
* Defines constants and utilities for working with gRPC security.
*
* @author Dave Syer
*/
public final class GrpcSecurity {
/**
* Constant for the Authorization header.
*/
public static final Metadata.Key<String> AUTHORIZATION_KEY = Metadata.Key.of("Authorization",
Metadata.ASCII_STRING_MARSHALLER);
private GrpcSecurity() {
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2024-2024 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.
*/
package org.springframework.grpc.server.security;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCall.Listener;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
public class SecurityContextServerInterceptor implements ServerInterceptor {
@Override
public <ReqT, RespT> Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
SecurityContext securityContext = SecurityContextHolder.getContext();
return new SecurityContextHandlerListener<ReqT, RespT>(next.startCall(call, headers), securityContext);
}
static class SecurityContextHandlerListener<ReqT, RespT> extends SimpleForwardingServerCallListener<ReqT> {
private SecurityContext securityContext;
SecurityContextHandlerListener(ServerCall.Listener<ReqT> delegate, SecurityContext securityContext) {
super(delegate);
this.securityContext = securityContext;
}
@Override
public void onMessage(ReqT message) {
SecurityContextHolder.setContext(this.securityContext);
try {
super.onMessage(message);
}
finally {
SecurityContextHolder.clearContext();
}
}
@Override
public void onHalfClose() {
SecurityContextHolder.setContext(this.securityContext);
try {
super.onHalfClose();
}
finally {
SecurityContextHolder.clearContext();
}
}
@Override
public void onReady() {
SecurityContextHolder.setContext(this.securityContext);
try {
super.onReady();
}
finally {
SecurityContextHolder.clearContext();
}
}
@Override
public void onCancel() {
super.onCancel();
SecurityContextHolder.clearContext();
}
@Override
public void onComplete() {
super.onComplete();
SecurityContextHolder.clearContext();
}
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2024-2024 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.
*/
package org.springframework.grpc.server.security;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.grpc.server.exception.GrpcExceptionHandler;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import io.grpc.Status;
public class SecurityGrpcExceptionHandler implements GrpcExceptionHandler {
private static final Log logger = LogFactory.getLog(SecurityGrpcExceptionHandler.class);
@Override
public Status handleException(Throwable exception) {
if (exception instanceof AuthenticationException) {
if (logger.isDebugEnabled()) {
logger.error("Failed to authenticate", exception);
}
return Status.UNAUTHENTICATED.withDescription(exception.getMessage());
}
if (exception instanceof AccessDeniedException) {
if (logger.isDebugEnabled()) {
logger.error("Failed to authorize", exception);
}
return Status.PERMISSION_DENIED.withDescription(exception.getMessage());
}
return null;
}
}

View File

@@ -50,6 +50,7 @@
<properties>
<spring-framework.version>6.2.1</spring-framework.version>
<spring-security.version>6.4.1</spring-security.version>
<netty.version>4.1.115.Final</netty.version>
<grpc.version>1.69.0</grpc.version>
<protobuf-java.version>3.25.5</protobuf-java.version>
@@ -87,6 +88,13 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-bom</artifactId>
<version>${spring-security.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-bom</artifactId>

View File

@@ -105,6 +105,16 @@
<artifactId>micrometer-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<optional>true</optional>
</dependency>
<!-- test dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -99,7 +99,7 @@ public class GrpcServerFactoryAutoConfiguration {
}
static class OnGrpcServletCondition extends AllNestedConditions {
public static class OnGrpcServletCondition extends AllNestedConditions {
OnGrpcServletCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
@@ -123,7 +123,7 @@ public class GrpcServerFactoryAutoConfiguration {
}
static class OnNativeGrpcServerCondition extends AnyNestedCondition {
public static class OnNativeGrpcServerCondition extends AnyNestedCondition {
OnNativeGrpcServerCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2024-2024 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.
*/
package org.springframework.grpc.autoconfigure.server.security;
import java.util.concurrent.Executors;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.grpc.autoconfigure.server.GrpcServerFactoryAutoConfiguration;
import org.springframework.grpc.autoconfigure.server.exception.GrpcExceptionHandlerAutoConfiguration;
import org.springframework.grpc.server.GlobalServerInterceptor;
import org.springframework.grpc.server.ServerBuilderCustomizer;
import org.springframework.grpc.server.exception.GrpcExceptionHandler;
import org.springframework.grpc.server.security.SecurityContextServerInterceptor;
import org.springframework.grpc.server.security.SecurityGrpcExceptionHandler;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.security.concurrent.DelegatingSecurityContextExecutor;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.web.SecurityFilterChain;
import io.grpc.ServerBuilder;
@ConditionalOnClass(ObjectPostProcessor.class)
@AutoConfiguration(before = GrpcExceptionHandlerAutoConfiguration.class, after = SecurityAutoConfiguration.class)
public class GrpcSecurityAutoConfiguration {
@Configuration(proxyBeanMethods = false)
static class ExceptionHandlerAutoConfiguration {
@Bean
public GrpcExceptionHandler accessExceptionHandler() {
return new SecurityGrpcExceptionHandler();
}
}
@ConditionalOnBean(SecurityFilterChain.class)
@Configuration(proxyBeanMethods = false)
@Conditional(GrpcServerFactoryAutoConfiguration.OnGrpcServletCondition.class)
static class GrpcSecurityConfigurerAutoConfiguration {
@Bean
@GlobalServerInterceptor
public SecurityContextServerInterceptor securityContextInterceptor() {
return new SecurityContextServerInterceptor();
}
@Bean
public <T extends ServerBuilder<T>> ServerBuilderCustomizer<T> securityContextCustomizer() {
// TODO: configure the thread pool via GrpcServerProperties?
return (serverBuilder) -> serverBuilder.executor(new DelegatingSecurityContextExecutor(
Executors.newCachedThreadPool(new CustomizableThreadFactory("grpc-server-"))));
}
}
}

View File

@@ -6,3 +6,4 @@ org.springframework.grpc.autoconfigure.server.health.GrpcServerHealthAutoConfigu
org.springframework.grpc.autoconfigure.server.GrpcServerObservationAutoConfiguration
org.springframework.grpc.autoconfigure.server.GrpcServerReflectionAutoConfiguration
org.springframework.grpc.autoconfigure.server.exception.GrpcExceptionHandlerAutoConfiguration
org.springframework.grpc.autoconfigure.server.security.GrpcSecurityAutoConfiguration