Skip to content

Commit 5a4eded

Browse files
committed
Add RSocket Support
Fixes gh-7360
1 parent 099d49a commit 5a4eded

File tree

46 files changed

+4366
-4
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+4366
-4
lines changed

config/spring-security-config.gradle

+3
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ dependencies {
1515
optional project(':spring-security-oauth2-jose')
1616
optional project(':spring-security-oauth2-resource-server')
1717
optional project(':spring-security-openid')
18+
optional project(':spring-security-rsocket')
1819
optional project(':spring-security-web')
1920
optional 'io.projectreactor:reactor-core'
2021
optional 'org.aspectj:aspectjweaver'
2122
optional 'org.springframework:spring-jdbc'
23+
optional 'org.springframework:spring-messaging'
2224
optional 'org.springframework:spring-tx'
2325
optional 'org.springframework:spring-webmvc'
2426
optional'org.springframework:spring-web'
@@ -39,6 +41,7 @@ dependencies {
3941
testCompile 'com.squareup.okhttp3:mockwebserver'
4042
testCompile 'ch.qos.logback:logback-classic'
4143
testCompile 'io.projectreactor.netty:reactor-netty'
44+
testCompile 'io.rsocket:rsocket-transport-netty'
4245
testCompile 'javax.annotation:jsr250-api:1.0'
4346
testCompile 'javax.xml.bind:jaxb-api'
4447
testCompile 'ldapsdk:ldapsdk:4.1'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.annotation.rsocket;
18+
19+
import org.springframework.context.annotation.Import;
20+
21+
import java.lang.annotation.Documented;
22+
import java.lang.annotation.ElementType;
23+
import java.lang.annotation.Retention;
24+
import java.lang.annotation.RetentionPolicy;
25+
import java.lang.annotation.Target;
26+
27+
/**
28+
* Add this annotation to a {@code Configuration} class to have Spring Security
29+
* {@link RSocketSecurity} support added.
30+
*
31+
* @author Rob Winch
32+
* @since 5.2
33+
* @see RSocketSecurity
34+
*/
35+
@Documented
36+
@Target(ElementType.TYPE)
37+
@Retention(RetentionPolicy.RUNTIME)
38+
@Import({ RSocketSecurityConfiguration.class })
39+
public @interface EnableRSocketSecurity { }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.config.annotation.rsocket;
18+
19+
import org.springframework.beans.BeansException;
20+
import org.springframework.context.ApplicationContext;
21+
import org.springframework.core.ResolvableType;
22+
import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler;
23+
import org.springframework.security.authentication.ReactiveAuthenticationManager;
24+
import org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager;
25+
import org.springframework.security.authorization.AuthorityReactiveAuthorizationManager;
26+
import org.springframework.security.authorization.AuthorizationDecision;
27+
import org.springframework.security.authorization.ReactiveAuthorizationManager;
28+
import org.springframework.security.config.Customizer;
29+
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
30+
import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;
31+
import org.springframework.security.rsocket.interceptor.PayloadInterceptor;
32+
import org.springframework.security.rsocket.interceptor.PayloadSocketAcceptorInterceptor;
33+
import org.springframework.security.rsocket.interceptor.authentication.AnonymousPayloadInterceptor;
34+
import org.springframework.security.rsocket.interceptor.authentication.AuthenticationPayloadInterceptor;
35+
import org.springframework.security.rsocket.interceptor.authentication.BearerPayloadExchangeConverter;
36+
import org.springframework.security.rsocket.interceptor.authorization.AuthorizationPayloadInterceptor;
37+
import org.springframework.security.rsocket.interceptor.authorization.PayloadExchangeMatcherReactiveAuthorizationManager;
38+
import org.springframework.security.rsocket.util.PayloadExchangeAuthorizationContext;
39+
import org.springframework.security.rsocket.util.PayloadExchangeMatcher;
40+
import org.springframework.security.rsocket.util.PayloadExchangeMatcherEntry;
41+
import org.springframework.security.rsocket.util.PayloadExchangeMatchers;
42+
import org.springframework.security.rsocket.util.RoutePayloadExchangeMatcher;
43+
import reactor.core.publisher.Mono;
44+
45+
import java.util.ArrayList;
46+
import java.util.List;
47+
48+
/**
49+
* Allows configuring RSocket based security.
50+
*
51+
* A minimal example can be found below:
52+
*
53+
* <pre class="code">
54+
* &#064;EnableRSocketSecurity
55+
* public class SecurityConfig {
56+
* // @formatter:off
57+
* &#064;Bean
58+
* PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
59+
* rsocket
60+
* .authorizePayload(authorize ->
61+
* authorize
62+
* .anyRequest().authenticated()
63+
* );
64+
* return rsocket.build();
65+
* }
66+
* // @formatter:on
67+
*
68+
* // @formatter:off
69+
* &#064;Bean
70+
* public MapReactiveUserDetailsService userDetailsService() {
71+
* UserDetails user = User.withDefaultPasswordEncoder()
72+
* .username("user")
73+
* .password("password")
74+
* .roles("USER")
75+
* .build();
76+
* return new MapReactiveUserDetailsService(user);
77+
* }
78+
* // @formatter:on
79+
* }
80+
* </pre>
81+
*
82+
* A more advanced configuration can be seen below:
83+
*
84+
* <pre class="code">
85+
* &#064;EnableRSocketSecurity
86+
* public class SecurityConfig {
87+
* // @formatter:off
88+
* &#064;Bean
89+
* PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
90+
* rsocket
91+
* .authorizePayload(authorize ->
92+
* authorize
93+
* // must have ROLE_SETUP to make connection
94+
* .setup().hasRole("SETUP")
95+
* // must have ROLE_ADMIN for routes starting with "admin."
96+
* .route("admin.*").hasRole("ADMIN")
97+
* // any other request must be authenticated for
98+
* .anyRequest().authenticated()
99+
* );
100+
* return rsocket.build();
101+
* }
102+
* // @formatter:on
103+
* }
104+
* </pre>
105+
* @author Rob Winch
106+
* @since 5.2
107+
*/
108+
public class RSocketSecurity {
109+
110+
private BasicAuthenticationSpec basicAuthSpec;
111+
112+
private JwtSpec jwtSpec;
113+
114+
private AuthorizePayloadsSpec authorizePayload;
115+
116+
private ApplicationContext context;
117+
118+
private ReactiveAuthenticationManager authenticationManager;
119+
120+
public RSocketSecurity authenticationManager(ReactiveAuthenticationManager authenticationManager) {
121+
this.authenticationManager = authenticationManager;
122+
return this;
123+
}
124+
125+
public RSocketSecurity basicAuthentication(Customizer<BasicAuthenticationSpec> basic) {
126+
if (this.basicAuthSpec == null) {
127+
this.basicAuthSpec = new BasicAuthenticationSpec();
128+
}
129+
basic.customize(this.basicAuthSpec);
130+
return this;
131+
}
132+
133+
public class BasicAuthenticationSpec {
134+
private ReactiveAuthenticationManager authenticationManager;
135+
136+
public BasicAuthenticationSpec authenticationManager(ReactiveAuthenticationManager authenticationManager) {
137+
this.authenticationManager = authenticationManager;
138+
return this;
139+
}
140+
141+
private ReactiveAuthenticationManager getAuthenticationManager() {
142+
if (this.authenticationManager == null) {
143+
return RSocketSecurity.this.authenticationManager;
144+
}
145+
return this.authenticationManager;
146+
}
147+
148+
protected AuthenticationPayloadInterceptor build() {
149+
ReactiveAuthenticationManager manager = getAuthenticationManager();
150+
return new AuthenticationPayloadInterceptor(manager);
151+
}
152+
153+
private BasicAuthenticationSpec() {}
154+
}
155+
156+
public RSocketSecurity jwt(Customizer<JwtSpec> jwt) {
157+
if (this.jwtSpec == null) {
158+
this.jwtSpec = new JwtSpec();
159+
}
160+
jwt.customize(this.jwtSpec);
161+
return this;
162+
}
163+
164+
public class JwtSpec {
165+
private ReactiveAuthenticationManager authenticationManager;
166+
167+
public JwtSpec authenticationManager(ReactiveAuthenticationManager authenticationManager) {
168+
this.authenticationManager = authenticationManager;
169+
return this;
170+
}
171+
172+
private ReactiveAuthenticationManager getAuthenticationManager() {
173+
if (this.authenticationManager != null) {
174+
return this.authenticationManager;
175+
}
176+
ReactiveJwtDecoder jwtDecoder = getBeanOrNull(ReactiveJwtDecoder.class);
177+
if (jwtDecoder != null) {
178+
this.authenticationManager = new JwtReactiveAuthenticationManager(jwtDecoder);
179+
return this.authenticationManager;
180+
}
181+
return RSocketSecurity.this.authenticationManager;
182+
}
183+
184+
protected AuthenticationPayloadInterceptor build() {
185+
ReactiveAuthenticationManager manager = getAuthenticationManager();
186+
AuthenticationPayloadInterceptor result = new AuthenticationPayloadInterceptor(manager);
187+
result.setAuthenticationConverter(new BearerPayloadExchangeConverter());
188+
return result;
189+
}
190+
191+
private JwtSpec() {}
192+
}
193+
194+
public RSocketSecurity authorizePayload(Customizer<AuthorizePayloadsSpec> authorize) {
195+
if (this.authorizePayload == null) {
196+
this.authorizePayload = new AuthorizePayloadsSpec();
197+
}
198+
authorize.customize(this.authorizePayload);
199+
return this;
200+
}
201+
202+
public PayloadSocketAcceptorInterceptor build() {
203+
PayloadSocketAcceptorInterceptor interceptor = new PayloadSocketAcceptorInterceptor(
204+
payloadInterceptors());
205+
RSocketMessageHandler handler = getBean(RSocketMessageHandler.class);
206+
interceptor.setDefaultDataMimeType(handler.getDefaultDataMimeType());
207+
interceptor.setDefaultMetadataMimeType(handler.getDefaultMetadataMimeType());
208+
return interceptor;
209+
}
210+
211+
private List<PayloadInterceptor> payloadInterceptors() {
212+
List<PayloadInterceptor> payloadInterceptors = new ArrayList<>();
213+
214+
if (this.basicAuthSpec != null) {
215+
payloadInterceptors.add(this.basicAuthSpec.build());
216+
}
217+
if (this.jwtSpec != null) {
218+
payloadInterceptors.add(this.jwtSpec.build());
219+
}
220+
payloadInterceptors.add(new AnonymousPayloadInterceptor("anonymousUser"));
221+
222+
if (this.authorizePayload != null) {
223+
payloadInterceptors.add(this.authorizePayload.build());
224+
}
225+
return payloadInterceptors;
226+
}
227+
228+
public class AuthorizePayloadsSpec {
229+
230+
private PayloadExchangeMatcherReactiveAuthorizationManager.Builder authzBuilder =
231+
PayloadExchangeMatcherReactiveAuthorizationManager.builder();
232+
233+
public Access setup() {
234+
return matcher(PayloadExchangeMatchers.setup());
235+
}
236+
237+
public Access anyRequest() {
238+
return matcher(PayloadExchangeMatchers.anyExchange());
239+
}
240+
241+
protected AuthorizationPayloadInterceptor build() {
242+
return new AuthorizationPayloadInterceptor(this.authzBuilder.build());
243+
}
244+
245+
public Access route(String pattern) {
246+
RSocketMessageHandler handler = getBean(RSocketMessageHandler.class);
247+
PayloadExchangeMatcher matcher = new RoutePayloadExchangeMatcher(
248+
handler.getMetadataExtractor(),
249+
handler.getRouteMatcher(),
250+
pattern);
251+
return matcher(matcher);
252+
}
253+
254+
public Access matcher(PayloadExchangeMatcher matcher) {
255+
return new Access(matcher);
256+
}
257+
258+
public class Access {
259+
260+
private final PayloadExchangeMatcher matcher;
261+
262+
private Access(PayloadExchangeMatcher matcher) {
263+
this.matcher = matcher;
264+
}
265+
266+
public AuthorizePayloadsSpec authenticated() {
267+
return access(AuthenticatedReactiveAuthorizationManager.authenticated());
268+
}
269+
270+
public AuthorizePayloadsSpec hasRole(String role) {
271+
return access(AuthorityReactiveAuthorizationManager.hasRole(role));
272+
}
273+
274+
public AuthorizePayloadsSpec permitAll() {
275+
return access((a, ctx) -> Mono
276+
.just(new AuthorizationDecision(true)));
277+
}
278+
279+
public AuthorizePayloadsSpec access(
280+
ReactiveAuthorizationManager<PayloadExchangeAuthorizationContext> authorization) {
281+
AuthorizePayloadsSpec.this.authzBuilder.add(new PayloadExchangeMatcherEntry<>(this.matcher, authorization));
282+
return AuthorizePayloadsSpec.this;
283+
}
284+
}
285+
}
286+
287+
private <T> T getBean(Class<T> beanClass) {
288+
if (this.context == null) {
289+
return null;
290+
}
291+
return this.context.getBean(beanClass);
292+
}
293+
294+
private <T> T getBeanOrNull(Class<T> beanClass) {
295+
return getBeanOrNull(ResolvableType.forClass(beanClass));
296+
}
297+
298+
private <T> T getBeanOrNull(ResolvableType type) {
299+
if (this.context == null) {
300+
return null;
301+
}
302+
String[] names = this.context.getBeanNamesForType(type);
303+
if (names.length == 1) {
304+
return (T) this.context.getBean(names[0]);
305+
}
306+
return null;
307+
}
308+
309+
protected void setApplicationContext(ApplicationContext applicationContext)
310+
throws BeansException {
311+
this.context = applicationContext;
312+
}
313+
}

0 commit comments

Comments
 (0)