使用 SpEL 与自定义投票器实现方法安全
使用 @PreAuthorize、SpEL 表达式和自定义授权逻辑实施细粒度访问控制。
使用 SpEL 与自定义投票器实现方法安全 是 CoddyKit 上的免费 Spring Boot 4 Complete Guide 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Spring Boot 4 Complete Guide 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Spring Boot 4 Complete Guide 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
Why Method Security?
URL-based security (HttpSecurity matchers) guards entry points, but it cannot see the arguments a method receives or the object it returns. Method security closes that gap by enforcing rules right at the service layer.
- Defense in depth — protection survives even if a controller forgets a check.
- Fine-grained — decide based on parameters, return values, and the authenticated principal.
- Reusable — the same secured service can be called from REST, GraphQL, or a message listener and stays protected.
In this lesson we enforce access with @PreAuthorize, SpEL expressions, and a custom authorization manager.
Enabling Method Security
In Spring Boot 4 / Spring Security 6, method security is opt-in. Add @EnableMethodSecurity to a configuration class. It activates the annotations through an AOP proxy.
prePostEnableddefaults to true —@PreAuthorizeand@PostAuthorizework out of the box.- Set
securedEnabled = truefor the legacy@Secured, orjsr250Enabled = truefor@RolesAllowed.
Note: the old @EnableGlobalMethodSecurity is removed — always use @EnableMethodSecurity.
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
// prePostEnabled = true by default
// @PreAuthorize / @PostAuthorize now active
}@PreAuthorize with Roles and Authorities
@PreAuthorize evaluates a SpEL expression before the method runs. If it returns false, Spring throws AccessDeniedException and the body never executes.
hasRole('ADMIN')— checks theROLE_ADMINauthority (the prefix is added for you).hasAuthority('SCOPE_orders:write')— exact authority match, no prefix added.hasAnyRole('ADMIN','MANAGER')and the boolean operatorsand/or/!.
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
public class AccountService {
@PreAuthorize("hasRole('ADMIN')")
public void closeAccount(Long accountId) {
// only ROLE_ADMIN reaches here
}
@PreAuthorize("hasAnyRole('ADMIN','SUPPORT') or hasAuthority('SCOPE_accounts:write')")
public void freezeAccount(Long accountId) {
// ...
}
}Referencing Method Arguments with #
The real power of SpEL is reading method arguments. Prefix a parameter name with # to use it inside the expression. This lets you compare the principal's identity to the data being acted on.
authentication— the currentAuthenticationobject.principal— the principal (often aUserDetailsor JWT).#username,#order.ownerId— method arguments and their properties.
Argument names require parameters compiled with -parameters (Spring Boot enables this by default).
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
public class ProfileService {
// A user may edit only their own profile, unless they are an admin
@PreAuthorize("#username == authentication.name or hasRole('ADMIN')")
public void updateProfile(String username, ProfileDto dto) {
// ...
}
}@PostAuthorize and returnObject
@PostAuthorize runs after the method returns and can inspect the result via returnObject. Use it when you must load the entity first to know who owns it.
- Good for “you can read this record only if it belongs to you.”
- The method body does execute, so avoid it for operations with side effects you must prevent.
- On denial, the return value is discarded and
AccessDeniedExceptionis thrown.
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.stereotype.Service;
@Service
public class DocumentService {
@PostAuthorize("returnObject.ownerUsername == authentication.name or hasRole('ADMIN')")
public Document findById(Long id) {
return repository.findById(id).orElseThrow();
}
}@PreFilter and @PostFilter on Collections
Filtering annotations prune collections element by element instead of throwing. They use a special variable filterObject bound to each element.
@PreFilter— strips disallowed elements from a collection argument before the method runs.@PostFilter— strips disallowed elements from the returned collection.- Use
filterTargetwhen a method has more than one collection parameter.
Caution: post-filtering large result sets in memory can be costly — prefer filtering in the query when possible.
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class OrderService {
// Caller sees only the orders they own (admins see all)
@PostFilter("filterObject.ownerUsername == authentication.name or hasRole('ADMIN')")
public List<Order> findRecentOrders() {
return repository.findRecent();
}
}Calling a Bean from SpEL with @
When logic gets complex, push it into a Spring bean and call it from the expression using @beanName.method(...). This keeps annotations readable and the rule unit-testable.
- The
@resolves a bean from the application context. - Pass
authenticationand method arguments straight into the bean method. - The method must return a
boolean.
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
@Component("projectAccess")
public class ProjectAccessEvaluator {
public boolean canEdit(Authentication auth, Long projectId) {
String user = auth.getName();
return membershipRepository.isEditor(user, projectId);
}
}
// Usage on a service method:
// @PreAuthorize("@projectAccess.canEdit(authentication, #projectId)")
// public void rename(Long projectId, String name) { ... }PermissionEvaluator and hasPermission
SpEL exposes hasPermission(target, permission), which delegates to a PermissionEvaluator bean. It is the canonical hook for domain-object (ACL-style) authorization without inlining logic in every annotation.
hasPermission(#doc, 'WRITE')— passes the object and a permission key.hasPermission(#id, 'com.app.Document', 'READ')— passes an id plus the type.- You register exactly one
PermissionEvaluatorvia aMethodSecurityExpressionHandler.
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import java.io.Serializable;
public class DocumentPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication auth, Object target, Object permission) {
if (target instanceof Document doc) {
return "WRITE".equals(permission)
? doc.getOwnerUsername().equals(auth.getName())
: true; // READ allowed for all in this example
}
return false;
}
@Override
public boolean hasPermission(Authentication auth, Serializable id,
String type, Object permission) {
return false; // resolve by id+type if needed
}
}Registering a Custom Expression Handler
To wire your PermissionEvaluator into SpEL, expose a DefaultMethodSecurityExpressionHandler bean and set the evaluator on it. Spring Security picks it up for all method annotations.
- The bean name is not important; the type is.
- You can also attach a custom
RoleHierarchyhere sohasRolerespects inheritance.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@Configuration
@EnableMethodSecurity
public class ExpressionHandlerConfig {
@Bean
static DefaultMethodSecurityExpressionHandler expressionHandler() {
var handler = new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(new DocumentPermissionEvaluator());
return handler;
}
}Custom AuthorizationManager (the New Voter)
Spring Security 6 replaced the legacy AccessDecisionVoter with the simpler AuthorizationManager<T>. For method security the type parameter is MethodInvocation. Implement check to return an AuthorizationDecision.
- Return
new AuthorizationDecision(true|false)— ornullto abstain and let other managers decide. - Register it with
@EnableMethodSecurity(prePostEnabled = false)plus an advisor, or combine managers withAuthorizationManagers.allOf(...).
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import java.util.function.Supplier;
public class BusinessHoursAuthorizationManager
implements AuthorizationManager<MethodInvocation> {
@Override
public AuthorizationDecision check(Supplier<Authentication> auth,
MethodInvocation invocation) {
int hour = java.time.LocalTime.now().getHour();
boolean withinHours = hour >= 9 && hour < 18;
return new AuthorizationDecision(withinHours);
}
}Pre vs Post: Choosing Correctly
Picking the wrong annotation either leaks data or blocks valid calls. A quick decision guide:
- Rule depends only on arguments + principal →
@PreAuthorize(fast, no side effects). - Rule depends on the loaded entity's ownership →
@PostAuthorize. - Trimming a collection per-element →
@PostFilter(or filter in the query). - Reusable domain-object rule →
hasPermission+PermissionEvaluator.
Remember: @PostAuthorize and @PostFilter run the method body, so never rely on them to stop a mutating operation.
Quick Check
You have a method Document findById(Long id) that loads a document, and access should be granted only if the returned document's ownerUsername equals the caller, or the caller is an admin. Which annotation expresses this correctly?
Recap
You now enforce fine-grained access at the method layer:
@EnableMethodSecurityturns on annotation-driven checks (no more@EnableGlobalMethodSecurity).@PreAuthorizeguards before execution using SpEL:hasRole,hasAuthority,#args, andauthentication.@PostAuthorizeinspectsreturnObject;@PreFilter/@PostFilterprune collections viafilterObject.- Push complex rules into a bean (
@beanName.method(...)) or aPermissionEvaluatorbehindhasPermission. - For cross-cutting policy, implement
AuthorizationManager<MethodInvocation>— the modern replacement for voters.
Rule of thumb: prefer pre-checks for speed and safety; reach for post-checks only when the decision needs the loaded data.
常见问题解答
「使用 SpEL 与自定义投票器实现方法安全」课时是免费的吗?
是的 — 「使用 SpEL 与自定义投票器实现方法安全」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Spring Boot 4 Complete Guide 课程的其余内容,请升级到 CoddyKit PRO。 Spring Boot 4 Complete Guide 课程共包含 4 节课。
「使用 SpEL 与自定义投票器实现方法安全」这节课中我会学到什么?
使用 @PreAuthorize、SpEL 表达式和自定义授权逻辑实施细粒度访问控制。 你通过在浏览器中直接运行的动手代码来练习 Spring Boot 4 Complete Guide,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Spring Boot 4 Complete Guide 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Spring Boot 4 Complete Guide 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「使用 SpEL 与自定义投票器实现方法安全」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Spring Boot 4 Complete Guide 课中编写并运行代码吗?
能。每节 Spring Boot 4 Complete Guide 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 资源服务器 JWT 验证与声明
- OAuth2 客户端与授权码流程
- 使用 SpEL 与自定义投票器实现方法安全
- 不透明令牌自省与令牌交换