Make Spring Security work in a Tomcat app
This commit is contained in:
61
samples/grpc-tomcat-secure/README.md
Normal file
61
samples/grpc-tomcat-secure/README.md
Normal 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)
|
||||
```
|
||||
183
samples/grpc-tomcat-secure/pom.xml
Normal file
183
samples/grpc-tomcat-secure/pom.xml
Normal 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>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
23
samples/grpc-tomcat-secure/src/main/proto/hello.proto
Normal file
23
samples/grpc-tomcat-secure/src/main/proto/hello.proto
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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=*
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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-"))));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user