How do you apply a SpEL access rule to an HTTP request in Spring Security 6, including IP-based restrictions with hasIpAddress?
answer
- SS6: access() takes AuthorizationManager, not String
- WebExpressionAuthorizationManager wraps SpEL
- hasIpAddress = web context only
- authentication / principal available in SpEL
- proxy breaks getRemoteAddr / hasIpAddress
basics
~10 sIn Spring Security 6 the String access("...") method was removed. Pass an AuthorizationManager to access(...). Wrap a SpEL string in WebExpressionAuthorizationManager, e.g. access(new WebExpressionAuthorizationManager("hasRole('ADMIN') and hasIpAddress('10.0.0.0/8')")).
solid answer
~40 sSpring Security 6 replaced the old string-based `.access("expr")` on `authorizeRequests` with an `AuthorizationManager`-based model on `authorizeHttpRequests`. To keep using SpEL you wrap the expression in a `WebExpressionAuthorizationManager` and pass it to `access(...)`. Its expression handler exposes the web root, so besides `hasRole`, `hasAuthority`, `authentication`, and `principal`, you also get web-only functions like `hasIpAddress('192.168.0.0/16')`. Example: `.requestMatchers("/admin/**").access(new WebExpressionAuthorizationManager("hasRole('ADMIN') and hasIpAddress('10.0.0.0/8')"))`. Use `access` only when built-in methods (hasRole, hasAuthority, authenticated) can't express the rule — combining conditions, IP checks, or referencing request attributes. For simple cases prefer the dedicated methods; for very complex logic prefer a custom `AuthorizationManager` over hard-to-test SpEL strings. Note hasIpAddress is only available in the web expression context, not method security.
code
java · 12 lines@Bean
SecurityFilterChain chain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
// SpEL via WebExpressionAuthorizationManager
.requestMatchers("/admin/**").access(new WebExpressionAuthorizationManager(
"hasRole('ADMIN') and hasIpAddress('10.0.0.0/8')"))
.requestMatchers("/internal/**").access(new WebExpressionAuthorizationManager(
"hasIpAddress('127.0.0.1/32') or authentication.name == 'svc'"))
.anyRequest().authenticated());
return http.build();
}go deeper
Aware that SpEL rules exist via access(); not expected to know the SS6 API change.
Knows access() now takes an AuthorizationManager and that WebExpressionAuthorizationManager wraps SpEL.
Explains the migration, available SpEL variables, hasIpAddress web-only scope, and proxy pitfalls.
Weighs SpEL vs typed custom AuthorizationManager for maintainability/testability and knows AuthorizationManagers combinators / IpAddressAuthorizationManager alternatives.
## The Spring Security 6 authorization model Spring Security 6 migrated request authorization from the legacy `authorizeRequests()` (backed by `SecurityExpressionHandler` voters) to `authorizeHttpRequests()`, which is backed by the `AuthorizationManager<RequestAuthorizationContext>` interface. Each matcher maps to an `AuthorizationManager` that returns an `AuthorizationDecision`. A major breaking change: the old **string** overload `.access("hasRole('ADMIN')")` was **removed**. `access(...)` now takes an `AuthorizationManager`, not a String. ## Keeping SpEL: WebExpressionAuthorizationManager To continue using SpEL expressions, wrap them: ```java .access(new WebExpressionAuthorizationManager("hasRole('ADMIN')")) ``` `WebExpressionAuthorizationManager` parses the SpEL once and evaluates it per request against a root object (`WebSecurityExpressionRoot`-style) that exposes: - `hasRole(...)`, `hasAnyRole(...)`, `hasAuthority(...)`, `hasAnyAuthority(...)` - `authenticated`, `fullyAuthenticated`, `anonymous`, `rememberMe`, `permitAll`, `denyAll` - `authentication` (the current `Authentication`) and `principal` - **web-only:** `hasIpAddress('CIDR')` and access to the `HttpServletRequest` ## hasIpAddress `hasIpAddress('192.168.1.0/24')` returns true when the request's remote address falls in the CIDR block. It exists **only** in the web expression context — it is not available in `@PreAuthorize` method security. Behind a proxy/load balancer, `getRemoteAddr()` returns the proxy IP unless you configure `ForwardedHeaderFilter` / trust the `X-Forwarded-For` chain, so IP rules can silently break in production. ## Composing conditions SpEL shines for combined logic: ```java .access(new WebExpressionAuthorizationManager( "hasRole('ADMIN') and hasIpAddress('10.0.0.0/8')")) ``` You can also reference `authentication`: ```java "hasRole('ADMIN') or authentication.name == 'root'" ``` ## When to use access() vs dedicated methods vs custom manager - **Prefer dedicated methods** (`hasRole`, `hasAuthority`, `authenticated`, `permitAll`) for simple rules — they're clearer and type-safe-ish. - **Use WebExpressionAuthorizationManager** when you need combined conditions, IP checks, or request/authentication references and want to stay declarative. - **Write a custom `AuthorizationManager`** when logic is complex, needs unit tests, or hits external state — SpEL strings are stringly-typed and only fail at runtime. ## Newer alternative Spring Security also ships `AuthorizationManagers` combinators (`allOf`, `anyOf`) and, in recent versions, `IpAddressAuthorizationManager.hasIpAddress(...)` as a typed alternative to the SpEL string. For pure IP rules that typed manager avoids SpEL entirely. ## Gotchas 1. Using the removed String `access("...")` overload → won't compile on SS6. 2. Expecting `hasIpAddress` in `@PreAuthorize` → it isn't there. 3. Proxy in front → `hasIpAddress` sees the proxy IP unless forwarded headers are handled. 4. SpEL typos only surface at request time, not startup (unless eagerly parsed).
- Why does hasIpAddress work in a SecurityFilterChain rule but not in @PreAuthorize?hasIpAddress is provided by the web expression root, which has access to the HttpServletRequest's remote address. Method security evaluates against a method-security expression root that has no request, so hasIpAddress isn't available there.
- Your hasIpAddress('10.0.0.0/8') rule fails in production behind a load balancer. Why?getRemoteAddr() returns the load balancer's IP, not the client's, so the CIDR check sees the wrong address. Fix by trusting the forwarded chain via ForwardedHeaderFilter (or the server's forward-headers-strategy) so the real client IP is used.
- When would you write a custom AuthorizationManager instead of a SpEL string?When logic is complex, needs unit testing, references injected services or external state, or must fail fast at construction — SpEL strings are only validated at evaluation time and are harder to test than a typed AuthorizationManager.