mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-23 12:32:13 +00:00
Update SpEL Documentation
Closes gh-12974
This commit is contained in:
parent
a0e8fc92f5
commit
97a42ba190
@ -59,7 +59,6 @@
|
||||
** xref:servlet/authorization/index.adoc[Authorization]
|
||||
*** xref:servlet/authorization/architecture.adoc[Authorization Architecture]
|
||||
*** xref:servlet/authorization/authorize-http-requests.adoc[Authorize HTTP Requests]
|
||||
*** xref:servlet/authorization/expression-based.adoc[Expression-Based Access Control]
|
||||
*** xref:servlet/authorization/method-security.adoc[Method Security]
|
||||
*** xref:servlet/authorization/acls.adoc[Domain Object Security ACLs]
|
||||
*** xref:servlet/authorization/events.adoc[Authorization Events]
|
||||
|
@ -67,4 +67,4 @@ interface MessageRepository : PagingAndSortingRepository<Message?, Long?> {
|
||||
|
||||
This checks to see if the `Authentication.getPrincipal().getId()` is equal to the recipient of the `Message`.
|
||||
Note that this example assumes you have customized the principal to be an Object that has an id property.
|
||||
By exposing the `SecurityEvaluationContextExtension` bean, all of the xref:servlet/authorization/expression-based.adoc#common-expressions[Common Security Expressions] are available within the Query.
|
||||
By exposing the `SecurityEvaluationContextExtension` bean, all of the xref:servlet/authorization/method-security.adoc#authorization-expressions[Common Security Expressions] are available within the Query.
|
||||
|
@ -163,7 +163,7 @@ Defaults to `true`.
|
||||
|
||||
[[nsa-http-use-expressions]]
|
||||
* **use-expressions**
|
||||
Enables EL-expressions in the `access` attribute, as described in the chapter on xref:servlet/authorization/expression-based.adoc#el-access-web[expression-based access-control].
|
||||
Enables EL-expressions in the `access` attribute, as described in the chapter on xref:servlet/authorization/authorize-http-requests.adoc#authorization-expressions[expression-based access-control].
|
||||
The default value is true.
|
||||
|
||||
|
||||
|
@ -651,6 +651,82 @@ You will notice that since we are using the `hasRole` expression we do not need
|
||||
<6> Any URL that has not already been matched on is denied access.
|
||||
This is a good strategy if you do not want to accidentally forget to update your authorization rules.
|
||||
|
||||
[[authorization-expressions]]
|
||||
== Expressing Authorization with SpEL
|
||||
|
||||
While using a concrete `AuthorizationManager` is recommended, there are some cases where an expression is necessary, like with `<intercept-url>` or with JSP Taglibs.
|
||||
For that reason, this section will focus on examples from those domains.
|
||||
|
||||
Given that, let's cover Spring Security's Web Security Authorization SpEL API a bit more in depth.
|
||||
|
||||
Spring Security encapsulates all of its authorization fields and methods in a set of root objects.
|
||||
The most generic root object is called `SecurityExpressionRoot` and it forms the basis for `WebSecurityExpressionRoot`.
|
||||
Spring Security supplies this root object to `StandardEvaluationContext` when preparing to evaluate an authorization expression.
|
||||
|
||||
[[using-authorization-expression-fields-and-methods]]
|
||||
=== Using Authorization Expression Fields and Methods
|
||||
|
||||
The first thing this provides is an enhanced set of authorization fields and methods to your SpEL expressions.
|
||||
What follows is a quick overview of the most common methods:
|
||||
|
||||
* `permitAll` - The request requires no authorization to be invoked; note that in this case, xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[the `Authentication`] is never retrieved from the session
|
||||
* `denyAll` - The request is not allowed under any circumstances; note that in this case, the `Authentication` is never retrieved from the session
|
||||
* `hasAuthority` - The request requires that the `Authentication` have xref:servlet/authorization/architecture.adoc#authz-authorities[a `GrantedAuthority`] that matches the given value
|
||||
* `hasRole` - A shortcut for `hasAuthority` that prefixes `ROLE_` or whatever is configured as the default prefix
|
||||
* `hasAnyAuthority` - The request requires that the `Authentication` have a `GrantedAuthority` that matches any of the given values
|
||||
* `hasAnyRole` - A shortcut for `hasAnyAuthority` that prefixes `ROLE_` or whatever is configured as the default prefix
|
||||
* `hasPermission` - A hook into your `PermissionEvaluator` instance for doing object-level authorization
|
||||
|
||||
And here is a brief look at the most common fields:
|
||||
|
||||
* `authentication` - The `Authentication` instance associated with this method invocation
|
||||
* `principal` - The `Authentication#getPrincipal` associated with this method invocation
|
||||
|
||||
Having now learned the patterns, rules, and how they can be paired together, you should be able to understand what is going on in this more complex example:
|
||||
|
||||
.Authorize Requests Using SpEL
|
||||
====
|
||||
.Xml
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
<http>
|
||||
<intercept-url pattern="/static/**" access="permitAll"/> <1>
|
||||
<intercept-url pattern="/admin/**" access="hasRole('ADMIN')"/> <2>
|
||||
<intercept-url pattern="/db/**" access="hasAuthority('db') and hasRole('ADMIN')"/> <3>
|
||||
<intercept-url pattern="/**" access="denyAll"/> <4>
|
||||
</http>
|
||||
----
|
||||
====
|
||||
<1> We specified a URL patters that any user can access.
|
||||
Specifically, any user can access a request if the URL starts with "/static/".
|
||||
<2> Any URL that starts with "/admin/" will be restricted to users who have the role "ROLE_ADMIN".
|
||||
You will notice that since we are invoking the `hasRole` method we do not need to specify the "ROLE_" prefix.
|
||||
<3> Any URL that starts with "/db/" requires the user to have both been granted the "db" permission as well as be a "ROLE_ADMIN".
|
||||
You will notice that since we are using the `hasRole` expression we do not need to specify the "ROLE_" prefix.
|
||||
<4> Any URL that has not already been matched on is denied access.
|
||||
This is a good strategy if you do not want to accidentally forget to update your authorization rules.
|
||||
|
||||
[[using_path_parameters]]
|
||||
=== Using Path Parameters
|
||||
|
||||
Additionally, Spring Security provides a mechanism for discovering path parameters so they can also be accessed in the SpEL expression as well.
|
||||
|
||||
For example, you can access a path parameter in your SpEL expression in the following way:
|
||||
|
||||
.Authorize Request using SpEL path variable
|
||||
====
|
||||
.Xml
|
||||
[source,xml,role="primary"]
|
||||
----
|
||||
<http>
|
||||
<intercept-url pattern="/resource/{name}" access="#name == authentication.name"/>
|
||||
<intercept-url pattern="/**" access="authenticated"/>
|
||||
</http>
|
||||
----
|
||||
====
|
||||
|
||||
This expression refers to the path variable after `/resource/` and requires that it is equal to `Authentication#getName`.
|
||||
|
||||
[[remote-authorization-manager]]
|
||||
=== Use an Authorization Database, Policy Agent, or Other Service
|
||||
If you want to configure Spring Security to use a separate service for authorization, you can create your own `AuthorizationManager` and match it to `anyRequest`.
|
||||
|
@ -1,525 +0,0 @@
|
||||
|
||||
[[el-access]]
|
||||
= Expression-Based Access Control
|
||||
Spring Security 3.0 introduced the ability to use Spring Expression Language (SpEL) expressions as an authorization mechanism in addition to the existing configuration attributes and access-decision voters.
|
||||
Expression-based access control is built on the same architecture but lets complicated Boolean logic be encapsulated in a single expression.
|
||||
|
||||
|
||||
== Overview
|
||||
Spring Security uses SpEL for expression support and you should look at how that works if you are interested in understanding the topic in more depth.
|
||||
Expressions are evaluated with a "`root object`" as part of the evaluation context.
|
||||
Spring Security uses specific classes for web and method security as the root object to provide built-in expressions and access to values, such as the current principal.
|
||||
|
||||
[[el-common-built-in]]
|
||||
=== Common Built-In Expressions
|
||||
The base class for expression root objects is `SecurityExpressionRoot`.
|
||||
This provides some common expressions that are available in both web and method security:
|
||||
|
||||
[[common-expressions]]
|
||||
.Common built-in expressions
|
||||
|===
|
||||
| Expression | Description
|
||||
|
||||
| `hasRole(String role)`
|
||||
| Returns `true` if the current principal has the specified role.
|
||||
|
||||
Example: `hasRole('admin')`
|
||||
|
||||
By default, if the supplied role does not start with `ROLE_`, it is added.
|
||||
You can customize this behavior by modifying the `defaultRolePrefix` on `DefaultWebSecurityExpressionHandler`.
|
||||
|
||||
| `hasAnyRole(String... roles)`
|
||||
| Returns `true` if the current principal has any of the supplied roles (given as a comma-separated list of strings).
|
||||
|
||||
Example: `hasAnyRole('admin', 'user')`.
|
||||
|
||||
By default, if the supplied role does not start with `ROLE_`, it is added.
|
||||
You can customize this behavior by modifying the `defaultRolePrefix` on `DefaultWebSecurityExpressionHandler`.
|
||||
|
||||
| `hasAuthority(String authority)`
|
||||
| Returns `true` if the current principal has the specified authority.
|
||||
|
||||
Example: `hasAuthority('read')`
|
||||
|
||||
| `hasAnyAuthority(String... authorities)`
|
||||
| Returns `true` if the current principal has any of the supplied authorities (given as a comma-separated list of strings).
|
||||
|
||||
Example: `hasAnyAuthority('read', 'write')`.
|
||||
|
||||
| `principal`
|
||||
| Allows direct access to the principal object that represents the current user.
|
||||
|
||||
| `authentication`
|
||||
| Allows direct access to the current `Authentication` object obtained from the `SecurityContext`.
|
||||
|
||||
| `permitAll`
|
||||
| Always evaluates to `true`.
|
||||
|
||||
| `denyAll`
|
||||
| Always evaluates to `false`.
|
||||
|
||||
| `isAnonymous()`
|
||||
| Returns `true` if the current principal is an anonymous user.
|
||||
|
||||
| `isRememberMe()`
|
||||
| Returns `true` if the current principal is a remember-me user.
|
||||
|
||||
| `isAuthenticated()`
|
||||
| Returns `true` if the user is not anonymous.
|
||||
|
||||
| `isFullyAuthenticated()`
|
||||
| Returns `true` if the user is not an anonymous and is not a remember-me user.
|
||||
|
||||
| `hasPermission(Object target, Object permission)`
|
||||
| Returns `true` if the user has access to the provided target for the given permission.
|
||||
Example, `hasPermission(domainObject, 'read')`.
|
||||
|
||||
| `hasPermission(Object targetId, String targetType, Object permission)`
|
||||
| Returns `true` if the user has access to the provided target for the given permission.
|
||||
Example, `hasPermission(1, 'com.example.domain.Message', 'read')`.
|
||||
|===
|
||||
|
||||
|
||||
|
||||
[[el-access-web]]
|
||||
== Web Security Expressions
|
||||
To use expressions to secure individual URLs, you first need to set the `use-expressions` attribute in the `<http>` element to `true`.
|
||||
Spring Security then expects the `access` attributes of the `<intercept-url>` elements to contain SpEL expressions.
|
||||
Each expression should evaluate to a Boolean, defining whether access should be allowed or not.
|
||||
The following listing shows an example:
|
||||
|
||||
====
|
||||
[source,xml]
|
||||
----
|
||||
<http>
|
||||
<intercept-url pattern="/admin*"
|
||||
access="hasRole('admin') and hasIpAddress('192.168.1.0/24')"/>
|
||||
...
|
||||
</http>
|
||||
----
|
||||
====
|
||||
|
||||
Here, we have defined that the `admin` area of an application (defined by the URL pattern) should be available only to users who have the granted authority (`admin`) and whose IP address matches a local subnet.
|
||||
We have already seen the built-in `hasRole` expression in the previous section.
|
||||
The `hasIpAddress` expression is an additional built-in expression that is specific to web security.
|
||||
It is defined by the `WebSecurityExpressionRoot` class, an instance of which is used as the expression root object when evaluating web-access expressions.
|
||||
This object also directly exposed the `HttpServletRequest` object under the name `request` so that you can invoke the request directly in an expression.
|
||||
If expressions are being used, a `WebExpressionVoter` is added to the `AccessDecisionManager` that is used by the namespace.
|
||||
So, if you do not use the namespace and want to use expressions, you have to add one of these to your configuration.
|
||||
|
||||
[[el-access-web-beans]]
|
||||
=== Referring to Beans in Web Security Expressions
|
||||
|
||||
If you wish to extend the expressions that are available, you can easily refer to any Spring Bean you expose.
|
||||
For example, you could use the following, assuming you have a Bean with the name of `webSecurity` that contains the following method signature:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class WebSecurity {
|
||||
public boolean check(Authentication authentication, HttpServletRequest request) {
|
||||
...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class WebSecurity {
|
||||
fun check(authentication: Authentication?, request: HttpServletRequest?): Boolean {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
You could then refer to the method as follows:
|
||||
|
||||
.Refer to method
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
http
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.requestMatchers("/user/**").access(new WebExpressionAuthorizationManager("@webSecurity.check(authentication,request)"))
|
||||
...
|
||||
)
|
||||
----
|
||||
|
||||
.XML
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<http>
|
||||
<intercept-url pattern="/user/**"
|
||||
access="@webSecurity.check(authentication,request)"/>
|
||||
...
|
||||
</http>
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
http {
|
||||
authorizeRequests {
|
||||
authorize("/user/**", "@webSecurity.check(authentication,request)")
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[[el-access-web-path-variables]]
|
||||
=== Path Variables in Web Security Expressions
|
||||
|
||||
At times, it is nice to be able to refer to path variables within a URL.
|
||||
For example, consider a RESTful application that looks up a user by ID from a URL path in a format of `+/user/{userId}+`.
|
||||
|
||||
You can easily refer to the path variable by placing it in the pattern.
|
||||
For example, you could use the following if you had a Bean with the name of `webSecurity` that contains the following method signature:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class WebSecurity {
|
||||
public boolean checkUserId(Authentication authentication, int id) {
|
||||
...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class WebSecurity {
|
||||
fun checkUserId(authentication: Authentication?, id: Int): Boolean {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
You could then refer to the method as follows:
|
||||
|
||||
.Path Variables
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary",attrs="-attributes"]
|
||||
----
|
||||
http
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.requestMatchers("/user/{userId}/**").access(new WebExpressionAuthorizationManager("@webSecurity.checkUserId(authentication,#userId)"))
|
||||
...
|
||||
);
|
||||
----
|
||||
|
||||
.XML
|
||||
[source,xml,role="secondary",attrs="-attributes"]
|
||||
----
|
||||
<http>
|
||||
<intercept-url pattern="/user/{userId}/**"
|
||||
access="@webSecurity.checkUserId(authentication,#userId)"/>
|
||||
...
|
||||
</http>
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary",attrs="-attributes"]
|
||||
----
|
||||
http {
|
||||
authorizeRequests {
|
||||
authorize("/user/{userId}/**", "@webSecurity.checkUserId(authentication,#userId)")
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
In this configuration, URLs that match would pass in the path variable (and convert it) into the `checkUserId` method.
|
||||
For example, if the URL were `/user/123/resource`, the ID passed in would be `123`.
|
||||
|
||||
== Method Security Expressions
|
||||
Method security is a bit more complicated than a simple allow or deny rule.
|
||||
Spring Security 3.0 introduced some new annotations to allow comprehensive support for the use of expressions.
|
||||
|
||||
[[el-pre-post-annotations]]
|
||||
=== @Pre and @Post Annotations
|
||||
There are four annotations that support expression attributes to allow pre and post-invocation authorization checks and also to support filtering of submitted collection arguments or return values.
|
||||
They are `@PreAuthorize`, `@PreFilter`, `@PostAuthorize`, and `@PostFilter`.
|
||||
Their use is enabled through the `global-method-security` namespace element:
|
||||
|
||||
====
|
||||
[source,xml]
|
||||
----
|
||||
<global-method-security pre-post-annotations="enabled"/>
|
||||
----
|
||||
====
|
||||
|
||||
==== Access Control using @PreAuthorize and @PostAuthorize
|
||||
The most obviously useful annotation is `@PreAuthorize`, which decides whether a method can actually be invoked or not.
|
||||
The following example (from the {gh-samples-url}/servlet/xml/java/contacts["Contacts" sample application]) uses the `@PreAuthorize` annotation:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@PreAuthorize("hasRole('USER')")
|
||||
public void create(Contact contact);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@PreAuthorize("hasRole('USER')")
|
||||
fun create(contact: Contact?)
|
||||
----
|
||||
====
|
||||
|
||||
This means that access is allowed only for users with the `ROLE_USER` role.
|
||||
Obviously, the same thing could easily be achieved by using a traditional configuration and a simple configuration attribute for the required role.
|
||||
However, consider the following example:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@PreAuthorize("hasPermission(#contact, 'admin')")
|
||||
public void deletePermission(Contact contact, Sid recipient, Permission permission);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@PreAuthorize("hasPermission(#contact, 'admin')")
|
||||
fun deletePermission(contact: Contact?, recipient: Sid?, permission: Permission?)
|
||||
----
|
||||
====
|
||||
|
||||
Here, we actually use a method argument as part of the expression to decide whether the current user has the `admin` permission for the given contact.
|
||||
The built-in `hasPermission()` expression is linked into the Spring Security ACL module through the application context, as we <<el-permission-evaluator,see later in this section>>.
|
||||
You can access any of the method arguments by name as expression variables.
|
||||
|
||||
Spring Security can resolve the method arguments in a number of ways.
|
||||
Spring Security uses `DefaultSecurityParameterNameDiscoverer` to discover the parameter names.
|
||||
By default, the following options are tried for a method.
|
||||
|
||||
* If Spring Security's `@P` annotation is present on a single argument to the method, the value is used.
|
||||
This is useful for interfaces compiled with a JDK prior to JDK 8 (which do not contain any information about the parameter names).
|
||||
The following example uses the `@P` annotation:
|
||||
|
||||
+
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import org.springframework.security.access.method.P;
|
||||
|
||||
...
|
||||
|
||||
@PreAuthorize("#c.name == authentication.name")
|
||||
public void doSomething(@P("c") Contact contact);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.access.method.P
|
||||
|
||||
...
|
||||
|
||||
@PreAuthorize("#c.name == authentication.name")
|
||||
fun doSomething(@P("c") contact: Contact?)
|
||||
----
|
||||
====
|
||||
|
||||
+
|
||||
|
||||
Behind the scenes, this is implemented by using `AnnotationParameterNameDiscoverer`, which you can customize to support the value attribute of any specified annotation.
|
||||
|
||||
* If Spring Data's `@Param` annotation is present on at least one parameter for the method, the value is used.
|
||||
This is useful for interfaces compiled with a JDK prior to JDK 8 which do not contain any information about the parameter names.
|
||||
The following example uses the `@Param` annotation:
|
||||
+
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
...
|
||||
|
||||
@PreAuthorize("#n == authentication.name")
|
||||
Contact findContactByName(@Param("n") String name);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.data.repository.query.Param
|
||||
|
||||
...
|
||||
|
||||
@PreAuthorize("#n == authentication.name")
|
||||
fun findContactByName(@Param("n") name: String?): Contact?
|
||||
----
|
||||
====
|
||||
+
|
||||
|
||||
Behind the scenes, this is implemented by using `AnnotationParameterNameDiscoverer`, which you can customize to support the value attribute of any specified annotation.
|
||||
|
||||
* If JDK 8 was used to compile the source with the `-parameters` argument and Spring 4+ is being used, the standard JDK reflection API is used to discover the parameter names.
|
||||
This works on both classes and interfaces.
|
||||
|
||||
* Finally, if the code was compiled with the debug symbols, the parameter names are discovered by using the debug symbols.
|
||||
This does not work for interfaces, since they do not have debug information about the parameter names.
|
||||
For interfaces, annotations or the JDK 8 approach must be used.
|
||||
|
||||
.[[el-pre-post-annotations-spel]]
|
||||
Any SpEL functionality is available within the expression, so you can also access properties on the arguments.
|
||||
For example, if you wanted a particular method to allow access only to a user whose username matched that of the contact, you could write
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@PreAuthorize("#contact.name == authentication.name")
|
||||
public void doSomething(Contact contact);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@PreAuthorize("#contact.name == authentication.name")
|
||||
fun doSomething(contact: Contact?)
|
||||
----
|
||||
====
|
||||
|
||||
.[[el-pre-post-annotations-post]]
|
||||
Here, we access another built-in expression, `authentication`, which is the `Authentication` stored in the security context.
|
||||
You can also access its `principal` property directly, by using the `principal` expression.
|
||||
The value is often a `UserDetails` instance, so you might use an expression such as `principal.username` or `principal.enabled`.
|
||||
|
||||
==== Filtering using @PreFilter and @PostFilter
|
||||
Spring Security supports filtering of collections, arrays, maps, and streams by using expressions.
|
||||
This is most commonly performed on the return value of a method.
|
||||
The following example uses `@PostFilter`:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@PreAuthorize("hasRole('USER')")
|
||||
@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
|
||||
public List<Contact> getAll();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@PreAuthorize("hasRole('USER')")
|
||||
@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
|
||||
fun getAll(): List<Contact?>
|
||||
----
|
||||
====
|
||||
|
||||
When using the `@PostFilter` annotation, Spring Security iterates through the returned collection or map and removes any elements for which the supplied expression is false.
|
||||
For an array, a new array instance that contains filtered elements is returned.
|
||||
`filterObject` refers to the current object in the collection.
|
||||
When a map is used, it refers to the current `Map.Entry` object, which lets you use `filterObject.key` or `filterObject.value` in the expression.
|
||||
You can also filter before the method call by using `@PreFilter`, though this is a less common requirement.
|
||||
The syntax is the same. However, if there is more than one argument that is a collection type, you have to select one by name using the `filterTarget` property of this annotation.
|
||||
|
||||
Note that filtering is obviously not a substitute for tuning your data retrieval queries.
|
||||
If you are filtering large collections and removing many of the entries, this is likely to be inefficient.
|
||||
|
||||
|
||||
[[el-method-built-in]]
|
||||
=== Built-In Expressions
|
||||
There are some built-in expressions that are specific to method security, which we have already seen in use earlier.
|
||||
The `filterTarget` and `returnValue` values are simple enough, but the use of the `hasPermission()` expression warrants a closer look.
|
||||
|
||||
|
||||
[[el-permission-evaluator]]
|
||||
==== The PermissionEvaluator interface
|
||||
`hasPermission()` expressions are delegated to an instance of `PermissionEvaluator`.
|
||||
It is intended to bridge between the expression system and Spring Security's ACL system, letting you specify authorization constraints on domain objects, based on abstract permissions.
|
||||
It has no explicit dependencies on the ACL module, so you could swap that out for an alternative implementation if required.
|
||||
The interface has two methods:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
boolean hasPermission(Authentication authentication, Object targetDomainObject,
|
||||
Object permission);
|
||||
|
||||
boolean hasPermission(Authentication authentication, Serializable targetId,
|
||||
String targetType, Object permission);
|
||||
----
|
||||
====
|
||||
|
||||
These methods map directly to the available versions of the expression, with the exception that the first argument (the `Authentication` object) is not supplied.
|
||||
The first is used in situations where the domain object, to which access is being controlled, is already loaded.
|
||||
Then the expression returns `true` if the current user has the given permission for that object.
|
||||
The second version is used in cases where the object is not loaded but its identifier is known.
|
||||
An abstract "`type`" specifier for the domain object is also required, letting the correct ACL permissions be loaded.
|
||||
This has traditionally been the Java class of the object but does not have to be, as long as it is consistent with how the permissions are loaded.
|
||||
|
||||
To use `hasPermission()` expressions, you have to explicitly configure a `PermissionEvaluator` in your application context.
|
||||
The following example shows how to do so:
|
||||
|
||||
====
|
||||
[source,xml]
|
||||
----
|
||||
<security:global-method-security pre-post-annotations="enabled">
|
||||
<security:expression-handler ref="expressionHandler"/>
|
||||
</security:global-method-security>
|
||||
|
||||
<bean id="expressionHandler" class=
|
||||
"org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
|
||||
<property name="permissionEvaluator" ref="myPermissionEvaluator"/>
|
||||
</bean>
|
||||
----
|
||||
====
|
||||
|
||||
Where `myPermissionEvaluator` is the bean which implements `PermissionEvaluator`.
|
||||
Usually, this is the implementation from the ACL module, which is called `AclPermissionEvaluator`.
|
||||
See the {gh-samples-url}/servlet/xml/java/contacts[`Contacts`] sample application configuration for more details.
|
||||
|
||||
==== Method Security Meta Annotations
|
||||
|
||||
You can make use of meta annotations for method security to make your code more readable.
|
||||
This is especially convenient if you find that you repeat the same complex expression throughout your code base.
|
||||
For example, consider the following:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
@PreAuthorize("#contact.name == authentication.name")
|
||||
----
|
||||
====
|
||||
|
||||
Instead of repeating this everywhere, you can create a meta annotation:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@PreAuthorize("#contact.name == authentication.name")
|
||||
public @interface ContactPermission {}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@PreAuthorize("#contact.name == authentication.name")
|
||||
annotation class ContactPermission
|
||||
----
|
||||
====
|
||||
|
||||
You can use meta annotations for any of the Spring Security method security annotations.
|
||||
To remain compliant with the specification, JSR-250 annotations do not support meta annotations.
|
||||
|
@ -110,7 +110,7 @@ image::{figures}/methodsecurity.png[]
|
||||
|
||||
1. Spring AOP invokes its proxy method for `readCustomer`. Among the proxy's other advisors, it invokes an {security-api-url}org/springframework/security/authorization/method/AuthorizationManagerBeforeMethodInterceptor/html[`AuthorizationManagerBeforeMethodInterceptor`] that matches <<annotation-method-pointcuts,the `@PreAuthorize` pointcut>>
|
||||
2. The interceptor invokes {security-api-url}org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.html[`PreAuthorizeAuthorizationManager#check`]
|
||||
3. The authorization manager uses a `MethodSecurityExpressionHandler` to parse the annotation's xref:servlet/authorization/expression-based.adoc[SpEL expression] and constructs a corresponding `EvaluationContext` from a `MethodSecurityExpressionRoot` containing xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[a `Supplier<Authentication>`] and `MethodInvocation`.
|
||||
3. The authorization manager uses a `MethodSecurityExpressionHandler` to parse the annotation's <<authorization-expressions,SpEL expression>> and constructs a corresponding `EvaluationContext` from a `MethodSecurityExpressionRoot` containing xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[a `Supplier<Authentication>`] and `MethodInvocation`.
|
||||
4. The interceptor uses this context to evaluate the expression; specifically, it reads xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[the `Authentication`] from the `Supplier` and checks whether it has `permission:read` in its collection of xref:servlet/authorization/architecture.adoc#authz-authorities[authorities]
|
||||
5. If the evaluation passes, then Spring AOP proceeds to invoke the method.
|
||||
6. If not, the interceptor publishes an `AuthorizationDeniedEvent` and throws an {security-api-url}org/springframework/security/access/AccessDeniedException.html[`AccessDeniedException`] which xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[the `ExceptionTranslationFilter`] catches and returns a 403 status code to the response
|
||||
@ -365,45 +365,10 @@ fun readAccountWithWrongRoleThenAccessDenied() {
|
||||
====
|
||||
|
||||
[TIP]
|
||||
`@PreAuthorize` also can be a <<meta-annotations, meta-annotation>>, be defined <<class-or-interface-annotations,at the class or interface level>>, and use xref:servlet/authorization/expression-based.adoc[SpEL authorization expressions].
|
||||
|
||||
While `@PreAuthorize` is quite helpful for declaring needed authorities, it can also be used to evaluate more complex permissions that involve the method parameters.
|
||||
To achieve that, you can use Spring Security's `@P` annotation to remember the parameter name:
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@PreAuthorize("#username == authentication.name")
|
||||
Collection<Order> findOrders(@P("username") String username) { ... }
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,java,role="secondary"]
|
||||
----
|
||||
@PreAuthorize("#username == authentication.name")
|
||||
fun findOrders(@P("username") val username: String): Collection<Order> { ... }
|
||||
----
|
||||
====
|
||||
|
||||
Or, Spring Security also integrate with {spring-framework-reference-url}web.html#spring-web[Spring MVC] to identify parameters like so:
|
||||
|
||||
====
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@GetMapping("/orders/{username}")
|
||||
@PreAuthorize("#username == authentication.name")
|
||||
Collection<Order> findOrders(@PathVariable("username") String username) { ... }
|
||||
----
|
||||
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@GetMapping("/orders/{username}")
|
||||
@PreAuthorize("#username == authentication.name")
|
||||
fun findOrders(@PathVariable("username") val username: String): Collection<Order> { ... }
|
||||
----
|
||||
====
|
||||
`@PreAuthorize` also can be a <<meta-annotations, meta-annotation>>, be defined <<class-or-interface-annotations,at the class or interface level>>, and use <<authorization-expressions, SpEL Authorization Expressions>>.
|
||||
|
||||
While `@PreAuthorize` is quite helpful for declaring needed authorities, it can also be used to evaluate more complex <<using_method_parameters,expressions that involve the method parameters>>.
|
||||
asdf
|
||||
The above two snippets are ensuring that the user can only request orders that belong to them by comparing the username parameter to xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication#getName`].
|
||||
|
||||
The result is that the above method will only be invoked if the `username` in the request path matches the logged-in user's `name`.
|
||||
@ -488,7 +453,7 @@ fun readAccountWhenNotOwnedThenAccessDenied() {
|
||||
====
|
||||
|
||||
[TIP]
|
||||
`@PostAuthorize` also can be a <<meta-annotations,meta-annotation>>, be defined <<class-or-interface-annotations,at the class or interface level>>, and use xref:servlet/authorization/expression-based.adoc[SpEL Authorization Expressions].
|
||||
`@PostAuthorize` also can be a <<meta-annotations,meta-annotation>>, be defined <<class-or-interface-annotations,at the class or interface level>>, and use <<authorization-expressions, SpEL Authorization Expressions>>.
|
||||
|
||||
`@PostAuthorize` is particularly helpful when defending against https://cheatsheetseries.owasp.org/cheatsheets/Insecure_Direct_Object_Reference_Prevention_Cheat_Sheet.html[Insecure Direct Object Reference].
|
||||
In fact, it can be defined as a <<meta-annotations,meta-annotation>> like so:
|
||||
@ -591,7 +556,7 @@ void updateAccountsWhenOwnedThenReturns() {
|
||||
====
|
||||
|
||||
[TIP]
|
||||
`@PreFilter` also can be a <<meta-annotations,meta-annotation>>, be defined <<class-or-interface-annotations,at the class or interface level>>, and use xref:servlet/authorization/expression-based.adoc[SpEL Authorization Expressions].
|
||||
`@PreFilter` also can be a <<meta-annotations,meta-annotation>>, be defined <<class-or-interface-annotations,at the class or interface level>>, and use <<authorization-expressions, SpEL Authorization Expressions>>.
|
||||
|
||||
`@PreFilter` supports arrays, collections, maps, and streams (so long as the stream is still open).
|
||||
|
||||
@ -663,7 +628,7 @@ void readAccountsWhenOwnedThenReturns() {
|
||||
====
|
||||
|
||||
[TIP]
|
||||
`@PostFilter` also can be a <<meta-annotations,meta-annotation>>, be defined <<class-or-interface-annotations,at the class or interface level>>, and use xref:servlet/authorization/expression-based.adoc[SpEL Authorization Expressions].
|
||||
`@PostFilter` also can be a <<meta-annotations,meta-annotation>>, be defined <<class-or-interface-annotations,at the class or interface level>>, and use <<authorization-expressions, SpEL Authorization Expressions>>.
|
||||
|
||||
`@PostFilter` supports arrays, collections, maps, and streams (so long as the stream is still open).
|
||||
|
||||
@ -953,7 +918,7 @@ If your needs are more complex than that, <<authorizing-with-annotations,use ann
|
||||
[[use-programmatic-authorization]]
|
||||
== Authorizing Methods Programmatically
|
||||
|
||||
As you've already seen, there are several ways that you can specify non-trivial authorization rules using xref:servlet/authorization/expression-based.adoc[Method Security SpEL expressions].
|
||||
As you've already seen, there are several ways that you can specify non-trivial authorization rules using <<authorization-expressions, Method Security SpEL expressions>>.
|
||||
|
||||
There are a number of ways that you can instead allow your logic to be Java-based instead of SpEL-based.
|
||||
This gives use access the entire Java language for increased testability and flow control.
|
||||
@ -1257,6 +1222,179 @@ After setting up AspectJ, you can quite simply state in the `@EnableMethodSecuri
|
||||
|
||||
And the result will be that Spring Security will publish its advisors as AspectJ advice so that they can be woven in accordingly.
|
||||
|
||||
[[authorization-expressions]]
|
||||
== Expressing Authorization with SpEL
|
||||
|
||||
You've already seen several examples using SpEL, so now let's cover the API a bit more in depth.
|
||||
|
||||
Spring Security encapsulates all of its authorization fields and methods in a set of root objects.
|
||||
The most generic root object is called `SecurityExpressionRoot` and it forms the basis for `MethodSecurityExpressionRoot`.
|
||||
Spring Security supplies this root object to `MethodSecurityEvaluationContext` when preparing to evaluate an authorization expression.
|
||||
|
||||
[[using-authorization-expression-fields-and-methods]]
|
||||
=== Using Authorization Expression Fields and Methods
|
||||
|
||||
The first thing this provides is an enhanced set of authorization fields and methods to your SpEL expressions.
|
||||
What follows is a quick overview of the most common methods:
|
||||
|
||||
* `permitAll` - The method requires no authorization to be invoked; note that in this case, xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[the `Authentication`] is never retrieved from the session
|
||||
* `denyAll` - The method is not allowed under any circumstances; note that in this case, the `Authentication` is never retrieved from the session
|
||||
* `hasAuthority` - The method requires that the `Authentication` have xref:servlet/authorization/architecture.adoc#authz-authorities[a `GrantedAuthority`] that matches the given value
|
||||
* `hasRole` - A shortcut for `hasAuthority` that prefixes `ROLE_` or whatever is configured as the default prefix
|
||||
* `hasAnyAuthority` - The method requires that the `Authentication` have a `GrantedAuthority` that matches any of the given values
|
||||
* `hasAnyRole` - A shortcut for `hasAnyAuthority` that prefixes `ROLE_` or whatever is configured as the default prefix
|
||||
* `hasPermission` - A hook into your `PermissionEvaluator` instance for doing object-level authorization
|
||||
|
||||
And here is a brief look at the most common fields:
|
||||
|
||||
* `authentication` - The `Authentication` instance associated with this method invocation
|
||||
* `principal` - The `Authentication#getPrincipal` associated with this method invocation
|
||||
|
||||
Having now learned the patterns, rules, and how they can be paired together, you should be able to understand what is going on in this more complex example:
|
||||
|
||||
.Authorize Requests
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Component
|
||||
public class MyService {
|
||||
@PreAuthorize("denyAll") <1>
|
||||
MyResource myDeprecatedMethod(...);
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')") <2>
|
||||
MyResource writeResource(...)
|
||||
|
||||
@PreAuthorize("hasAuthority('db') and hasRole('ADMIN')") <3>
|
||||
MyResource deleteResource(...)
|
||||
|
||||
@PreAuthorize("principal.claims['aud'] == 'my-audience'") <4>
|
||||
MyResource readResource(...);
|
||||
|
||||
@PreAuthorize("@authz.check(authentication, #root)")
|
||||
MyResource shareResource(...);
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Component
|
||||
open class MyService {
|
||||
@PreAuthorize("denyAll") <1>
|
||||
fun myDeprecatedMethod(...): MyResource
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')") <2>
|
||||
fun writeResource(...): MyResource
|
||||
|
||||
@PreAuthorize("hasAuthority('db') and hasRole('ADMIN')") <3>
|
||||
fun deleteResource(...): MyResource
|
||||
|
||||
@PreAuthorize("principal.claims['aud'] == 'my-audience'") <4>
|
||||
fun readResource(...): MyResource
|
||||
|
||||
@PreAuthorize("@authz.check(#root)")
|
||||
fun shareResource(...): MyResource;
|
||||
}
|
||||
----
|
||||
|
||||
.Xml
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
<sec:method-security>
|
||||
<protect-pointcut expression="execution(* com.mycompany.*Service.myDeprecatedMethod(..))" access="denyAll"/> <1>
|
||||
<protect-pointcut expression="execution(* com.mycompany.*Service.writeResource(..))" access="hasRole('ADMIN')"/> <2>
|
||||
<protect-pointcut expression="execution(* com.mycompany.*Service.deleteResource(..))" access="hasAuthority('db') and hasRole('ADMIN')"/> <3>
|
||||
<protect-pointcut expression="execution(* com.mycompany.*Service.readResource(..))" access="principal.claims['aud'] == 'my-audience'"/> <4>
|
||||
<protect-pointcut expression="execution(* com.mycompany.*Service.shareResource(..))" access="@authz.check(#root)"/> <5>
|
||||
</sec:method-security>
|
||||
----
|
||||
====
|
||||
<1> This method may not be invoked by anyone for any reason
|
||||
<2> This method may only be invoked by ``Authentication``s granted the `ROLE_ADMIN` authority
|
||||
<3> This method may only be invoked by ``Authentication``s granted the `db` and `ROLE_ADMIN` authorities
|
||||
<4> This method may only be invoked by ``Princpal``s with an `aud` claim equal to "my-audience"
|
||||
<5> This method may only be invoked if the bean ``authz``'s `check` method returns `true`
|
||||
|
||||
[[using_method_parameters]]
|
||||
=== Using Method Parameters
|
||||
|
||||
Additionally, Spring Security provides a mechanism for discovering method parameters so they can also be accessed in the SpEL expression as well.
|
||||
|
||||
For a complete reference, Spring Security uses `DefaultSecurityParameterNameDiscoverer` to discover the parameter names.
|
||||
By default, the following options are tried for a method.
|
||||
|
||||
1. If Spring Security's `@P` annotation is present on a single argument to the method, the value is used.
|
||||
The following example uses the `@P` annotation:
|
||||
|
||||
+
|
||||
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import org.springframework.security.access.method.P;
|
||||
|
||||
...
|
||||
|
||||
@PreAuthorize("hasPermission(#c, 'write')")
|
||||
public void updateContact(@P("c") Contact contact);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.access.method.P
|
||||
|
||||
...
|
||||
|
||||
@PreAuthorize("hasPermission(#c, 'write')")
|
||||
fun doSomething(@P("c") contact: Contact?)
|
||||
----
|
||||
====
|
||||
+
|
||||
The intention of this expression is to require that the current `Authentication` have `write` permission specifically for this `Contact` instance.
|
||||
+
|
||||
Behind the scenes, this is implemented by using `AnnotationParameterNameDiscoverer`, which you can customize to support the value attribute of any specified annotation.
|
||||
|
||||
* If xref:servlet/integrations/data.adoc[Spring Data's] `@Param` annotation is present on at least one parameter for the method, the value is used.
|
||||
The following example uses the `@Param` annotation:
|
||||
+
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
...
|
||||
|
||||
@PreAuthorize("#n == authentication.name")
|
||||
Contact findContactByName(@Param("n") String name);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.data.repository.query.Param
|
||||
|
||||
...
|
||||
|
||||
@PreAuthorize("#n == authentication.name")
|
||||
fun findContactByName(@Param("n") name: String?): Contact?
|
||||
----
|
||||
====
|
||||
+
|
||||
The intention of this expression is to require that `name` be equal to `Authentication#getName` for the invocation to be authorized.
|
||||
+
|
||||
Behind the scenes, this is implemented by using `AnnotationParameterNameDiscoverer`, which you can customize to support the value attribute of any specified annotation.
|
||||
|
||||
* If you compile your code with the `-parameters` argument, the standard JDK reflection API is used to discover the parameter names.
|
||||
This works on both classes and interfaces.
|
||||
|
||||
* Finally, if you compile your code with debug symbols, the parameter names are discovered by using the debug symbols.
|
||||
This does not work for interfaces, since they do not have debug information about the parameter names.
|
||||
For interfaces, either annotations or the `-parameters` approach must be used.
|
||||
|
||||
[[migration-enableglobalmethodsecurity]]
|
||||
== Migrating from `@EnableGlobalMethodSecurity`
|
||||
|
||||
|
@ -147,7 +147,7 @@ The `ROLE_` prefix is a marker that indicates that a simple comparison with the
|
||||
In other words, a normal role-based check should be used.
|
||||
Access-control in Spring Security is not limited to the use of simple roles (hence the use of the prefix to differentiate between different types of security attributes).
|
||||
We see later how the interpretation can vary. The interpretation of the comma-separated values in the `access` attribute depends on the which implementation of the <<ns-access-manager,`AccessDecisionManager`>> is used.
|
||||
Since Spring Security 3.0, you can also populate the attribute with an xref:servlet/authorization/expression-based.adoc#el-access[EL expression].
|
||||
Since Spring Security 3.0, you can also populate the attribute with an xref:servlet/authorization/authorize-http-requests.adoc#authorization-expressions[EL expression].
|
||||
|
||||
|
||||
[NOTE]
|
||||
@ -387,7 +387,7 @@ If you replace a namespace filter that requires an authentication entry point (t
|
||||
== Method Security
|
||||
Since version 2.0, Spring Security has substantial support for adding security to your service layer methods.
|
||||
It provides support for JSR-250 annotation security as well as the framework's original `@Secured` annotation.
|
||||
Since version 3.0, you can also make use of xref:servlet/authorization/expression-based.adoc#el-access[expression-based annotations].
|
||||
Since version 3.0, you can also make use of xref:servlet/authorization/method-security.adoc#authorizing-with-annotations[expression-based annotations].
|
||||
You can apply security to a single bean (by using the `intercept-methods` element to decorate the bean declaration), or you can secure multiple beans across the entire service layer using the AspectJ style pointcuts.
|
||||
|
||||
[[ns-access-manager]]
|
||||
|
@ -67,4 +67,4 @@ interface MessageRepository : PagingAndSortingRepository<Message,Long> {
|
||||
|
||||
This checks to see if the `Authentication.getPrincipal().getId()` is equal to the recipient of the `Message`.
|
||||
Note that this example assumes you have customized the principal to be an Object that has an id property.
|
||||
By exposing the `SecurityEvaluationContextExtension` bean, all of the xref:servlet/authorization/expression-based.adoc#common-expressions[Common Security Expressions] are available within the Query.
|
||||
By exposing the `SecurityEvaluationContextExtension` bean, all of the xref:servlet/authorization/method-security.adoc#authorization-expressions[Common Security Expressions] are available within the Query.
|
||||
|
@ -23,7 +23,7 @@ In Spring Security 3.0, it can be used in two ways.
|
||||
The legacy options from Spring Security 2.0 are also supported, but discouraged.
|
||||
====
|
||||
|
||||
The first approach uses a xref:servlet/authorization/expression-based.adoc#el-access-web[web-security expression], which is specified in the `access` attribute of the tag.
|
||||
The first approach uses a xref:servlet/authorization/authorize-http-requests.adoc#authorization-expressions[web-security expression], which is specified in the `access` attribute of the tag.
|
||||
The expression evaluation is delegated to the `SecurityExpressionHandler<FilterInvocation>` defined in the application context (you should have web expressions enabled in your `<http>` namespace configuration to make sure this service is available).
|
||||
So, for example, you might have:
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user