parent
1b72e9d4e0
commit
56fd50fa2f
|
@ -196,7 +196,9 @@ This will be different in different companies, so you have to find it out yourse
|
|||
Before adding a Spring Security LDAP configuration to an application, it's a good idea to write a simple test using standard Java LDAP code (without Spring Security involved), and make sure you can get that to work first.
|
||||
For example, to authenticate a user, you could use the following code:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
|
||||
@Test
|
||||
|
@ -214,6 +216,22 @@ public void ldapAuthenticationIsSuccessful() throws Exception {
|
|||
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Test
|
||||
fun ldapAuthenticationIsSuccessful() {
|
||||
val env = Hashtable<String, String>()
|
||||
env[Context.SECURITY_AUTHENTICATION] = "simple"
|
||||
env[Context.SECURITY_PRINCIPAL] = "cn=joe,ou=users,dc=mycompany,dc=com"
|
||||
env[Context.PROVIDER_URL] = "ldap://mycompany.com:389/dc=mycompany,dc=com"
|
||||
env[Context.SECURITY_CREDENTIALS] = "joespassword"
|
||||
env[Context.INITIAL_CONTEXT_FACTORY] = "com.sun.jndi.ldap.LdapCtxFactory"
|
||||
val ctx = InitialLdapContext(env, null)
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
==== Session Management
|
||||
|
||||
Session management issues are a common source of forum questions.
|
||||
|
@ -498,7 +516,9 @@ To load the data from an alternative source, you must be using an explicitly dec
|
|||
You can't use the namespace.
|
||||
You would then implement `FilterInvocationSecurityMetadataSource` to load the data as you please for a particular `FilterInvocation` footnote:[The `FilterInvocation` object contains the `HttpServletRequest`, so you can obtain the URL or any other relevant information on which to base your decision on what the list of returned attributes will contain.]. A very basic outline would look something like this:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
|
||||
public class MyFilterSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
|
||||
|
@ -526,6 +546,31 @@ You would then implement `FilterInvocationSecurityMetadataSource` to load the da
|
|||
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class MyFilterSecurityMetadataSource : FilterInvocationSecurityMetadataSource {
|
||||
override fun getAttributes(securedObject: Any): List<ConfigAttribute> {
|
||||
val fi = securedObject as FilterInvocation
|
||||
val url = fi.requestUrl
|
||||
val httpMethod = fi.request.method
|
||||
|
||||
// Lookup your database (or other source) using this information and populate the
|
||||
// list of attributes
|
||||
return ArrayList()
|
||||
}
|
||||
|
||||
override fun getAllConfigAttributes(): Collection<ConfigAttribute>? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun supports(clazz: Class<*>): Boolean {
|
||||
return FilterInvocation::class.java.isAssignableFrom(clazz)
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
For more information, look at the code for `DefaultFilterInvocationSecurityMetadataSource`.
|
||||
|
||||
|
||||
|
@ -537,7 +582,9 @@ The `DefaultLdapAuthoritiesPopulator` loads the user authorities from the LDAP d
|
|||
|
||||
To use JDBC instead, you can implement the interface yourself, using whatever SQL is appropriate for your schema:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
|
||||
public class MyAuthoritiesPopulator implements LdapAuthoritiesPopulator {
|
||||
|
@ -562,6 +609,28 @@ To use JDBC instead, you can implement the interface yourself, using whatever SQ
|
|||
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class MyAuthoritiesPopulator : LdapAuthoritiesPopulator {
|
||||
@Autowired
|
||||
lateinit var template: JdbcTemplate
|
||||
|
||||
override fun getGrantedAuthorities(userData: DirContextOperations, username: String): MutableList<GrantedAuthority?> {
|
||||
return template.query("select role from roles where username = ?",
|
||||
arrayOf(username)
|
||||
) { rs, _ ->
|
||||
/**
|
||||
* We're assuming here that you're using the standard convention of using the role
|
||||
* prefix "ROLE_" to mark attributes which are supported by Spring Security's RoleVoter.
|
||||
*/
|
||||
SimpleGrantedAuthority("ROLE_" + rs.getString(1))
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
You would then add a bean of this type to your application context and inject it into the `LdapAuthenticationProvider`. This is covered in the section on configuring LDAP using explicit Spring beans in the LDAP chapter of the reference manual.
|
||||
Note that you can't use the namespace for configuration in this case.
|
||||
You should also consult the Javadoc for the relevant classes and interfaces.
|
||||
|
@ -578,7 +647,9 @@ More information can be found in the https://docs.spring.io/spring/docs/3.0.x/sp
|
|||
Normally, you would add the functionality you require to the `postProcessBeforeInitialization` method of `BeanPostProcessor`. Let's say that you want to customize the `AuthenticationDetailsSource` used by the `UsernamePasswordAuthenticationFilter`, (created by the `form-login` element). You want to extract a particular header called `CUSTOM_HEADER` from the request and make use of it while authenticating the user.
|
||||
The processor class would look like this:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
|
||||
public class CustomBeanPostProcessor implements BeanPostProcessor {
|
||||
|
@ -603,5 +674,25 @@ public class CustomBeanPostProcessor implements BeanPostProcessor {
|
|||
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class CustomBeanPostProcessor : BeanPostProcessor {
|
||||
override fun postProcessAfterInitialization(bean: Any, name: String): Any {
|
||||
if (bean is UsernamePasswordAuthenticationFilter) {
|
||||
println("********* Post-processing $name")
|
||||
bean.setAuthenticationDetailsSource(
|
||||
AuthenticationDetailsSource<HttpServletRequest, Any?> { context -> context.getHeader("CUSTOM_HEADER") })
|
||||
}
|
||||
return bean
|
||||
}
|
||||
|
||||
override fun postProcessBeforeInitialization(bean: Any, name: String?): Any {
|
||||
return bean
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
You would then register this bean in your application context.
|
||||
Spring will automatically invoke it on the beans defined in the application context.
|
||||
|
|
|
@ -340,7 +340,9 @@ Now that Spring Security obtains PGTs, you can use them to create proxy tickets
|
|||
The CAS <<samples,sample application>> contains a working example in the `ProxyTicketSampleServlet`.
|
||||
Example code can be found below:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response)
|
||||
throws ServletException, IOException {
|
||||
|
@ -358,6 +360,24 @@ String proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8");
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
protected fun doGet(request: HttpServletRequest, response: HttpServletResponse?) {
|
||||
// NOTE: The CasAuthenticationToken can also be obtained using
|
||||
// SecurityContextHolder.getContext().getAuthentication()
|
||||
val token = request.userPrincipal as CasAuthenticationToken
|
||||
// proxyTicket could be reused to make calls to the CAS service even if the
|
||||
// target url differs
|
||||
val proxyTicket = token.assertion.principal.getProxyTicketFor(targetUrl)
|
||||
|
||||
// Make a remote call using the proxy ticket
|
||||
val serviceUrl: String = targetUrl + "?ticket=" + URLEncoder.encode(proxyTicket, "UTF-8")
|
||||
val proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8")
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[[cas-pt]]
|
||||
==== Proxy Ticket Authentication
|
||||
The `CasAuthenticationProvider` distinguishes between stateful and stateless clients.
|
||||
|
|
|
@ -148,7 +148,9 @@ We do not intend to support non-long identifiers in Spring Security's ACL module
|
|||
|
||||
The following fragment of code shows how to create an `Acl`, or modify an existing `Acl`:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
// Prepare the information we'd like in our access control entry (ACE)
|
||||
ObjectIdentity oi = new ObjectIdentityImpl(Foo.class, new Long(44));
|
||||
|
@ -168,6 +170,26 @@ acl.insertAce(acl.getEntries().length, p, sid, true);
|
|||
aclService.updateAcl(acl);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val oi: ObjectIdentity = ObjectIdentityImpl(Foo::class.java, 44)
|
||||
val sid: Sid = PrincipalSid("Samantha")
|
||||
val p: Permission = BasePermission.ADMINISTRATION
|
||||
|
||||
// Create or update the relevant ACL
|
||||
var acl: MutableAcl? = null
|
||||
acl = try {
|
||||
aclService.readAclById(oi) as MutableAcl
|
||||
} catch (nfe: NotFoundException) {
|
||||
aclService.createAcl(oi)
|
||||
}
|
||||
|
||||
// Now grant some permissions via an access control entry (ACE)
|
||||
acl!!.insertAce(acl.entries.size, p, sid, true)
|
||||
aclService.updateAcl(acl)
|
||||
----
|
||||
====
|
||||
|
||||
|
||||
In the example above, we're retrieving the ACL associated with the "Foo" domain object with identifier number 44.
|
||||
|
|
|
@ -603,7 +603,9 @@ and it will be invoked after the `@PostAuthorize` interceptor.
|
|||
We can enable annotation-based security using the `@EnableGlobalMethodSecurity` annotation on any `@Configuration` instance.
|
||||
For example, the following would enable Spring Security's `@Secured` annotation.
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableGlobalMethodSecurity(securedEnabled = true)
|
||||
public class MethodSecurityConfig {
|
||||
|
@ -611,11 +613,23 @@ public class MethodSecurityConfig {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableGlobalMethodSecurity(securedEnabled = true)
|
||||
open class MethodSecurityConfig {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Adding an annotation to a method (on a class or interface) would then limit the access to that method accordingly.
|
||||
Spring Security's native annotation support defines a set of attributes for the method.
|
||||
These will be passed to the AccessDecisionManager for it to make the actual decision:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public interface BankService {
|
||||
|
||||
|
@ -630,9 +644,27 @@ public Account post(Account account, double amount);
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
interface BankService {
|
||||
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
|
||||
fun readAccount(id: Long): Account
|
||||
|
||||
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
|
||||
fun findAccounts(): Array<Account>
|
||||
|
||||
@Secured("ROLE_TELLER")
|
||||
fun post(account: Account, amount: Double): Account
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Support for JSR-250 annotations can be enabled using
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableGlobalMethodSecurity(jsr250Enabled = true)
|
||||
public class MethodSecurityConfig {
|
||||
|
@ -640,10 +672,22 @@ public class MethodSecurityConfig {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableGlobalMethodSecurity(jsr250Enabled = true)
|
||||
open class MethodSecurityConfig {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
These are standards-based and allow simple role-based constraints to be applied but do not have the power Spring Security's native annotations.
|
||||
To use the new expression-based syntax, you would use
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||
public class MethodSecurityConfig {
|
||||
|
@ -651,9 +695,21 @@ public class MethodSecurityConfig {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||
open class MethodSecurityConfig {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
and the equivalent Java code would be
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public interface BankService {
|
||||
|
||||
|
@ -668,13 +724,31 @@ public Account post(Account account, double amount);
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
interface BankService {
|
||||
@PreAuthorize("isAnonymous()")
|
||||
fun readAccount(id: Long): Account
|
||||
|
||||
@PreAuthorize("isAnonymous()")
|
||||
fun findAccounts(): Array<Account>
|
||||
|
||||
@PreAuthorize("hasAuthority('ROLE_TELLER')")
|
||||
fun post(account: Account, amount: Double): Account
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
=== GlobalMethodSecurityConfiguration
|
||||
|
||||
Sometimes you may need to perform operations that are more complicated than are possible with the `@EnableGlobalMethodSecurity` annotation allow.
|
||||
For these instances, you can extend the `GlobalMethodSecurityConfiguration` ensuring that the `@EnableGlobalMethodSecurity` annotation is present on your subclass.
|
||||
For example, if you wanted to provide a custom `MethodSecurityExpressionHandler`, you could use the following configuration:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
|
||||
|
@ -686,6 +760,19 @@ public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||
open class MethodSecurityConfig : GlobalMethodSecurityConfiguration() {
|
||||
override fun createExpressionHandler(): MethodSecurityExpressionHandler {
|
||||
// ... create and return custom MethodSecurityExpressionHandler ...
|
||||
return expressionHandler
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
For additional information about methods that can be overridden, refer to the `GlobalMethodSecurityConfiguration` Javadoc.
|
||||
|
||||
[[ns-global-method]]
|
||||
|
@ -703,7 +790,9 @@ Adding an annotation to a method (on an class or interface) would then limit the
|
|||
Spring Security's native annotation support defines a set of attributes for the method.
|
||||
These will be passed to the `AccessDecisionManager` for it to make the actual decision:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public interface BankService {
|
||||
|
||||
|
@ -718,6 +807,23 @@ public Account post(Account account, double amount);
|
|||
}
|
||||
----
|
||||
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
interface BankService {
|
||||
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
|
||||
fun readAccount(id: Long): Account
|
||||
|
||||
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
|
||||
fun findAccounts(): Array<Account>
|
||||
|
||||
@Secured("ROLE_TELLER")
|
||||
fun post(account: Account, amount: Double): Account
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Support for JSR-250 annotations can be enabled using
|
||||
|
||||
[source,xml]
|
||||
|
@ -735,7 +841,9 @@ To use the new expression-based syntax, you would use
|
|||
|
||||
and the equivalent Java code would be
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public interface BankService {
|
||||
|
||||
|
@ -750,6 +858,22 @@ public Account post(Account account, double amount);
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
interface BankService {
|
||||
@PreAuthorize("isAnonymous()")
|
||||
fun readAccount(id: Long): Account
|
||||
|
||||
@PreAuthorize("isAnonymous()")
|
||||
fun findAccounts(): Array<Account>
|
||||
|
||||
@PreAuthorize("hasAuthority('ROLE_TELLER')")
|
||||
fun post(account: Account, amount: Double): Account
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Expression-based annotations are a good choice if you need to define simple rules that go beyond checking the role names against the user's list of authorities.
|
||||
|
||||
[NOTE]
|
||||
|
|
|
@ -14,7 +14,9 @@ It wraps a delegate `Runnable` in order to initialize the `SecurityContextHolder
|
|||
It then invokes the delegate Runnable ensuring to clear the `SecurityContextHolder` afterwards.
|
||||
The `DelegatingSecurityContextRunnable` looks something like this:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public void run() {
|
||||
try {
|
||||
|
@ -26,13 +28,29 @@ try {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
fun run() {
|
||||
try {
|
||||
SecurityContextHolder.setContext(securityContext)
|
||||
delegate.run()
|
||||
} finally {
|
||||
SecurityContextHolder.clearContext()
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
While very simple, it makes it seamless to transfer the SecurityContext from one Thread to another.
|
||||
This is important since, in most cases, the SecurityContextHolder acts on a per Thread basis.
|
||||
For example, you might have used Spring Security's <<nsa-global-method-security>> support to secure one of your services.
|
||||
You can now easily transfer the `SecurityContext` of the current `Thread` to the `Thread` that invokes the secured service.
|
||||
An example of how you might do this can be found below:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Runnable originalRunnable = new Runnable() {
|
||||
public void run() {
|
||||
|
@ -47,6 +65,19 @@ DelegatingSecurityContextRunnable wrappedRunnable =
|
|||
new Thread(wrappedRunnable).start();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val originalRunnable = Runnable {
|
||||
// invoke secured service
|
||||
}
|
||||
val context: SecurityContext = SecurityContextHolder.getContext()
|
||||
val wrappedRunnable = DelegatingSecurityContextRunnable(originalRunnable, context)
|
||||
|
||||
Thread(wrappedRunnable).start()
|
||||
----
|
||||
====
|
||||
|
||||
The code above performs the following steps:
|
||||
|
||||
* Creates a `Runnable` that will be invoking our secured service.
|
||||
|
@ -59,7 +90,9 @@ Since it is quite common to create a `DelegatingSecurityContextRunnable` with th
|
|||
The following code is the same as the code above:
|
||||
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Runnable originalRunnable = new Runnable() {
|
||||
public void run() {
|
||||
|
@ -73,6 +106,19 @@ DelegatingSecurityContextRunnable wrappedRunnable =
|
|||
new Thread(wrappedRunnable).start();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val originalRunnable = Runnable {
|
||||
// invoke secured service
|
||||
}
|
||||
|
||||
val wrappedRunnable = DelegatingSecurityContextRunnable(originalRunnable)
|
||||
|
||||
Thread(wrappedRunnable).start()
|
||||
----
|
||||
====
|
||||
|
||||
The code we have is simple to use, but it still requires knowledge that we are using Spring Security.
|
||||
In the next section we will take a look at how we can utilize `DelegatingSecurityContextExecutor` to hide the fact that we are using Spring Security.
|
||||
|
||||
|
@ -85,7 +131,9 @@ The design of `DelegatingSecurityContextExecutor` is very similar to that of `De
|
|||
You can see an example of how it might be used below:
|
||||
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||
Authentication authentication =
|
||||
|
@ -106,6 +154,25 @@ public void run() {
|
|||
executor.execute(originalRunnable);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val context: SecurityContext = SecurityContextHolder.createEmptyContext()
|
||||
val authentication: Authentication =
|
||||
UsernamePasswordAuthenticationToken("user", "doesnotmatter", AuthorityUtils.createAuthorityList("ROLE_USER"))
|
||||
context.authentication = authentication
|
||||
|
||||
val delegateExecutor = SimpleAsyncTaskExecutor()
|
||||
val executor = DelegatingSecurityContextExecutor(delegateExecutor, context)
|
||||
|
||||
val originalRunnable = Runnable {
|
||||
// invoke secured service
|
||||
}
|
||||
|
||||
executor.execute(originalRunnable)
|
||||
----
|
||||
====
|
||||
|
||||
The code performs the following steps:
|
||||
|
||||
* Creates the `SecurityContext` to be used for our `DelegatingSecurityContextExecutor`.
|
||||
|
@ -118,7 +185,9 @@ In this instance, the same `SecurityContext` will be used for every Runnable sub
|
|||
This is nice if we are running background tasks that need to be run by a user with elevated privileges.
|
||||
* At this point you may be asking yourself "How does this shield my code of any knowledge of Spring Security?" Instead of creating the `SecurityContext` and the `DelegatingSecurityContextExecutor` in our own code, we can inject an already initialized instance of `DelegatingSecurityContextExecutor`.
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Autowired
|
||||
private Executor executor; // becomes an instance of our DelegatingSecurityContextExecutor
|
||||
|
@ -133,6 +202,21 @@ executor.execute(originalRunnable);
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Autowired
|
||||
lateinit var executor: Executor // becomes an instance of our DelegatingSecurityContextExecutor
|
||||
|
||||
fun submitRunnable() {
|
||||
val originalRunnable = Runnable {
|
||||
// invoke secured service
|
||||
}
|
||||
executor.execute(originalRunnable)
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Now our code is unaware that the `SecurityContext` is being propagated to the `Thread`, then the `originalRunnable` is run, and then the `SecurityContextHolder` is cleared out.
|
||||
In this example, the same user is being used to run each thread.
|
||||
What if we wanted to use the user from `SecurityContextHolder` at the time we invoked `executor.execute(Runnable)` (i.e. the currently logged in user) to process ``originalRunnable``?
|
||||
|
@ -140,13 +224,23 @@ This can be done by removing the `SecurityContext` argument from our `Delegating
|
|||
For example:
|
||||
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
SimpleAsyncTaskExecutor delegateExecutor = new SimpleAsyncTaskExecutor();
|
||||
DelegatingSecurityContextExecutor executor =
|
||||
new DelegatingSecurityContextExecutor(delegateExecutor);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val delegateExecutor = SimpleAsyncTaskExecutor()
|
||||
val executor = DelegatingSecurityContextExecutor(delegateExecutor)
|
||||
----
|
||||
====
|
||||
|
||||
Now anytime `executor.execute(Runnable)` is executed the `SecurityContext` is first obtained by the `SecurityContextHolder` and then that `SecurityContext` is used to create our `DelegatingSecurityContextRunnable`.
|
||||
This means that we are running our `Runnable` with the same user that was used to invoke the `executor.execute(Runnable)` code.
|
||||
|
||||
|
|
|
@ -8,7 +8,9 @@ If the request does not contain any cookies and Spring Security is first, the re
|
|||
The easiest way to ensure that CORS is handled first is to use the `CorsFilter`.
|
||||
Users can integrate the `CorsFilter` with Spring Security by providing a `CorsConfigurationSource` using the following:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
@ -33,6 +35,32 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
open class WebSecurityConfig : WebSecurityConfigurerAdapter() {
|
||||
override fun configure(http: HttpSecurity) {
|
||||
http {
|
||||
// by default uses a Bean by the name of corsConfigurationSource
|
||||
cors { }
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun corsConfigurationSource(): CorsConfigurationSource {
|
||||
val configuration = CorsConfiguration()
|
||||
configuration.allowedOrigins = listOf("https://example.com")
|
||||
configuration.allowedMethods = listOf("GET", "POST")
|
||||
val source = UrlBasedCorsConfigurationSource()
|
||||
source.registerCorsConfiguration("/**", configuration)
|
||||
return source
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
or in XML
|
||||
|
||||
[source,xml]
|
||||
|
@ -48,7 +76,9 @@ or in XML
|
|||
|
||||
If you are using Spring MVC's CORS support, you can omit specifying the `CorsConfigurationSource` and Spring Security will leverage the CORS configuration provided to Spring MVC.
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
@ -64,6 +94,23 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
open class WebSecurityConfig : WebSecurityConfigurerAdapter() {
|
||||
override fun configure(http: HttpSecurity) {
|
||||
http {
|
||||
// if Spring MVC is on classpath and no CorsConfigurationSource is provided,
|
||||
// Spring Security will use CORS configuration provided to Spring MVC
|
||||
cors { }
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
or in XML
|
||||
|
||||
[source,xml]
|
||||
|
|
|
@ -10,7 +10,9 @@ It is not only useful but necessary to include the user in the queries to suppor
|
|||
To use this support, add `org.springframework.security:spring-security-data` dependency and provide a bean of type `SecurityEvaluationContextExtension`.
|
||||
In Java Configuration, this would look like:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
|
||||
|
@ -18,6 +20,16 @@ public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
fun securityEvaluationContextExtension(): SecurityEvaluationContextExtension {
|
||||
return SecurityEvaluationContextExtension()
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
In XML Configuration, this would look like:
|
||||
|
||||
[source,xml]
|
||||
|
@ -31,7 +43,9 @@ In XML Configuration, this would look like:
|
|||
Now Spring Security can be used within your queries.
|
||||
For example:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Repository
|
||||
public interface MessageRepository extends PagingAndSortingRepository<Message,Long> {
|
||||
|
@ -40,6 +54,17 @@ public interface MessageRepository extends PagingAndSortingRepository<Message,Lo
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Repository
|
||||
interface MessageRepository : PagingAndSortingRepository<Message?, Long?> {
|
||||
@Query("select m from Message m where m.to.id = ?#{ principal?.id }")
|
||||
fun findInbox(pageable: Pageable?): Page<Message?>?
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
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 <<common-expressions,Common Security Expressions>> are available within the Query.
|
||||
|
|
|
@ -6,7 +6,9 @@ This can improve the performance of serializing Spring Security related classes
|
|||
|
||||
To use it, register the `SecurityJackson2Modules.getModules(ClassLoader)` with `ObjectMapper` (https://github.com/FasterXML/jackson-databind[jackson-databind]):
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
ClassLoader loader = getClass().getClassLoader();
|
||||
|
@ -19,6 +21,21 @@ SecurityContext context = new SecurityContextImpl();
|
|||
String json = mapper.writeValueAsString(context);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val mapper = ObjectMapper()
|
||||
val loader = javaClass.classLoader
|
||||
val modules: MutableList<Module> = SecurityJackson2Modules.getModules(loader)
|
||||
mapper.registerModules(modules)
|
||||
|
||||
// ... use ObjectMapper as normally ...
|
||||
val context: SecurityContext = SecurityContextImpl()
|
||||
// ...
|
||||
val json: String = mapper.writeValueAsString(context)
|
||||
----
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The following Spring Security modules provide Jackson support:
|
||||
|
|
|
@ -56,7 +56,9 @@ For a `web.xml` this means that you should place your configuration in the `Disp
|
|||
|
||||
Below `WebSecurityConfiguration` in placed in the ``DispatcherServlet``s `ApplicationContext`.
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class SecurityInitializer extends
|
||||
AbstractAnnotationConfigDispatcherServletInitializer {
|
||||
|
@ -79,6 +81,28 @@ public class SecurityInitializer extends
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class SecurityInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
|
||||
override fun getRootConfigClasses(): Array<Class<*>>? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getServletConfigClasses(): Array<Class<*>> {
|
||||
return arrayOf(
|
||||
RootConfiguration::class.java,
|
||||
WebMvcConfiguration::class.java
|
||||
)
|
||||
}
|
||||
|
||||
override fun getServletMappings(): Array<String> {
|
||||
return arrayOf("/")
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
It is always recommended to provide authorization rules by matching on the `HttpServletRequest` and method security.
|
||||
|
@ -90,15 +114,27 @@ This is what is known as https://en.wikipedia.org/wiki/Defense_in_depth_(computi
|
|||
|
||||
Consider a controller that is mapped as follows:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@RequestMapping("/admin")
|
||||
public String admin() {
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@RequestMapping("/admin")
|
||||
fun admin(): String {
|
||||
----
|
||||
====
|
||||
|
||||
If we wanted to restrict access to this controller method to admin users, a developer can provide authorization rules by matching on the `HttpServletRequest` with the following:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
protected configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
|
@ -108,6 +144,19 @@ protected configure(HttpSecurity http) throws Exception {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
override fun configure(http: HttpSecurity) {
|
||||
http {
|
||||
authorizeRequests {
|
||||
authorize(AntPathRequestMatcher("/admin"), hasRole("ADMIN"))
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
or in XML
|
||||
|
||||
[source,xml]
|
||||
|
@ -128,7 +177,9 @@ Instead, we can leverage Spring Security's `MvcRequestMatcher`.
|
|||
The following configuration will protect the same URLs that Spring MVC will match on by using Spring MVC to match on the URL.
|
||||
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
protected configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
|
@ -138,6 +189,19 @@ protected configure(HttpSecurity http) throws Exception {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
override fun configure(http: HttpSecurity) {
|
||||
http {
|
||||
authorizeRequests {
|
||||
authorize("/admin", hasRole("ADMIN"))
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
or in XML
|
||||
|
||||
[source,xml]
|
||||
|
@ -168,7 +232,9 @@ Once `AuthenticationPrincipalArgumentResolver` is properly configured, you can b
|
|||
|
||||
Consider a situation where a custom `UserDetailsService` that returns an `Object` that implements `UserDetails` and your own `CustomUser` `Object`. The `CustomUser` of the currently authenticated user could be accessed using the following code:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@RequestMapping("/messages/inbox")
|
||||
public ModelAndView findMessagesForUser() {
|
||||
|
@ -180,9 +246,24 @@ public ModelAndView findMessagesForUser() {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@RequestMapping("/messages/inbox")
|
||||
open fun findMessagesForUser(): ModelAndView {
|
||||
val authentication: Authentication = SecurityContextHolder.getContext().authentication
|
||||
val custom: CustomUser? = if (authentication as CustomUser == null) null else authentication.principal
|
||||
|
||||
// .. find messages for this user and return them ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
As of Spring Security 3.2 we can resolve the argument more directly by adding an annotation. For example:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
|
||||
|
@ -195,12 +276,25 @@ public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser cust
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@RequestMapping("/messages/inbox")
|
||||
open fun findMessagesForUser(@AuthenticationPrincipal customUser: CustomUser?): ModelAndView {
|
||||
|
||||
// .. find messages for this user and return them ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Sometimes it may be necessary to transform the principal in some way.
|
||||
For example, if `CustomUser` needed to be final it could not be extended.
|
||||
In this situation the `UserDetailsService` might returns an `Object` that implements `UserDetails` and provides a method named `getCustomUser` to access `CustomUser`.
|
||||
For example, it might look like:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class CustomUserUserDetails extends User {
|
||||
// ...
|
||||
|
@ -210,9 +304,25 @@ public class CustomUserUserDetails extends User {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class CustomUserUserDetails(
|
||||
username: String?,
|
||||
password: String?,
|
||||
authorities: MutableCollection<out GrantedAuthority>?
|
||||
) : User(username, password, authorities) {
|
||||
// ...
|
||||
val customUser: CustomUser? = null
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
We could then access the `CustomUser` using a https://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html[SpEL expression] that uses `Authentication.getPrincipal()` as the root object:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
|
||||
|
@ -225,10 +335,27 @@ public ModelAndView findMessagesForUser(@AuthenticationPrincipal(expression = "c
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
|
||||
// ...
|
||||
|
||||
@RequestMapping("/messages/inbox")
|
||||
open fun findMessagesForUser(@AuthenticationPrincipal(expression = "customUser") customUser: CustomUser?): ModelAndView {
|
||||
|
||||
// .. find messages for this user and return them ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
We can also refer to Beans in our SpEL expressions.
|
||||
For example, the following could be used if we were using JPA to manage our Users and we wanted to modify and save a property on the current user.
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
|
||||
|
@ -245,13 +372,36 @@ public ModelAndView updateName(@AuthenticationPrincipal(expression = "@jpaEntity
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
|
||||
// ...
|
||||
|
||||
@PutMapping("/users/self")
|
||||
open fun updateName(
|
||||
@AuthenticationPrincipal(expression = "@jpaEntityManager.merge(#this)") attachedCustomUser: CustomUser,
|
||||
@RequestParam firstName: String?
|
||||
): ModelAndView {
|
||||
|
||||
// change the firstName on an attached instance which will be persisted to the database
|
||||
attachedCustomUser.setFirstName(firstName)
|
||||
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
We can further remove our dependency on Spring Security by making `@AuthenticationPrincipal` a meta annotation on our own annotation.
|
||||
Below we demonstrate how we could do this on an annotation named `@CurrentUser`.
|
||||
|
||||
NOTE: It is important to realize that in order to remove the dependency on Spring Security, it is the consuming application that would create `@CurrentUser`.
|
||||
This step is not strictly required, but assists in isolating your dependency to Spring Security to a more central location.
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Target({ElementType.PARAMETER, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
|
@ -260,10 +410,23 @@ This step is not strictly required, but assists in isolating your dependency to
|
|||
public @interface CurrentUser {}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@MustBeDocumented
|
||||
@AuthenticationPrincipal
|
||||
annotation class CurrentUser
|
||||
----
|
||||
====
|
||||
|
||||
Now that `@CurrentUser` has been specified, we can use it to signal to resolve our `CustomUser` of the currently authenticated user.
|
||||
We have also isolated our dependency on Spring Security to a single file.
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@RequestMapping("/messages/inbox")
|
||||
public ModelAndView findMessagesForUser(@CurrentUser CustomUser customUser) {
|
||||
|
@ -272,6 +435,17 @@ public ModelAndView findMessagesForUser(@CurrentUser CustomUser customUser) {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@RequestMapping("/messages/inbox")
|
||||
open fun findMessagesForUser(@CurrentUser customUser: CustomUser?): ModelAndView {
|
||||
|
||||
// .. find messages for this user and return them ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
|
||||
[[mvc-async]]
|
||||
=== Spring MVC Async Integration
|
||||
|
@ -280,7 +454,9 @@ Spring Web MVC 3.2+ has excellent support for https://docs.spring.io/spring/docs
|
|||
With no additional configuration, Spring Security will automatically setup the `SecurityContext` to the `Thread` that invokes a `Callable` returned by your controllers.
|
||||
For example, the following method will automatically have its `Callable` invoked with the `SecurityContext` that was available when the `Callable` was created:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@RequestMapping(method=RequestMethod.POST)
|
||||
public Callable<String> processUpload(final MultipartFile file) {
|
||||
|
@ -294,6 +470,19 @@ return new Callable<String>() {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@RequestMapping(method = [RequestMethod.POST])
|
||||
open fun processUpload(file: MultipartFile?): Callable<String> {
|
||||
return Callable {
|
||||
// ...
|
||||
"someView"
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
.Associating SecurityContext to Callable's
|
||||
====
|
||||
|
@ -360,7 +549,9 @@ If you use XML based configuration, you must add this yourself.
|
|||
|
||||
Once `CsrfTokenArgumentResolver` is properly configured, you can expose the `CsrfToken` to your static HTML based application.
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@RestController
|
||||
public class CsrfController {
|
||||
|
@ -372,5 +563,18 @@ public class CsrfController {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@RestController
|
||||
class CsrfController {
|
||||
@RequestMapping("/csrf")
|
||||
fun csrf(token: CsrfToken): CsrfToken {
|
||||
return token
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
It is important to keep the `CsrfToken` a secret from other domains.
|
||||
This means if you are using https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS[Cross Origin Sharing (CORS)], you should **NOT** expose the `CsrfToken` to any external domains.
|
||||
|
|
|
@ -24,7 +24,9 @@ For example, you might have created a custom `UserDetailsService` that returns a
|
|||
You could obtain this information with the following:
|
||||
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Authentication auth = httpServletRequest.getUserPrincipal();
|
||||
// assume integrated custom UserDetails called MyCustomUserDetails
|
||||
|
@ -34,6 +36,18 @@ String firstName = userDetails.getFirstName();
|
|||
String lastName = userDetails.getLastName();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val auth: Authentication = httpServletRequest.getUserPrincipal()
|
||||
// assume integrated custom UserDetails called MyCustomUserDetails
|
||||
// by default, typically instance of UserDetails
|
||||
val userDetails: MyCustomUserDetails = auth.principal as MyCustomUserDetails
|
||||
val firstName: String = userDetails.firstName
|
||||
val lastName: String = userDetails.lastName
|
||||
----
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
It should be noted that it is typically bad practice to perform so much logic throughout your application.
|
||||
|
@ -46,11 +60,20 @@ The https://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.h
|
|||
Typically users should not pass in the "ROLE_" prefix into this method since it is added automatically.
|
||||
For example, if you want to determine if the current user has the authority "ROLE_ADMIN", you could use the following:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
boolean isAdmin = httpServletRequest.isUserInRole("ADMIN");
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val isAdmin: Boolean = httpServletRequest.isUserInRole("ADMIN")
|
||||
----
|
||||
====
|
||||
|
||||
This might be useful to determine if certain UI components should be displayed.
|
||||
For example, you might display admin links only if the current user is an admin.
|
||||
|
||||
|
@ -70,7 +93,9 @@ If they are not authenticated, the configured AuthenticationEntryPoint will be u
|
|||
The https://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#login%28java.lang.String,%20java.lang.String%29[HttpServletRequest.login(String,String)] method can be used to authenticate the user with the current `AuthenticationManager`.
|
||||
For example, the following would attempt to authenticate with the username "user" and password "password":
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
try {
|
||||
httpServletRequest.login("user","password");
|
||||
|
@ -79,6 +104,17 @@ httpServletRequest.login("user","password");
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
try {
|
||||
httpServletRequest.login("user", "password")
|
||||
} catch (ex: ServletException) {
|
||||
// fail to authenticate
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
It is not necessary to catch the ServletException if you want Spring Security to process the failed authentication attempt.
|
||||
|
@ -99,7 +135,9 @@ The https://docs.oracle.com/javaee/6/api/javax/servlet/AsyncContext.html#start%2
|
|||
Using Spring Security's concurrency support, Spring Security overrides the AsyncContext.start(Runnable) to ensure that the current SecurityContext is used when processing the Runnable.
|
||||
For example, the following would output the current user's Authentication:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
final AsyncContext async = httpServletRequest.startAsync();
|
||||
async.start(new Runnable() {
|
||||
|
@ -117,6 +155,24 @@ async.start(new Runnable() {
|
|||
});
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val async: AsyncContext = httpServletRequest.startAsync()
|
||||
async.start {
|
||||
val authentication: Authentication = SecurityContextHolder.getContext().authentication
|
||||
try {
|
||||
val asyncResponse = async.response as HttpServletResponse
|
||||
asyncResponse.status = HttpServletResponse.SC_OK
|
||||
asyncResponse.writer.write(String.valueOf(authentication))
|
||||
async.complete()
|
||||
} catch (ex: Exception) {
|
||||
throw RuntimeException(ex)
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[[servletapi-async]]
|
||||
==== Async Servlet Support
|
||||
If you are using Java Based configuration, you are ready to go.
|
||||
|
@ -161,7 +217,9 @@ Prior to Spring Security 3.2, the SecurityContext from the SecurityContextHolder
|
|||
This can cause issues in an Async environment.
|
||||
For example, consider the following:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
httpServletRequest.startAsync();
|
||||
new Thread("AsyncThread") {
|
||||
|
@ -180,6 +238,26 @@ new Thread("AsyncThread") {
|
|||
}.start();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
httpServletRequest.startAsync()
|
||||
object : Thread("AsyncThread") {
|
||||
override fun run() {
|
||||
try {
|
||||
// Do work
|
||||
TimeUnit.SECONDS.sleep(1)
|
||||
|
||||
// Write to and commit the httpServletResponse
|
||||
httpServletResponse.outputStream.flush()
|
||||
} catch (ex: java.lang.Exception) {
|
||||
ex.printStackTrace()
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
----
|
||||
====
|
||||
|
||||
The issue is that this Thread is not known to Spring Security, so the SecurityContext is not propagated to it.
|
||||
This means when we commit the HttpServletResponse there is no SecurityContext.
|
||||
When Spring Security automatically saved the SecurityContext on committing the HttpServletResponse it would lose our logged in user.
|
||||
|
|
|
@ -18,7 +18,9 @@ Spring Security 4.0 has introduced authorization support for WebSockets through
|
|||
To configure authorization using Java Configuration, simply extend the `AbstractSecurityWebSocketMessageBrokerConfigurer` and configure the `MessageSecurityMetadataSourceRegistry`.
|
||||
For example:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
public class WebSocketSecurityConfig
|
||||
|
@ -31,6 +33,18 @@ public class WebSocketSecurityConfig
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() { // <1> <2>
|
||||
override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) {
|
||||
messages.simpDestMatchers("/user/**").authenticated() // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
This will ensure that:
|
||||
|
||||
<1> Any inbound CONNECT message requires a valid CSRF token to enforce <<websocket-sameorigin,Same Origin Policy>>
|
||||
|
@ -70,7 +84,9 @@ Spring Security 4.0 has introduced authorization support for WebSockets through
|
|||
To configure authorization using Java Configuration, simply extend the `AbstractSecurityWebSocketMessageBrokerConfigurer` and configure the `MessageSecurityMetadataSourceRegistry`.
|
||||
For example:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
|
||||
|
@ -89,6 +105,24 @@ public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBro
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() {
|
||||
override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) {
|
||||
messages
|
||||
.nullDestMatcher().authenticated() // <1>
|
||||
.simpSubscribeDestMatchers("/user/queue/errors").permitAll() // <2>
|
||||
.simpDestMatchers("/app/**").hasRole("USER") // <3>
|
||||
.simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") // <4>
|
||||
.simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() // <5>
|
||||
.anyMessage().denyAll() // <6>
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
This will ensure that:
|
||||
|
||||
<1> Any message without a destination (i.e. anything other than Message type of MESSAGE or SUBSCRIBE) will require the user to be authenticated
|
||||
|
@ -232,7 +266,9 @@ var token = "${_csrf.token}";
|
|||
If you are using static HTML, you can expose the `CsrfToken` on a REST endpoint.
|
||||
For example, the following would expose the `CsrfToken` on the URL /csrf
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@RestController
|
||||
public class CsrfController {
|
||||
|
@ -244,6 +280,19 @@ public class CsrfController {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@RestController
|
||||
class CsrfController {
|
||||
@RequestMapping("/csrf")
|
||||
fun csrf(token: CsrfToken): CsrfToken {
|
||||
return token
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The JavaScript can make a REST call to the endpoint and use the response to populate the headerName and the token.
|
||||
|
||||
We can now include the token in our Stomp client.
|
||||
|
@ -266,7 +315,9 @@ stompClient.connect(headers, function(frame) {
|
|||
If you want to allow other domains to access your site, you can disable Spring Security's protection.
|
||||
For example, in Java Configuration you can use the following:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
|
||||
|
@ -280,6 +331,21 @@ public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBro
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() {
|
||||
|
||||
// ...
|
||||
|
||||
override fun sameOriginDisabled(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
|
||||
[[websocket-sockjs]]
|
||||
=== Working with SockJS
|
||||
|
@ -311,7 +377,9 @@ For example, the following will instruct Spring Security to use "X-Frame-Options
|
|||
|
||||
Similarly, you can customize frame options to use the same origin within Java Configuration using the following:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
public class WebSecurityConfig extends
|
||||
|
@ -330,6 +398,25 @@ public class WebSecurityConfig extends
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
open class WebSecurityConfig : WebSecurityConfigurerAdapter() {
|
||||
override fun configure(http: HttpSecurity) {
|
||||
http {
|
||||
// ...
|
||||
headers {
|
||||
frameOptions {
|
||||
sameOrigin = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[[websocket-sockjs-csrf]]
|
||||
==== SockJS & Relaxing CSRF
|
||||
|
||||
|
@ -347,7 +434,9 @@ We can easily achieve this by providing a CSRF RequestMatcher.
|
|||
Our Java Configuration makes this extremely easy.
|
||||
For example, if our stomp endpoint is "/chat" we can disable CSRF protection for only URLs that start with "/chat/" using the following configuration:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
|
@ -373,6 +462,30 @@ public class WebSecurityConfig
|
|||
...
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
open class WebSecurityConfig : WebSecurityConfigurerAdapter() {
|
||||
override fun configure(http: HttpSecurity) {
|
||||
http {
|
||||
csrf {
|
||||
ignoringAntMatchers("/chat/**")
|
||||
}
|
||||
headers {
|
||||
frameOptions {
|
||||
sameOrigin = true
|
||||
}
|
||||
}
|
||||
authorizeRequests {
|
||||
// ...
|
||||
}
|
||||
// ...
|
||||
|
||||
----
|
||||
====
|
||||
|
||||
If we are using XML based configuration, we can use the <<nsa-csrf-request-matcher-ref,csrf@request-matcher-ref>>.
|
||||
For example:
|
||||
|
||||
|
|
|
@ -502,6 +502,51 @@ public class OAuth2LoginConfig {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
open class OAuth2LoginConfig {
|
||||
@EnableWebSecurity
|
||||
open class OAuth2LoginSecurityConfig : WebSecurityConfigurerAdapter() {
|
||||
override fun configure(http: HttpSecurity) {
|
||||
http {
|
||||
authorizeRequests {
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
oauth2Login { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun clientRegistrationRepository(): ClientRegistrationRepository {
|
||||
return InMemoryClientRegistrationRepository(googleClientRegistration())
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun authorizedClientService(
|
||||
clientRegistrationRepository: ClientRegistrationRepository?
|
||||
): OAuth2AuthorizedClientService {
|
||||
return InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository)
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun authorizedClientRepository(
|
||||
authorizedClientService: OAuth2AuthorizedClientService?
|
||||
): OAuth2AuthorizedClientRepository {
|
||||
return AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService)
|
||||
}
|
||||
|
||||
private fun googleClientRegistration(): ClientRegistration {
|
||||
return CommonOAuth2Provider.GOOGLE.getBuilder("google")
|
||||
.clientId("google-client-id")
|
||||
.clientSecret("google-client-secret")
|
||||
.build()
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
.Xml
|
||||
[source,xml,role="secondary"]
|
||||
----
|
||||
|
|
|
@ -196,13 +196,27 @@ The resulting `Authentication#getPrincipal` is a Spring Security `Saml2Authentic
|
|||
|
||||
Any class that uses both Spring Security and OpenSAML should statically initialize `OpenSamlInitializationService` at the beginning of the class, like so:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
static {
|
||||
OpenSamlInitializationService.initialize();
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
companion object {
|
||||
init {
|
||||
OpenSamlInitializationService.initialize()
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
This replaces OpenSAML's `InitializationService#initialize`.
|
||||
|
||||
Occasionally, it can be valuable to customize how OpenSAML builds, marshalls, and unmarshalls SAML objects.
|
||||
|
@ -211,7 +225,9 @@ In these circumstances, you may instead want to call `OpenSamlInitializationServ
|
|||
For example, when sending an unsigned AuthNRequest, you may want to force reauthentication.
|
||||
In that case, you can register your own `AuthnRequestMarshaller`, like so:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
static {
|
||||
OpenSamlInitializationService.requireInitialize(factory -> {
|
||||
|
@ -237,6 +253,34 @@ static {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
companion object {
|
||||
init {
|
||||
OpenSamlInitializationService.requireInitialize {
|
||||
val marshaller = object : AuthnRequestMarshaller() {
|
||||
override fun marshall(xmlObject: XMLObject, element: Element): Element {
|
||||
configureAuthnRequest(xmlObject as AuthnRequest)
|
||||
return super.marshall(xmlObject, element)
|
||||
}
|
||||
|
||||
override fun marshall(xmlObject: XMLObject, document: Document): Element {
|
||||
configureAuthnRequest(xmlObject as AuthnRequest)
|
||||
return super.marshall(xmlObject, document)
|
||||
}
|
||||
|
||||
private fun configureAuthnRequest(authnRequest: AuthnRequest) {
|
||||
authnRequest.isForceAuthn = true
|
||||
}
|
||||
}
|
||||
it.marshallerFactory.registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller)
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The `requireInitialize` method may only be called once per application instance.
|
||||
|
||||
[[servlet-saml2login-sansboot]]
|
||||
|
@ -327,7 +371,8 @@ For example, you can look up the asserting party's configuration by hitting its
|
|||
|
||||
.Relying Party Registration Repository
|
||||
====
|
||||
[source,java]
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Value("${metadata.location}")
|
||||
String assertingPartyMetadataLocation;
|
||||
|
@ -341,13 +386,30 @@ public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
|
|||
return new InMemoryRelyingPartyRegistrationRepository(registration);
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Value("\${metadata.location}")
|
||||
var assertingPartyMetadataLocation: String? = null
|
||||
|
||||
@Bean
|
||||
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
|
||||
val registration = RelyingPartyRegistrations
|
||||
.fromMetadataLocation(assertingPartyMetadataLocation)
|
||||
.registrationId("example")
|
||||
.build()
|
||||
return InMemoryRelyingPartyRegistrationRepository(registration)
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Or you can provide each detail manually, as you can see below:
|
||||
|
||||
.Relying Party Registration Repository Manual Configuration
|
||||
====
|
||||
[source,java]
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Value("${verification.key}")
|
||||
File verificationKey;
|
||||
|
@ -368,6 +430,34 @@ public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exc
|
|||
return new InMemoryRelyingPartyRegistrationRepository(registration);
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Value("\${verification.key}")
|
||||
var verificationKey: File? = null
|
||||
|
||||
@Bean
|
||||
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository {
|
||||
val certificate: X509Certificate? = X509Support.decodeCertificate(verificationKey!!)
|
||||
val credential: Saml2X509Credential = Saml2X509Credential.verification(certificate)
|
||||
val registration = RelyingPartyRegistration
|
||||
.withRegistrationId("example")
|
||||
.assertingPartyDetails { party: AssertingPartyDetails.Builder ->
|
||||
party
|
||||
.entityId("https://idp.example.com/issuer")
|
||||
.singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
|
||||
.wantAuthnRequestsSigned(false)
|
||||
.verificationX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
|
||||
c.add(
|
||||
credential
|
||||
)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
return InMemoryRelyingPartyRegistrationRepository(registration)
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
|
@ -431,7 +521,9 @@ Also, you can provide asserting party metadata like its `Issuer` value, where it
|
|||
|
||||
The following `RelyingPartyRegistration` is the minimum required for most setups:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
|
||||
.fromMetadataLocation("https://ap.example.org/metadata")
|
||||
|
@ -439,9 +531,21 @@ RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
|
|||
.build();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val relyingPartyRegistration = RelyingPartyRegistrations
|
||||
.fromMetadataLocation("https://ap.example.org/metadata")
|
||||
.registrationId("my-id")
|
||||
.build()
|
||||
----
|
||||
====
|
||||
|
||||
Though a more sophisticated setup is also possible, like so:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("my-id")
|
||||
.entityId("{baseUrl}/{registrationId}")
|
||||
|
@ -455,6 +559,25 @@ RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.wit
|
|||
.build();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val relyingPartyRegistration =
|
||||
RelyingPartyRegistration.withRegistrationId("my-id")
|
||||
.entityId("{baseUrl}/{registrationId}")
|
||||
.decryptionX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
|
||||
c.add(relyingPartyDecryptingCredential())
|
||||
}
|
||||
.assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
|
||||
.assertingPartyDetails { party -> party
|
||||
.entityId("https://ap.example.org")
|
||||
.verificationX509Credentials { c -> c.add(assertingPartyVerifyingCredential()) }
|
||||
.singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
|
||||
}
|
||||
.build()
|
||||
----
|
||||
====
|
||||
|
||||
[TIP]
|
||||
The top-level metadata methods are details about the relying party.
|
||||
The methods inside `assertingPartyDetails` are details about the asserting party.
|
||||
|
@ -512,7 +635,9 @@ At a minimum, it's necessary to have a certificate from the asserting party so t
|
|||
To construct a `Saml2X509Credential` that you'll use to verify assertions from the asserting party, you can load the file and use
|
||||
the `CertificateFactory` like so:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Resource resource = new ClassPathResource("ap.crt");
|
||||
try (InputStream is = resource.getInputStream()) {
|
||||
|
@ -522,13 +647,27 @@ try (InputStream is = resource.getInputStream()) {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val resource = ClassPathResource("ap.crt")
|
||||
resource.inputStream.use {
|
||||
return Saml2X509Credential.verification(
|
||||
CertificateFactory.getInstance("X.509").generateCertificate(it) as X509Certificate?
|
||||
)
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Let's say that the asserting party is going to also encrypt the assertion.
|
||||
In that case, the relying party will need a private key to be able to decrypt the encrypted value.
|
||||
|
||||
In that case, you'll need an `RSAPrivateKey` as well as its corresponding `X509Certificate`.
|
||||
You can load the first using Spring Security's `RsaKeyConverters` utility class and the second as you did before:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
X509Certificate certificate = relyingPartyDecryptionCertificate();
|
||||
Resource resource = new ClassPathResource("rp.crt");
|
||||
|
@ -538,6 +677,18 @@ try (InputStream is = resource.getInputStream()) {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val certificate: X509Certificate = relyingPartyDecryptionCertificate()
|
||||
val resource = ClassPathResource("rp.crt")
|
||||
resource.inputStream.use {
|
||||
val rsa: RSAPrivateKey = RsaKeyConverters.pkcs8().convert(it)
|
||||
return Saml2X509Credential.decryption(rsa, certificate)
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[TIP]
|
||||
When you specify the locations of these files as the appropriate Spring Boot properties, then Spring Boot will perform these conversions for you.
|
||||
|
||||
|
@ -556,7 +707,9 @@ The default looks up the registration id from the URI's last path element and lo
|
|||
|
||||
You can provide a simpler resolver that, for example, always returns the same relying party:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class SingleRelyingPartyRegistrationResolver
|
||||
implements Converter<HttpServletRequest, RelyingPartyRegistration> {
|
||||
|
@ -568,6 +721,17 @@ public class SingleRelyingPartyRegistrationResolver
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class SingleRelyingPartyRegistrationResolver : Converter<HttpServletRequest?, RelyingPartyRegistration?> {
|
||||
override fun convert(request: HttpServletRequest?): RelyingPartyRegistration? {
|
||||
return this.relyingParty
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Then, you can provide this resolver to the appropriate filters that <<servlet-saml2login-sp-initiated-factory, produce `<saml2:AuthnRequest>` s>>, <<servlet-saml2login-authenticate-responses, authenticate `<saml2:Response>` s>>, and <<servlet-saml2login-metadata, produce `<saml2:SPSSODescriptor>` metadata>>.
|
||||
|
||||
[NOTE]
|
||||
|
@ -610,7 +774,9 @@ Second, in a database, it's not necessary to replicate `RelyingPartyRegistration
|
|||
|
||||
Third, in Java, you can create a custom configuration method, like so:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
private RelyingPartyRegistration.Builder
|
||||
addRelyingPartyDetails(RelyingPartyRegistration.Builder builder) {
|
||||
|
@ -636,6 +802,36 @@ public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
private fun addRelyingPartyDetails(builder: RelyingPartyRegistration.Builder): RelyingPartyRegistration.Builder {
|
||||
val signingCredential: Saml2X509Credential = ...
|
||||
builder.signingX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
|
||||
c.add(
|
||||
signingCredential
|
||||
)
|
||||
}
|
||||
// ... other relying party configurations
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
|
||||
val okta = addRelyingPartyDetails(
|
||||
RelyingPartyRegistrations
|
||||
.fromMetadataLocation(oktaMetadataUrl)
|
||||
.registrationId("okta")
|
||||
).build()
|
||||
val azure = addRelyingPartyDetails(
|
||||
RelyingPartyRegistrations
|
||||
.fromMetadataLocation(oktaMetadataUrl)
|
||||
.registrationId("azure")
|
||||
).build()
|
||||
return InMemoryRelyingPartyRegistrationRepository(okta, azure)
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[[servlet-saml2login-sp-initiated-factory]]
|
||||
=== Producing `<saml2:AuthnRequest>` s
|
||||
|
||||
|
@ -682,7 +878,21 @@ RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.wit
|
|||
.assertingPartyDetails(party -> party
|
||||
// ...
|
||||
.wantAuthnRequestsSigned(false)
|
||||
);
|
||||
)
|
||||
.build();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,java,role="secondary"]
|
||||
----
|
||||
var relyingPartyRegistration: RelyingPartyRegistration =
|
||||
RelyingPartyRegistration.withRegistrationId("okta")
|
||||
// ...
|
||||
.assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
|
||||
// ...
|
||||
.wantAuthnRequestsSigned(false)
|
||||
}
|
||||
.build();
|
||||
----
|
||||
====
|
||||
|
||||
|
@ -695,7 +905,9 @@ You can configure the algorithm based on the asserting party's <<servlet-saml2lo
|
|||
|
||||
Or, you can provide it manually:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
String metadataLocation = "classpath:asserting-party-metadata.xml";
|
||||
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
|
||||
|
@ -703,9 +915,29 @@ RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations.fr
|
|||
.assertingPartyDetails((party) -> party
|
||||
// ...
|
||||
.signingAlgorithms((sign) -> sign.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512))
|
||||
);
|
||||
)
|
||||
.build();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
var metadataLocation = "classpath:asserting-party-metadata.xml"
|
||||
var relyingPartyRegistration: RelyingPartyRegistration =
|
||||
RelyingPartyRegistrations.fromMetadataLocation(metadataLocation)
|
||||
// ...
|
||||
.assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
|
||||
// ...
|
||||
.signingAlgorithms { sign: MutableList<String?> ->
|
||||
sign.add(
|
||||
SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA512
|
||||
)
|
||||
}
|
||||
}
|
||||
.build();
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: The snippet above uses the OpenSAML `SignatureConstants` class to supply the algorithm name.
|
||||
But, that's just for convenience.
|
||||
Since the datatype is `String`, you can supply the name of the algorithm directly.
|
||||
|
@ -714,16 +946,32 @@ Since the datatype is `String`, you can supply the name of the algorithm directl
|
|||
Some asserting parties require that the `<saml2:AuthnRequest>` be POSTed.
|
||||
This can be configured automatically via `RelyingPartyRegistrations`, or you can supply it manually, like so:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("okta")
|
||||
// ...
|
||||
.assertingPartyDetails(party -> party
|
||||
// ...
|
||||
.singleSignOnServiceBinding(Saml2MessageType.POST)
|
||||
);
|
||||
.singleSignOnServiceBinding(Saml2MessageBinding.POST)
|
||||
)
|
||||
.build();
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
var relyingPartyRegistration: RelyingPartyRegistration? =
|
||||
RelyingPartyRegistration.withRegistrationId("okta")
|
||||
// ...
|
||||
.assertingPartyDetails { party: AssertingPartyDetails.Builder -> party
|
||||
// ...
|
||||
.singleSignOnServiceBinding(Saml2MessageBinding.POST)
|
||||
}
|
||||
.build()
|
||||
----
|
||||
====
|
||||
|
||||
[[servlet-saml2login-sp-initiated-factory-custom-authnrequest]]
|
||||
==== Customizing OpenSAML's `AuthnRequest` Instance
|
||||
|
@ -736,7 +984,9 @@ This will give you access to post-process the `AuthnRequest` instance before it'
|
|||
|
||||
But, if you do need something from the request, then you can use create a custom `Saml2AuthenticationRequestContext` implementation and then a `Converter<Saml2AuthenticationRequestContext, AuthnRequest>` to build an `AuthnRequest` yourself, like so:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Component
|
||||
public class AuthnRequestConverter implements
|
||||
|
@ -765,9 +1015,37 @@ public class AuthnRequestConverter implements
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Component
|
||||
class AuthnRequestConverter : Converter<MySaml2AuthenticationRequestContext, AuthnRequest> {
|
||||
private val authnRequestBuilder: AuthnRequestBuilder? = null
|
||||
private val issuerBuilder: IssuerBuilder? = null
|
||||
|
||||
// ... constructor
|
||||
override fun convert(context: MySaml2AuthenticationRequestContext): AuthnRequest {
|
||||
val myContext: MySaml2AuthenticationRequestContext = context
|
||||
val issuer: Issuer = issuerBuilder.buildObject()
|
||||
issuer.value = myContext.getIssuer()
|
||||
val authnRequest: AuthnRequest = authnRequestBuilder.buildObject()
|
||||
authnRequest.issuer = issuer
|
||||
authnRequest.destination = myContext.getDestination()
|
||||
authnRequest.assertionConsumerServiceURL = myContext.getAssertionConsumerServiceUrl()
|
||||
|
||||
// ... additional settings
|
||||
authRequest.setForceAuthn(myContext.getForceAuthn())
|
||||
return authnRequest
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Then, you can construct your own `Saml2AuthenticationRequestContextResolver` and `Saml2AuthenticationRequestFactory` and publish them as `@Bean` s:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Bean
|
||||
Saml2AuthenticationRequestContextResolver authenticationRequestContextResolver() {
|
||||
|
@ -790,6 +1068,32 @@ Saml2AuthenticationRequestFactory authenticationRequestFactory(
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Bean
|
||||
open fun authenticationRequestContextResolver(): Saml2AuthenticationRequestContextResolver {
|
||||
val resolver: Saml2AuthenticationRequestContextResolver = DefaultSaml2AuthenticationRequestContextResolver()
|
||||
return Saml2AuthenticationRequestContextResolver { request: HttpServletRequest ->
|
||||
val context = resolver.resolve(request)
|
||||
MySaml2AuthenticationRequestContext(
|
||||
context,
|
||||
request.getParameter("force") != null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun authenticationRequestFactory(
|
||||
authnRequestConverter: AuthnRequestConverter?
|
||||
): Saml2AuthenticationRequestFactory? {
|
||||
val authenticationRequestFactory = OpenSamlAuthenticationRequestFactory()
|
||||
authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter)
|
||||
return authenticationRequestFactory
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[[servlet-saml2login-authenticate-responses]]
|
||||
=== Authenticating `<saml2:Response>` s
|
||||
|
||||
|
@ -810,7 +1114,9 @@ To configure these, you'll use the `saml2Login#authenticationManager` method in
|
|||
It's not uncommon for the asserting and relying parties to have system clocks that aren't perfectly synchronized.
|
||||
For that reason, you can configure `OpenSamlAuthenticationProvider` 's default assertion validator with some tolerance:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
@ -838,13 +1144,44 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
open class SecurityConfig : WebSecurityConfigurerAdapter() {
|
||||
override fun configure(http: HttpSecurity) {
|
||||
val authenticationProvider = OpenSamlAuthenticationProvider()
|
||||
authenticationProvider.setAssertionValidator(
|
||||
OpenSamlAuthenticationProvider
|
||||
.createDefaultAssertionValidator(Converter<OpenSamlAuthenticationProvider.AssertionToken, ValidationContext> {
|
||||
val params: MutableMap<String, Any> = HashMap()
|
||||
params[CLOCK_SKEW] =
|
||||
Duration.ofMinutes(10).toMillis()
|
||||
ValidationContext(params)
|
||||
})
|
||||
)
|
||||
http {
|
||||
authorizeRequests {
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
saml2Login {
|
||||
authenticationManager = ProviderManager(authenticationProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[[servlet-saml2login-opensamlauthenticationprovider-userdetailsservice]]
|
||||
==== Coordinating with a `UserDetailsService`
|
||||
|
||||
Or, perhaps you would like to include user details from a legacy `UserDetailsService`.
|
||||
In that case, the response authentication converter can come in handy, as can be seen below:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
@ -874,6 +1211,38 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||
}
|
||||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
open class SecurityConfig : WebSecurityConfigurerAdapter() {
|
||||
@Autowired
|
||||
var userDetailsService: UserDetailsService? = null
|
||||
|
||||
override fun configure(http: HttpSecurity) {
|
||||
val authenticationProvider = OpenSamlAuthenticationProvider()
|
||||
authenticationProvider.setResponseAuthenticationConverter { responseToken: OpenSamlAuthenticationProvider.ResponseToken ->
|
||||
val authentication = OpenSamlAuthenticationProvider
|
||||
.createDefaultResponseAuthenticationConverter() <1>
|
||||
.convert(responseToken)
|
||||
val assertion: Assertion = responseToken.response.assertions[0]
|
||||
val username: String = assertion.subject.nameID.value
|
||||
val userDetails = userDetailsService!!.loadUserByUsername(username) <2>
|
||||
MySaml2Authentication(userDetails, authentication) <3>
|
||||
}
|
||||
http {
|
||||
authorizeRequests {
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
saml2Login {
|
||||
authenticationManager = ProviderManager(authenticationProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
<1> First, call the default converter, which extracts attributes and authorities from the response
|
||||
<2> Second, call the <<servlet-authentication-userdetailsservice, `UserDetailsService`>> using the relevant information
|
||||
<3> Third, return a custom authentication that includes the user details
|
||||
|
@ -896,7 +1265,9 @@ To perform additional validation, you can configure your own assertion validator
|
|||
[[servlet-saml2login-opensamlauthenticationprovider-onetimeuse]]
|
||||
For example, you can use OpenSAML's `OneTimeUseConditionValidator` to also validate a `<OneTimeUse>` condition, like so:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||
OneTimeUseConditionValidator validator = ...;
|
||||
|
@ -918,6 +1289,30 @@ provider.setAssertionValidator(assertionToken -> {
|
|||
});
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
var provider = OpenSamlAuthenticationProvider()
|
||||
var validator: OneTimeUseConditionValidator = ...
|
||||
provider.setAssertionValidator { assertionToken ->
|
||||
val result = OpenSamlAuthenticationProvider
|
||||
.createDefaultAssertionValidator()
|
||||
.convert(assertionToken)
|
||||
val assertion: Assertion = assertionToken.assertion
|
||||
val oneTimeUse: OneTimeUse = assertion.conditions.oneTimeUse
|
||||
val context = ValidationContext()
|
||||
try {
|
||||
if (validator.validate(oneTimeUse, assertion, context) == ValidationResult.VALID) {
|
||||
return@setAssertionValidator result
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return@setAssertionValidator result.concat(Saml2Error(INVALID_ASSERTION, e.message))
|
||||
}
|
||||
result.concat(Saml2Error(INVALID_ASSERTION, context.validationFailureMessage))
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
While recommended, it's not necessary to call `OpenSamlAuthenticationProvider` 's default assertion validator.
|
||||
A circumstance where you would skip it would be if you don't need it to check the `<AudienceRestriction>` or the `<SubjectConfirmation>` since you are doing those yourself.
|
||||
|
@ -934,20 +1329,40 @@ The assertion decrypter is for decrypting encrypted elements of the `<saml2:Asse
|
|||
You can replace `OpenSamlAuthenticationProvider`'s default decryption strategy with your own.
|
||||
For example, if you have a separate service that decrypts the assertions in a `<saml2:Response>`, you can use it instead like so:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
MyDecryptionService decryptionService = ...;
|
||||
OpenSamlAuthenticationProvider provider = new OpenSamlAuthenticationProvider();
|
||||
provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val decryptionService: MyDecryptionService = ...
|
||||
val provider = OpenSamlAuthenticationProvider()
|
||||
provider.setResponseElementsDecrypter { responseToken -> decryptionService.decrypt(responseToken.response) }
|
||||
----
|
||||
====
|
||||
|
||||
If you are also decrypting individual elements in a `<saml2:Assertion>`, you can customize the assertion decrypter, too:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
provider.setAssertionElementsDecrypter((assertionToken) -> decryptionService.decrypt(assertionToken.getAssertion()));
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
provider.setAssertionElementsDecrypter { assertionToken -> decryptionService.decrypt(assertionToken.assertion) }
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: There are two separate decrypters since assertions can be signed separately from responses.
|
||||
Trying to decrypt a signed assertion's elements before signature verification may invalidate the signature.
|
||||
If your asserting party signs the response only, then it's safe to decrypt all elements using only the response decrypter.
|
||||
|
@ -959,7 +1374,9 @@ If your asserting party signs the response only, then it's safe to decrypt all e
|
|||
Of course, the `authenticationManager` DSL method can be also used to perform a completely custom SAML 2.0 authentication.
|
||||
This authentication manager should expect a `Saml2AuthenticationToken` object containing the SAML 2.0 Response XML data.
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
@ -979,6 +1396,26 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
open class SecurityConfig : WebSecurityConfigurerAdapter() {
|
||||
override fun configure(http: HttpSecurity) {
|
||||
val customAuthenticationManager: AuthenticationManager = MySaml2AuthenticationManager(...)
|
||||
http {
|
||||
authorizeRequests {
|
||||
authorize(anyRequest, authenticated)
|
||||
}
|
||||
saml2Login {
|
||||
authenticationManager = customAuthenticationManager
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[[servlet-saml2login-authenticatedprincipal]]
|
||||
=== Using `Saml2AuthenticatedPrincipal`
|
||||
|
||||
|
@ -987,7 +1424,9 @@ Once the relying party validates an assertion, the result is a `Saml2Authenticat
|
|||
|
||||
This means that you can access the principal in your controller like so:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Controller
|
||||
public class MainController {
|
||||
|
@ -1000,6 +1439,21 @@ public class MainController {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Controller
|
||||
class MainController {
|
||||
@GetMapping("/")
|
||||
fun index(@AuthenticationPrincipal principal: Saml2AuthenticatedPrincipal, model: Model): String {
|
||||
val email = principal.getFirstAttribute<String>("email")
|
||||
model.setAttribute("email", email)
|
||||
return "index"
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[TIP]
|
||||
Because the SAML 2.0 specification allows for each attribute to have multiple values, you can either call `getAttribute` to get the list of attributes or `getFirstAttribute` to get the first in the list.
|
||||
`getFirstAttribute` is quite handy when you know that there is only one value.
|
||||
|
@ -1009,7 +1463,9 @@ Because the SAML 2.0 specification allows for each attribute to have multiple va
|
|||
|
||||
You can publish a metadata endpoint by adding the `Saml2MetadataFilter` to the filter chain, as you'll see below:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
Converter<HttpServletRequest, RelyingPartyRegistration> relyingPartyRegistrationResolver =
|
||||
new DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository);
|
||||
|
@ -1023,26 +1479,62 @@ http
|
|||
.addFilterBefore(filter, Saml2WebSsoAuthenticationFilter.class);
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
val relyingPartyRegistrationResolver: Converter<HttpServletRequest, RelyingPartyRegistration> =
|
||||
DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrationRepository)
|
||||
val filter = Saml2MetadataFilter(
|
||||
relyingPartyRegistrationResolver,
|
||||
OpenSamlMetadataResolver()
|
||||
)
|
||||
|
||||
http {
|
||||
//...
|
||||
saml2Login { }
|
||||
addFilterBefore<Saml2WebSsoAuthenticationFilter>(filter)
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
You can use this metadata endpoint to register your relying party with your asserting party.
|
||||
This is often as simple as finding the correct form field to supply the metadata endpoint.
|
||||
|
||||
By default, the metadata endpoint is `+/saml2/service-provider-metadata/{registrationId}+`.
|
||||
You can change this by calling the `setRequestMatcher` method on the filter:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET"));
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata/{registrationId}", "GET"))
|
||||
----
|
||||
====
|
||||
|
||||
ensuring that the `registrationId` hint is at the end of the path.
|
||||
|
||||
Or, if you have registered a custom relying party registration resolver in the constructor, then you can specify a path without a `registrationId` hint, like so:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
filter.setRequestMatcher(new AntPathRequestMatcher("/saml2/metadata", "GET"));
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
filter.setRequestMatcher(AntPathRequestMatcher("/saml2/metadata", "GET"))
|
||||
----
|
||||
====
|
||||
|
||||
[[servlet-saml2login-logout]]
|
||||
=== Performing Single Logout
|
||||
|
||||
|
@ -1050,7 +1542,9 @@ Spring Security does not yet support single logout.
|
|||
|
||||
Generally speaking, though, you can achieve this by creating and registering a custom `LogoutSuccessHandler` and `RequestMatcher`:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
http
|
||||
// ...
|
||||
|
@ -1060,6 +1554,19 @@ http
|
|||
)
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
http {
|
||||
logout {
|
||||
// ...
|
||||
logoutSuccessHandler = myCustomSuccessHandler()
|
||||
logoutRequestMatcher = myRequestMatcher()
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
The success handler will send logout requests to the asserting party.
|
||||
|
||||
The request matcher will detect logout requests from the asserting party.
|
||||
|
|
|
@ -30,13 +30,24 @@ Hello org.springframework.security.authentication.UsernamePasswordAuthentication
|
|||
|
||||
Before we can use Spring Security Test support, we must perform some setup. An example can be seen below:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@RunWith(SpringJUnit4ClassRunner.class) // <1>
|
||||
@ContextConfiguration // <2>
|
||||
public class WithMockUserTests {
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@RunWith(SpringJUnit4ClassRunner::class)
|
||||
@ContextConfiguration
|
||||
class WithMockUserTests {
|
||||
----
|
||||
====
|
||||
|
||||
This is a basic example of how to setup Spring Security Test. The highlights are:
|
||||
|
||||
<1> `@RunWith` instructs the spring-test module that it should create an `ApplicationContext`. This is no different than using the existing Spring Test support. For additional information, refer to the https://docs.spring.io/spring-framework/docs/4.0.x/spring-framework-reference/htmlsingle/#integration-testing-annotations-standard[Spring Reference]
|
||||
|
@ -51,7 +62,9 @@ If you only need Spring Security related support, you can replace `@ContextConfi
|
|||
Remember we added the `@PreAuthorize` annotation to our `HelloMessageService` and so it requires an authenticated user to invoke it.
|
||||
If we ran the following test, we would expect the following test will pass:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Test(expected = AuthenticationCredentialsNotFoundException.class)
|
||||
public void getMessageUnauthenticated() {
|
||||
|
@ -59,6 +72,16 @@ public void getMessageUnauthenticated() {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Test(expected = AuthenticationCredentialsNotFoundException::class)
|
||||
fun getMessageUnauthenticated() {
|
||||
messageService.getMessage()
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
[[test-method-withmockuser]]
|
||||
=== @WithMockUser
|
||||
|
||||
|
@ -66,7 +89,9 @@ The question is "How could we most easily run the test as a specific user?"
|
|||
The answer is to use `@WithMockUser`.
|
||||
The following test will be run as a user with the username "user", the password "password", and the roles "ROLE_USER".
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Test
|
||||
@WithMockUser
|
||||
|
@ -76,6 +101,18 @@ String message = messageService.getMessage();
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Test
|
||||
@WithMockUser
|
||||
fun getMessageWithMockUser() {
|
||||
val message: String = messageService.getMessage()
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Specifically the following is true:
|
||||
|
||||
* The user with the username "user" does not have to exist since we are mocking the user
|
||||
|
@ -87,7 +124,9 @@ Our example is nice because we are able to leverage a lot of defaults.
|
|||
What if we wanted to run the test with a different username?
|
||||
The following test would run with the username "customUser". Again, the user does not need to actually exist.
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Test
|
||||
@WithMockUser("customUsername")
|
||||
|
@ -97,10 +136,24 @@ public void getMessageWithMockUserCustomUsername() {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Test
|
||||
@WithMockUser("customUsername")
|
||||
fun getMessageWithMockUserCustomUsername() {
|
||||
val message: String = messageService.getMessage()
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
We can also easily customize the roles.
|
||||
For example, this test will be invoked with the username "admin" and the roles "ROLE_USER" and "ROLE_ADMIN".
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Test
|
||||
@WithMockUser(username="admin",roles={"USER","ADMIN"})
|
||||
|
@ -110,10 +163,24 @@ public void getMessageWithMockUserCustomUser() {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Test
|
||||
@WithMockUser(username="admin",roles=["USER","ADMIN"])
|
||||
fun getMessageWithMockUserCustomUser() {
|
||||
val message: String = messageService.getMessage()
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
If we do not want the value to automatically be prefixed with ROLE_ we can leverage the authorities attribute.
|
||||
For example, this test will be invoked with the username "admin" and the authorities "USER" and "ADMIN".
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Test
|
||||
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
|
||||
|
@ -123,11 +190,25 @@ public void getMessageWithMockUserCustomAuthorities() {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Test
|
||||
@WithMockUser(username = "admin", authorities = ["ADMIN", "USER"])
|
||||
fun getMessageWithMockUserCustomUsername() {
|
||||
val message: String = messageService.getMessage()
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Of course it can be a bit tedious placing the annotation on every test method.
|
||||
Instead, we can place the annotation at the class level and every test will use the specified user.
|
||||
For example, the following would run every test with a user with the username "admin", the password "password", and the roles "ROLE_USER" and "ROLE_ADMIN".
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration
|
||||
|
@ -135,10 +216,22 @@ For example, the following would run every test with a user with the username "a
|
|||
public class WithMockUserTests {
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@RunWith(SpringJUnit4ClassRunner::class)
|
||||
@ContextConfiguration
|
||||
@WithMockUser(username="admin",roles=["USER","ADMIN"])
|
||||
class WithMockUserTests {
|
||||
----
|
||||
====
|
||||
|
||||
If you are using JUnit 5's `@Nested` test support, you can also place the annotation on the enclosing class to apply to all nested classes.
|
||||
For example, the following would run every test with a user with the username "admin", the password "password", and the roles "ROLE_USER" and "ROLE_ADMIN" for both test methods.
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration
|
||||
|
@ -157,6 +250,24 @@ public class WithMockUserTests {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@ContextConfiguration
|
||||
@WithMockUser(username = "admin", roles = ["USER", "ADMIN"])
|
||||
class WithMockUserTests {
|
||||
@Nested
|
||||
inner class TestSuite1 { // ... all test methods use admin user
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class TestSuite2 { // ... all test methods use admin user
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
By default the `SecurityContext` is set during the `TestExecutionListener.beforeTestMethod` event.
|
||||
This is the equivalent of happening before JUnit's `@Before`.
|
||||
You can change this to happen during the `TestExecutionListener.beforeTestExecution` event which is after JUnit's `@Before` but before the test method is invoked.
|
||||
|
@ -174,7 +285,9 @@ Using `@WithAnonymousUser` allows running as an anonymous user.
|
|||
This is especially convenient when you wish to run most of your tests with a specific user, but want to run a few tests as an anonymous user.
|
||||
For example, the following will run withMockUser1 and withMockUser2 using <<test-method-withmockuser,@WithMockUser>> and anonymous as an anonymous user.
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@WithMockUser
|
||||
|
@ -196,6 +309,29 @@ public class WithUserClassLevelAuthenticationTests {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@RunWith(SpringJUnit4ClassRunner::class)
|
||||
@WithMockUser
|
||||
class WithUserClassLevelAuthenticationTests {
|
||||
@Test
|
||||
fun withMockUser1() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun withMockUser2() {
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithAnonymousUser
|
||||
fun anonymous() {
|
||||
// override default to run as anonymous user
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
By default the `SecurityContext` is set during the `TestExecutionListener.beforeTestMethod` event.
|
||||
This is the equivalent of happening before JUnit's `@Before`.
|
||||
You can change this to happen during the `TestExecutionListener.beforeTestExecution` event which is after JUnit's `@Before` but before the test method is invoked.
|
||||
|
@ -219,7 +355,9 @@ That is exactly what `@WithUserDetails` does.
|
|||
|
||||
Assuming we have a `UserDetailsService` exposed as a bean, the following test will be invoked with an `Authentication` of type `UsernamePasswordAuthenticationToken` and a principal that is returned from the `UserDetailsService` with the username of "user".
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Test
|
||||
@WithUserDetails
|
||||
|
@ -229,10 +367,24 @@ public void getMessageWithUserDetails() {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Test
|
||||
@WithUserDetails
|
||||
fun getMessageWithUserDetails() {
|
||||
val message: String = messageService.getMessage()
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
We can also customize the username used to lookup the user from our `UserDetailsService`.
|
||||
For example, this test would be run with a principal that is returned from the `UserDetailsService` with the username of "customUsername".
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Test
|
||||
@WithUserDetails("customUsername")
|
||||
|
@ -242,10 +394,24 @@ public void getMessageWithUserDetailsCustomUsername() {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Test
|
||||
@WithUserDetails("customUsername")
|
||||
fun getMessageWithUserDetailsCustomUsername() {
|
||||
val message: String = messageService.getMessage()
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
We can also provide an explicit bean name to look up the `UserDetailsService`.
|
||||
For example, this test would look up the username of "customUsername" using the `UserDetailsService` with the bean name "myUserDetailsService".
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Test
|
||||
@WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
|
||||
|
@ -255,6 +421,18 @@ public void getMessageWithUserDetailsServiceBeanName() {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Test
|
||||
@WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
|
||||
fun getMessageWithUserDetailsServiceBeanName() {
|
||||
val message: String = messageService.getMessage()
|
||||
// ...
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Like `@WithMockUser` we can also place our annotation at the class level so that every test uses the same user.
|
||||
However unlike `@WithMockUser`, `@WithUserDetails` requires the user to exist.
|
||||
|
||||
|
@ -278,7 +456,9 @@ We will now see an option that allows the most flexibility.
|
|||
We can create our own annotation that uses the `@WithSecurityContext` to create any `SecurityContext` we want.
|
||||
For example, we might create an annotation named `@WithMockCustomUser` as shown below:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
|
||||
|
@ -290,12 +470,23 @@ public @interface WithMockCustomUser {
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory::class)
|
||||
annotation class WithMockCustomUser(val username: String = "rob", val name: String = "Rob Winch")
|
||||
----
|
||||
====
|
||||
|
||||
You can see that `@WithMockCustomUser` is annotated with the `@WithSecurityContext` annotation.
|
||||
This is what signals to Spring Security Test support that we intend to create a `SecurityContext` for the test.
|
||||
The `@WithSecurityContext` annotation requires we specify a `SecurityContextFactory` that will create a new `SecurityContext` given our `@WithMockCustomUser` annotation.
|
||||
You can find our `WithMockCustomUserSecurityContextFactory` implementation below:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
public class WithMockCustomUserSecurityContextFactory
|
||||
implements WithSecurityContextFactory<WithMockCustomUser> {
|
||||
|
@ -313,12 +504,30 @@ public class WithMockCustomUserSecurityContextFactory
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class WithMockCustomUserSecurityContextFactory : WithSecurityContextFactory<WithMockCustomUser> {
|
||||
override fun createSecurityContext(customUser: WithMockCustomUser): SecurityContext {
|
||||
val context = SecurityContextHolder.createEmptyContext()
|
||||
val principal = CustomUserDetails(customUser.name, customUser.username)
|
||||
val auth: Authentication =
|
||||
UsernamePasswordAuthenticationToken(principal, "password", principal.authorities)
|
||||
context.authentication = auth
|
||||
return context
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
We can now annotate a test class or a test method with our new annotation and Spring Security's `WithSecurityContextTestExecutionListener` will ensure that our `SecurityContext` is populated appropriately.
|
||||
|
||||
When creating your own `WithSecurityContextFactory` implementations, it is nice to know that they can be annotated with standard Spring annotations.
|
||||
For example, the `WithUserDetailsSecurityContextFactory` uses the `@Autowired` annotation to acquire the `UserDetailsService`:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
final class WithUserDetailsSecurityContextFactory
|
||||
implements WithSecurityContextFactory<WithUserDetails> {
|
||||
|
@ -342,6 +551,25 @@ final class WithUserDetailsSecurityContextFactory
|
|||
}
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
class WithUserDetailsSecurityContextFactory @Autowired constructor(private val userDetailsService: UserDetailsService) :
|
||||
WithSecurityContextFactory<WithUserDetails> {
|
||||
override fun createSecurityContext(withUser: WithUserDetails): SecurityContext {
|
||||
val username: String = withUser.value
|
||||
Assert.hasLength(username, "value() must be non-empty String")
|
||||
val principal = userDetailsService.loadUserByUsername(username)
|
||||
val authentication: Authentication =
|
||||
UsernamePasswordAuthenticationToken(principal, principal.password, principal.authorities)
|
||||
val context = SecurityContextHolder.createEmptyContext()
|
||||
context.authentication = authentication
|
||||
return context
|
||||
}
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
By default the `SecurityContext` is set during the `TestExecutionListener.beforeTestMethod` event.
|
||||
This is the equivalent of happening before JUnit's `@Before`.
|
||||
You can change this to happen during the `TestExecutionListener.beforeTestExecution` event which is after JUnit's `@Before` but before the test method is invoked.
|
||||
|
@ -358,21 +586,41 @@ You can change this to happen during the `TestExecutionListener.beforeTestExecut
|
|||
If you reuse the same user within your tests often, it is not ideal to have to repeatedly specify the attributes.
|
||||
For example, if there are many tests related to an administrative user with the username "admin" and the roles `ROLE_USER` and `ROLE_ADMIN` you would have to write:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@WithMockUser(username="admin",roles={"USER","ADMIN"})
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@WithMockUser(username="admin",roles=["USER","ADMIN"])
|
||||
----
|
||||
====
|
||||
|
||||
Rather than repeating this everywhere, we can use a meta annotation.
|
||||
For example, we could create a meta annotation named `WithMockAdmin`:
|
||||
|
||||
[source,java]
|
||||
====
|
||||
.Java
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@WithMockUser(value="rob",roles="ADMIN")
|
||||
public @interface WithMockAdmin { }
|
||||
----
|
||||
|
||||
.Kotlin
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@WithMockUser(value = "rob", roles = ["ADMIN"])
|
||||
annotation class WithMockAdmin
|
||||
----
|
||||
====
|
||||
|
||||
Now we can use `@WithMockAdmin` in the same way as the more verbose `@WithMockUser`.
|
||||
|
||||
Meta annotations work with any of the testing annotations described above.
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue