method.adoc 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. [[jc-erms]]
  2. = EnableReactiveMethodSecurity
  3. Spring Security supports method security by using https://projectreactor.io/docs/core/release/reference/#context[Reactor's Context], which is set up by `ReactiveSecurityContextHolder`.
  4. The following example shows how to retrieve the currently logged in user's message:
  5. [NOTE]
  6. ====
  7. For this example to work, the return type of the method must be a `org.reactivestreams.Publisher` (that is, a `Mono` or a `Flux`).
  8. This is necessary to integrate with Reactor's `Context`.
  9. ====
  10. [[jc-enable-reactive-method-security-authorization-manager]]
  11. == EnableReactiveMethodSecurity with AuthorizationManager
  12. In Spring Security 5.8, we can enable annotation-based security using the `@EnableReactiveMethodSecurity(useAuthorizationManager=true)` annotation on any `@Configuration` instance.
  13. This improves upon `@EnableReactiveMethodSecurity` in a number of ways. `@EnableReactiveMethodSecurity(useAuthorizationManager=true)`:
  14. 1. Uses the simplified `AuthorizationManager` API instead of metadata sources, config attributes, decision managers, and voters.
  15. This simplifies reuse and customization.
  16. 2. Supports reactive return types including Kotlin coroutines.
  17. 3. Is built using native Spring AOP, removing abstractions and allowing you to use Spring AOP building blocks to customize
  18. 4. Checks for conflicting annotations to ensure an unambiguous security configuration
  19. 5. Complies with JSR-250
  20. [NOTE]
  21. ====
  22. For earlier versions, please read about similar support with <<jc-enable-reactive-method-security, @EnableReactiveMethodSecurity>>.
  23. ====
  24. For example, the following would enable Spring Security's `@PreAuthorize` annotation:
  25. .Method Security Configuration
  26. [tabs]
  27. ======
  28. Java::
  29. +
  30. [source,java,role="primary"]
  31. ----
  32. @EnableReactiveMethodSecurity(useAuthorizationManager=true)
  33. public class MethodSecurityConfig {
  34. // ...
  35. }
  36. ----
  37. ======
  38. Adding an annotation to a method (on a class or interface) would then limit the access to that method accordingly.
  39. Spring Security's native annotation support defines a set of attributes for the method.
  40. These will be passed to the various method interceptors, like `AuthorizationManagerBeforeReactiveMethodInterceptor`, for it to make the actual decision:
  41. .Method Security Annotation Usage
  42. [tabs]
  43. ======
  44. Java::
  45. +
  46. [source,java,role="primary"]
  47. ----
  48. public interface BankService {
  49. @PreAuthorize("hasRole('USER')")
  50. Mono<Account> readAccount(Long id);
  51. @PreAuthorize("hasRole('USER')")
  52. Flux<Account> findAccounts();
  53. @PreAuthorize("@func.apply(#account)")
  54. Mono<Account> post(Account account, Double amount);
  55. }
  56. ----
  57. ======
  58. In this case `hasRole` refers to the method found in `SecurityExpressionRoot` that gets invoked by the SpEL evaluation engine.
  59. `@bean` refers to a custom component you have defined, where `apply` can return `Boolean` or `Mono<Boolean>` to indicate the authorization decision.
  60. A bean like that might look something like this:
  61. .Method Security Reactive Boolean Expression
  62. [tabs]
  63. ======
  64. Java::
  65. +
  66. [source,java,role="primary"]
  67. ----
  68. @Bean
  69. public Function<Account, Mono<Boolean>> func() {
  70. return (account) -> Mono.defer(() -> Mono.just(account.getId().equals(12)));
  71. }
  72. ----
  73. ======
  74. === Customizing Authorization
  75. Spring Security's `@PreAuthorize`, `@PostAuthorize`, `@PreFilter`, and `@PostFilter` ship with rich expression-based support.
  76. [[jc-reactive-method-security-custom-granted-authority-defaults]]
  77. Also, for role-based authorization, Spring Security adds a default `ROLE_` prefix, which is uses when evaluating expressions like `hasRole`.
  78. You can configure the authorization rules to use a different prefix by exposing a `GrantedAuthorityDefaults` bean, like so:
  79. .Custom GrantedAuthorityDefaults
  80. [tabs]
  81. ======
  82. Java::
  83. +
  84. [source,java,role="primary"]
  85. ----
  86. @Bean
  87. @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  88. static GrantedAuthorityDefaults grantedAuthorityDefaults() {
  89. return new GrantedAuthorityDefaults("MYPREFIX_");
  90. }
  91. ----
  92. ======
  93. [TIP]
  94. ====
  95. We expose `GrantedAuthorityDefaults` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes.
  96. Since the `GrantedAuthorityDefaults` bean is part of internal workings of Spring Security, we should also expose it as an infrastructural bean effectively avoiding some warnings related to bean post-processing (see https://github.com/spring-projects/spring-security/issues/14751[gh-14751]).
  97. ====
  98. [[customizing-expression-handling]]
  99. === Customizing Expression Handling
  100. Or, third, you can customize how each SpEL expression is handled.
  101. To do that, you can expose a custom `MethodSecurityExpressionHandler`, like so:
  102. .Custom MethodSecurityExpressionHandler
  103. [tabs]
  104. ======
  105. Java::
  106. +
  107. [source,java,role="primary"]
  108. ----
  109. @Bean
  110. static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
  111. DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
  112. handler.setRoleHierarchy(roleHierarchy);
  113. return handler;
  114. }
  115. ----
  116. Kotlin::
  117. +
  118. [source,kotlin,role="secondary"]
  119. ----
  120. companion object {
  121. @Bean
  122. fun methodSecurityExpressionHandler(val roleHierarchy: RoleHierarchy) : MethodSecurityExpressionHandler {
  123. val handler = DefaultMethodSecurityExpressionHandler()
  124. handler.setRoleHierarchy(roleHierarchy)
  125. return handler
  126. }
  127. }
  128. ----
  129. Xml::
  130. +
  131. [source,xml,role="secondary"]
  132. ----
  133. <sec:method-security>
  134. <sec:expression-handler ref="myExpressionHandler"/>
  135. </sec:method-security>
  136. <bean id="myExpressionHandler"
  137. class="org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler">
  138. <property name="roleHierarchy" ref="roleHierarchy"/>
  139. </bean>
  140. ----
  141. ======
  142. [TIP]
  143. ====
  144. We expose `MethodSecurityExpressionHandler` using a `static` method to ensure that Spring publishes it before it initializes Spring Security's method security `@Configuration` classes
  145. ====
  146. You can also subclass xref:servlet/authorization/method-security.adoc#subclass-defaultmethodsecurityexpressionhandler[`DefaultMessageSecurityExpressionHandler`] to add your own custom authorization expressions beyond the defaults.
  147. [[jc-reactive-method-security-custom-authorization-manager]]
  148. === Custom Authorization Managers
  149. Method authorization is a combination of before- and after-method authorization.
  150. [NOTE]
  151. ====
  152. Before-method authorization is performed before the method is invoked.
  153. If that authorization denies access, the method is not invoked, and an `AccessDeniedException` is thrown.
  154. After-method authorization is performed after the method is invoked, but before the method returns to the caller.
  155. If that authorization denies access, the value is not returned, and an `AccessDeniedException` is thrown
  156. ====
  157. To recreate what adding `@EnableReactiveMethodSecurity(useAuthorizationManager=true)` does by default, you would publish the following configuration:
  158. .Full Pre-post Method Security Configuration
  159. [tabs]
  160. ======
  161. Java::
  162. +
  163. [source,java,role="primary"]
  164. ----
  165. @Configuration
  166. class MethodSecurityConfig {
  167. @Bean
  168. BeanDefinitionRegistryPostProcessor aopConfig() {
  169. return AopConfigUtils::registerAutoProxyCreatorIfNecessary;
  170. }
  171. @Bean
  172. @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  173. PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor() {
  174. return new PreFilterAuthorizationReactiveMethodInterceptor();
  175. }
  176. @Bean
  177. @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  178. AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor() {
  179. return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize();
  180. }
  181. @Bean
  182. @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  183. AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor() {
  184. return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize();
  185. }
  186. @Bean
  187. @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  188. PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor() {
  189. return new PostFilterAuthorizationReactiveMethodInterceptor();
  190. }
  191. }
  192. ----
  193. ======
  194. Notice that Spring Security's method security is built using Spring AOP.
  195. So, interceptors are invoked based on the order specified.
  196. This can be customized by calling `setOrder` on the interceptor instances like so:
  197. .Publish Custom Advisor
  198. [tabs]
  199. ======
  200. Java::
  201. +
  202. [source,java,role="primary"]
  203. ----
  204. @Bean
  205. @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  206. Advisor postFilterAuthorizationMethodInterceptor() {
  207. PostFilterAuthorizationMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor();
  208. interceptor.setOrder(AuthorizationInterceptorOrders.POST_AUTHORIZE.getOrder() - 1);
  209. return interceptor;
  210. }
  211. ----
  212. ======
  213. You may want to only support `@PreAuthorize` in your application, in which case you can do the following:
  214. .Only @PreAuthorize Configuration
  215. [tabs]
  216. ======
  217. Java::
  218. +
  219. [source,java,role="primary"]
  220. ----
  221. @Configuration
  222. class MethodSecurityConfig {
  223. @Bean
  224. BeanDefinitionRegistryPostProcessor aopConfig() {
  225. return AopConfigUtils::registerAutoProxyCreatorIfNecessary;
  226. }
  227. @Bean
  228. @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  229. Advisor preAuthorize() {
  230. return AuthorizationManagerBeforeMethodInterceptor.preAuthorize();
  231. }
  232. }
  233. ----
  234. ======
  235. Or, you may have a custom before-method `ReactiveAuthorizationManager` that you want to add to the list.
  236. In this case, you will need to tell Spring Security both the `ReactiveAuthorizationManager` and to which methods and classes your authorization manager applies.
  237. Thus, you can configure Spring Security to invoke your `ReactiveAuthorizationManager` in between `@PreAuthorize` and `@PostAuthorize` like so:
  238. .Custom Before Advisor
  239. [tabs]
  240. ======
  241. Java::
  242. +
  243. [source,java,role="primary"]
  244. ----
  245. @EnableReactiveMethodSecurity(useAuthorizationManager=true)
  246. class MethodSecurityConfig {
  247. @Bean
  248. @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  249. public Advisor customAuthorize() {
  250. JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut();
  251. pattern.setPattern("org.mycompany.myapp.service.*");
  252. ReactiveAuthorizationManager<MethodInvocation> rule = AuthorityAuthorizationManager.isAuthenticated();
  253. AuthorizationManagerBeforeReactiveMethodInterceptor interceptor = new AuthorizationManagerBeforeReactiveMethodInterceptor(pattern, rule);
  254. interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
  255. return interceptor;
  256. }
  257. }
  258. ----
  259. ======
  260. [TIP]
  261. ====
  262. You can place your interceptor in between Spring Security method interceptors using the order constants specified in `AuthorizationInterceptorsOrder`.
  263. ====
  264. The same can be done for after-method authorization.
  265. After-method authorization is generally concerned with analysing the return value to verify access.
  266. For example, you might have a method that confirms that the account requested actually belongs to the logged-in user like so:
  267. .@PostAuthorize example
  268. [tabs]
  269. ======
  270. Java::
  271. +
  272. [source,java,role="primary"]
  273. ----
  274. public interface BankService {
  275. @PreAuthorize("hasRole('USER')")
  276. @PostAuthorize("returnObject.owner == authentication.name")
  277. Mono<Account> readAccount(Long id);
  278. }
  279. ----
  280. ======
  281. You can supply your own `AuthorizationMethodInterceptor` to customize how access to the return value is evaluated.
  282. For example, if you have your own custom annotation, you can configure it like so:
  283. .Custom After Advisor
  284. [tabs]
  285. ======
  286. Java::
  287. +
  288. [source,java,role="primary"]
  289. ----
  290. @EnableReactiveMethodSecurity(useAuthorizationManager=true)
  291. class MethodSecurityConfig {
  292. @Bean
  293. @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  294. public Advisor customAuthorize(ReactiveAuthorizationManager<MethodInvocationResult> rules) {
  295. AnnotationMethodMatcher pattern = new AnnotationMethodMatcher(MySecurityAnnotation.class);
  296. AuthorizationManagerAfterReactiveMethodInterceptor interceptor = new AuthorizationManagerAfterReactiveMethodInterceptor(pattern, rules);
  297. interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1);
  298. return interceptor;
  299. }
  300. }
  301. ----
  302. ======
  303. and it will be invoked after the `@PostAuthorize` interceptor.
  304. == EnableReactiveMethodSecurity
  305. [tabs]
  306. ======
  307. Java::
  308. +
  309. [source,java,role="primary"]
  310. ----
  311. Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER");
  312. Mono<String> messageByUsername = ReactiveSecurityContextHolder.getContext()
  313. .map(SecurityContext::getAuthentication)
  314. .map(Authentication::getName)
  315. .flatMap(this::findMessageByUsername)
  316. // In a WebFlux application the `subscriberContext` is automatically setup using `ReactorContextWebFilter`
  317. .contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication));
  318. StepVerifier.create(messageByUsername)
  319. .expectNext("Hi user")
  320. .verifyComplete();
  321. ----
  322. Kotlin::
  323. +
  324. [source,kotlin,role="secondary"]
  325. ----
  326. val authentication: Authentication = TestingAuthenticationToken("user", "password", "ROLE_USER")
  327. val messageByUsername: Mono<String> = ReactiveSecurityContextHolder.getContext()
  328. .map(SecurityContext::getAuthentication)
  329. .map(Authentication::getName)
  330. .flatMap(this::findMessageByUsername) // In a WebFlux application the `subscriberContext` is automatically setup using `ReactorContextWebFilter`
  331. .contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication))
  332. StepVerifier.create(messageByUsername)
  333. .expectNext("Hi user")
  334. .verifyComplete()
  335. ----
  336. ======
  337. Where `this::findMessageByUsername` is defined as:
  338. [tabs]
  339. ======
  340. Java::
  341. +
  342. [source,java,role="primary"]
  343. ----
  344. Mono<String> findMessageByUsername(String username) {
  345. return Mono.just("Hi " + username);
  346. }
  347. ----
  348. Kotlin::
  349. +
  350. [source,kotlin,role="secondary"]
  351. ----
  352. fun findMessageByUsername(username: String): Mono<String> {
  353. return Mono.just("Hi $username")
  354. }
  355. ----
  356. ======
  357. The following minimal method security configures method security in reactive applications:
  358. [tabs]
  359. ======
  360. Java::
  361. +
  362. [source,java,role="primary"]
  363. ----
  364. @Configuration
  365. @EnableReactiveMethodSecurity
  366. public class SecurityConfig {
  367. @Bean
  368. public MapReactiveUserDetailsService userDetailsService() {
  369. User.UserBuilder userBuilder = User.withDefaultPasswordEncoder();
  370. UserDetails rob = userBuilder.username("rob")
  371. .password("rob")
  372. .roles("USER")
  373. .build();
  374. UserDetails admin = userBuilder.username("admin")
  375. .password("admin")
  376. .roles("USER","ADMIN")
  377. .build();
  378. return new MapReactiveUserDetailsService(rob, admin);
  379. }
  380. }
  381. ----
  382. Kotlin::
  383. +
  384. [source,kotlin,role="secondary"]
  385. ----
  386. @Configuration
  387. @EnableReactiveMethodSecurity
  388. class SecurityConfig {
  389. @Bean
  390. fun userDetailsService(): MapReactiveUserDetailsService {
  391. val userBuilder: User.UserBuilder = User.withDefaultPasswordEncoder()
  392. val rob = userBuilder.username("rob")
  393. .password("rob")
  394. .roles("USER")
  395. .build()
  396. val admin = userBuilder.username("admin")
  397. .password("admin")
  398. .roles("USER", "ADMIN")
  399. .build()
  400. return MapReactiveUserDetailsService(rob, admin)
  401. }
  402. }
  403. ----
  404. ======
  405. Consider the following class:
  406. [tabs]
  407. ======
  408. Java::
  409. +
  410. [source,java,role="primary"]
  411. ----
  412. @Component
  413. public class HelloWorldMessageService {
  414. @PreAuthorize("hasRole('ADMIN')")
  415. public Mono<String> findMessage() {
  416. return Mono.just("Hello World!");
  417. }
  418. }
  419. ----
  420. Kotlin::
  421. +
  422. [source,kotlin,role="secondary"]
  423. ----
  424. @Component
  425. class HelloWorldMessageService {
  426. @PreAuthorize("hasRole('ADMIN')")
  427. fun findMessage(): Mono<String> {
  428. return Mono.just("Hello World!")
  429. }
  430. }
  431. ----
  432. ======
  433. Alternatively, the following class uses Kotlin coroutines:
  434. [tabs]
  435. ======
  436. Kotlin::
  437. +
  438. [source,kotlin,role="primary"]
  439. ----
  440. @Component
  441. class HelloWorldMessageService {
  442. @PreAuthorize("hasRole('ADMIN')")
  443. suspend fun findMessage(): String {
  444. delay(10)
  445. return "Hello World!"
  446. }
  447. }
  448. ----
  449. ======
  450. Combined with our configuration above, `@PreAuthorize("hasRole('ADMIN')")` ensures that `findByMessage` is invoked only by a user with the `ADMIN` role.
  451. Note that any of the expressions in standard method security work for `@EnableReactiveMethodSecurity`.
  452. However, at this time, we support only a return type of `Boolean` or `boolean` of the expression.
  453. This means that the expression must not block.
  454. When integrating with xref:reactive/configuration/webflux.adoc#jc-webflux[WebFlux Security], the Reactor Context is automatically established by Spring Security according to the authenticated user:
  455. [tabs]
  456. ======
  457. Java::
  458. +
  459. [source,java,role="primary"]
  460. ----
  461. @Configuration
  462. @EnableWebFluxSecurity
  463. @EnableReactiveMethodSecurity
  464. public class SecurityConfig {
  465. @Bean
  466. SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) throws Exception {
  467. return http
  468. // Demonstrate that method security works
  469. // Best practice to use both for defense in depth
  470. .authorizeExchange(exchanges -> exchanges
  471. .anyExchange().permitAll()
  472. )
  473. .httpBasic(withDefaults())
  474. .build();
  475. }
  476. @Bean
  477. MapReactiveUserDetailsService userDetailsService() {
  478. User.UserBuilder userBuilder = User.withDefaultPasswordEncoder();
  479. UserDetails rob = userBuilder.username("rob")
  480. .password("rob")
  481. .roles("USER")
  482. .build();
  483. UserDetails admin = userBuilder.username("admin")
  484. .password("admin")
  485. .roles("USER","ADMIN")
  486. .build();
  487. return new MapReactiveUserDetailsService(rob, admin);
  488. }
  489. }
  490. ----
  491. Kotlin::
  492. +
  493. [source,kotlin,role="secondary"]
  494. ----
  495. @Configuration
  496. @EnableWebFluxSecurity
  497. @EnableReactiveMethodSecurity
  498. class SecurityConfig {
  499. @Bean
  500. open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
  501. return http {
  502. authorizeExchange {
  503. authorize(anyExchange, permitAll)
  504. }
  505. httpBasic { }
  506. }
  507. }
  508. @Bean
  509. fun userDetailsService(): MapReactiveUserDetailsService {
  510. val userBuilder: User.UserBuilder = User.withDefaultPasswordEncoder()
  511. val rob = userBuilder.username("rob")
  512. .password("rob")
  513. .roles("USER")
  514. .build()
  515. val admin = userBuilder.username("admin")
  516. .password("admin")
  517. .roles("USER", "ADMIN")
  518. .build()
  519. return MapReactiveUserDetailsService(rob, admin)
  520. }
  521. }
  522. ----
  523. ======
  524. You can find a complete sample in {gh-samples-url}/reactive/webflux/java/method[hellowebflux-method].