用户权限一般我们会保存在数据库,当用户信息UserDetails初始化的时候设置到GrantedAuthority中。这一步一般在我们自定义实现接口UserDetailsService中做。通过此操作,用户全部权限加载到了Authentication,并存到了全局上下文SecurityContextHolder中。后续请求可直接从SecurityContextHolder中获取用户权限信息。
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username){
//省略其他逻辑
...
//根据用户名从数据库加载用户权限
//设置到UserDetails
}
}
以上只是伪代码,具体逻辑要根据自己的项目编写代码。
所谓的资源权限就是所有我们的菜单,接口,按钮等。比如修改用户部门这个接口只能提供给管理员,客服只能拥有客服的权限,不能拥有修改其他用户的权限。每个资源都对应一个权限,下边我们介绍两个常用的权限设计。
一般情况下我们要把资源的权限配置到数据库中,一般会配置具体的页面权限,接口权限等。我们依托于数据库做资源权限管理时,是要实现接口FilterInvocationSecurityMetadataSource,去到数据库中查询资源所需的权限集合。
代码示例:
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@Service
public class CustomInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
//资源权限map集合
private Map<RequestMatcher, Collection<ConfigAttribute>> requestMap;
public CustomInvocationSecurityMetadataSource() {
requestMap = new HashMap<>();
// 从数据库加载URL与权限的映射关系
// 假设我们有一个名为"permissions"的表,包含"url"和"role"字段
List<PermissionEntity> permissionEntities = permissionRepository.findAll();
for (PermissionEntity permissionEntity : permissionEntities) {
RequestMatcher requestMatcher = new AntPathRequestMatcher(permissionEntity.getUrl());
Collection<ConfigAttribute> configAttributes = SecurityConfig.createList(permissionEntity.getRole());
requestMap.put(requestMatcher, configAttributes);
}
}
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
HttpServletRequest request = ((FilterInvocation) object).getRequest();
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) {
//从资源权限map集合匹配,有就返回
if (entry.getKey().matches(request)) {
return entry.getValue();
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
通过创建一个CustomInvocationSecurityMetadataSource类来实现InvocationSecurityMetadataSourceService接口,从数据库加载URL与权限的映射关系。
SpringSecurity不仅提供了自带的注解来设置接口权限,也可以自定义注解
在Spring Security中,有几个常用的自带注解用于定义资源权限,它们的含义如下:
以下是一个Java代码示例,演示了如何使用这些自带注解定义资源权限:
@RestController
public class MyController {
@GetMapping("/public")
public String publicResource() {
return "This is a public resource.";
}
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String adminResource() {
return "This is an admin resource.";
}
@GetMapping("/user")
@Secured("ROLE_USER")
public String userResource() {
return "This is a user resource.";
}
@GetMapping("/manager")
@RolesAllowed("ROLE_MANAGER")
public String managerResource() {
return "This is a manager resource.";
}
}
在上述示例中, /public 是一个公共资源,任何用户都可以访问。 /admin 是一个需要 ADMIN 角色的资源,只有具有 ADMIN 角色的用户才能访问。 /user 是一个需要 ROLE_USER 角色的资源,只有具有 ROLE_USER 角色的用户才能访问。 /manager 是一个需要 ROLE_MANAGER 角色的资源,只有具有 ROLE_MANAGER 角色的用户才能访问。
使用SpEL表达式,您可以编写更复杂的权限规则,例如根据用户的属性进行判断或进行更细粒度的权限控制。请确保在配置Spring Security时启用了SpEL表达式的支持。
@PreAuthorize("@el.check('admin','user:edit')")
public ResponseEntity<Object> update(@Validated @RequestBody User resources){
//业务逻辑
}
controller加上注解并配置SpEL表达式。
@Service(value = "el")
public class ElPermissionConfig {
public Boolean check(String ...permissions){
// 获取当前用户的所有权限
List<String> elPermissions = SecurityUtils.getUserDetails().getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
// 判断当前用户的所有权限是否包含接口上定义的权限
return elPermissions.contains("admin") || Arrays.stream(permissions).anyMatch(elPermissions::contains);
}
}
定义SPEL表达式校验逻辑。
1.创建自定义注解
/**
* 自定义注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermission {
}
2.controller使用
@Log("用户登录")
@@RequiresPermission("ROLE_ADMIN")
@PostMapping(value = "/edit")
public ResponseEntity<Object> edit(@Validated @RequestBody User user, HttpServletRequest request){
//业务逻辑
......
}
3.编写自定义逻辑
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//其他业务逻辑
......
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// 搜寻匿名标记 url: @RequiresPermission
Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = applicationContext.getBean(RequestMappingHandlerMapping.class).getHandlerMethods();
Set<String> anonymousUrls = new HashSet<>();
for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethodMap.entrySet()) {
HandlerMethod handlerMethod = infoEntry.getValue();
RequiresPermission requiresPermission = handlerMethod.getMethodAnnotation(RequiresPermission .class);
if (null != requiresPermission ) {
//获取所有有自定义注解的路径并放入集合中
anonymousUrls.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
}
}
httpSecurity
//省略其他配置
...
// 自定义匿名访问所有url放行 : 允许匿名和带权限以及登录用户访问
.antMatchers(anonymousUrls.toArray(new String[0])).permitAll()
....
}
}
上述是放到SecurityConfig配置类中,我们也可以切面实现自定义注解校验逻辑。
4.切面实现校验逻辑
创建一个自定义的权限校验切面,用于在方法执行前进行权限校验。
import org.aspectj.lang.annotation.*;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class PermissionValidationAspect {
@Before("@annotation(requiresPermission)")
public void validatePermission(RequiresPermission requiresPermission) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 根据authentication获取当前用户的权限信息
// 进行权限校验
if (!authentication.getAuthorities().contains(requiresPermission.value())) {
throw new AccessDeniedException("Access Denied");
}
}
}
Spring Security配置类中启用权限校验切面。
@Configuration
@EnableAspectJAutoProxy
public class SecurityConfig {
// 其他配置...
@Bean
public PermissionValidationAspect permissionValidationAspect() {
return new PermissionValidationAspect();
}
}
Spring Security中的资源权限注解在应用程序启动时被初始化,并且保存在内存中。这些注解的初始化是通过Spring Security的配置和自动装配机制完成的。
在Spring Security的源码中,资源权限注解的初始化主要是通过 @EnableGlobalMethodSecurity 注解和相应的配置类来完成。这个注解通常被应用在配置类上,用于启用方法级别的安全性控制。
以下是一个简单的代码示例,展示了如何使用 @EnableGlobalMethodSecurity 注解来启用资源权限注解的初始化:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// Security configuration
}
在上述示例中, @EnableGlobalMethodSecurity(prePostEnabled = true) 注解启用了资源权限注解的初始化,并设置了 prePostEnabled 为 true ,表示启用 @PreAuthorize 和 @PostAuthorize 注解。
通过这种方式,资源权限注解将在应用程序启动时进行初始化,并且保存在内存中,以便在方法执行时进行权限验证。
请注意,具体的资源权限注解的实现细节可以在Spring Security的源码中找到,包括注解的解析和验证过程。
以下是一个示例的Spring Boot项目中的Java代码,演示如何自定义用户权限存储到数据库并替换原有的FilterSecurityInterceptor过滤器,并分别实现AccessDecisionManager、InvocationSecurityMetadataSourceService和UserDetailsService接口。
此代码示例只是一个简单的流程展示,具体到项目中要根据实际业务做调整。
sql
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
password VARCHAR(100) NOT NULL,
role VARCHAR(20) NOT NULL
);
@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String username;
private String password;
private String role;
// getters and setters
}
@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
UserEntity findByUsername(String username);
}
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userEntity = userRepository.findByUsername(username);
if (userEntity == null) {
throw new UsernameNotFoundException("User not found");
}
return User.builder()
.username(userEntity.getUsername())
.password(userEntity.getPassword())
//此处可根据具体的权限表结构做处理
.roles(userEntity.getRole())
.build();
}
}
@Service
public class CustomInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private PermissionRepository permissionRepository;
private Map<RequestMatcher, Collection<ConfigAttribute>> requestMap;
public CustomInvocationSecurityMetadataSource() {
requestMap = new HashMap<>();
List<PermissionEntity> permissionEntities = permissionRepository.findAll();
for (PermissionEntity permissionEntity : permissionEntities) {
RequestMatcher requestMatcher = new AntPathRequestMatcher(permissionEntity.getUrl());
Collection<ConfigAttribute> configAttributes = SecurityConfig.createList(permissionEntity.getRole());
requestMap.put(requestMatcher, configAttributes);
}
}
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
HttpServletRequest request = ((FilterInvocation) object).getRequest();
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) {
if (entry.getKey().matches(request)) {
return entry.getValue();
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
@Service
public class CustomAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
if (configAttributes == null) {
return;
}
for (ConfigAttribute configAttribute : configAttributes) {
String requiredRole = configAttribute.getAttribute();
for (GrantedAuthority authority : authentication.getAuthorities()) {
if (requiredRole.equals(authority.getAuthority())) {
return;
}
}
}
throw new AccessDeniedException("Access Denied");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
@Component
public class CustomFilterSecurityInterceptor extends FilterSecurityInterceptor {
@Autowired
public CustomFilterSecurityInterceptor(FilterInvocationSecurityMetadataSource securityMetadataSource,
AccessDecisionManager accessDecisionManager) {
setSecurityMetadataSource(securityMetadataSource);
setAccessDecisionManager(accessDecisionManager);
}
@Override
protected FilterInvocationSecurityMetadataSource obtainSecurityMetadataSource() {
return super.obtainSecurityMetadataSource();
}
@Override
public AccessDecisionManager getAccessDecisionManager() {
return super.getAccessDecisionManager();
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Autowired
private CustomFilterSecurityInterceptor customFilterSecurityInterceptor;
@Autowired
private CustomAccessDecisionManager customAccessDecisionManager;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilterBefore(customFilterSecurityInterceptor, FilterSecurityInterceptor.class);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/public/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilterBefore(customFilterSecurityInterceptor, FilterSecurityInterceptor.class);
}
@Bean
public AccessDecisionManager accessDecisionManager() {
return customAccessDecisionManager;
}
}
这样,您就完成了在Spring Boot项目中自定义用户权限存储到数据库并替换原有的FilterSecurityInterceptor过滤器,并分别实现AccessDecisionManager、InvocationSecurityMetadataSourceService和UserDetailsService接口的操作。
请注意,这只是一个示例,具体的实现细节可能因您的应用程序架构和需求而有所不同。
上文中是具体在项目中我们需要配置和自定义的业务逻辑,下边我们看下在源码中它们的使用。
我们先从过滤器FilterSecurityInterceptor看
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
//调用invoke方法
invoke(new FilterInvocation(request, response, chain));
}
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
//校验逻辑
if (isApplied(filterInvocation) && this.observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
return;
}
// first time this request being called, so perform security checking
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
//调用父类的方法,进行鉴权
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
调用父类的方法beforeInvocation进行鉴权
protected org.springframework.security.access.intercept.InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException("Security invocation attempted for object " + object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}
//获取当前请求路径所需要的访问权限
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
if (CollectionUtils.isEmpty(attributes)) {
Assert.isTrue(!this.rejectPublicInvocations,
() -> "Secure object invocation " + object
+ " was denied as public invocations are not allowed via this interceptor. "
+ "This indicates a configuration error because the "
+ "rejectPublicInvocations property is set to 'true'");
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Authorized public object %s", object));
}
publishEvent(new PublicInvocationEvent(object));
return null; // no further work post-invocation
}
//校验认证
if (SecurityContextHolder.getContext().getAuthentication() == null) {
credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound",
"An Authentication object was not found in the SecurityContext"), object, attributes);
}
//获取登录用户信息
Authentication authenticated = authenticateIfRequired();
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Authorizing %s with attributes %s", object, attributes));
}
// Attempt authorization 进行鉴权
attemptAuthorization(object, attributes, authenticated);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Authorized %s with attributes %s", object, attributes));
}
//鉴权成功监听
if (this.publishAuthorizationSuccess) {
publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
// Attempt to run as a different user
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
if (runAs != null) {
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Switched to RunAs authentication %s", runAs));
}
// need to revert to token.Authenticated post-invocation
return new org.springframework.security.access.intercept.InterceptorStatusToken(origCtx, true, attributes, object);
}
this.logger.trace("Did not switch RunAs authentication since RunAsManager returned null");
// no further work post-invocation
return new org.springframework.security.access.intercept.InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
}
这里可以看到方法this.obtainSecurityMetadataSource().getAttributes(object),当我们自定义了FilterInvocationSecurityMetadataSource是,就是调用我们自定义方法中的getAttributes逻辑。同时获取获得用户的Authentication,里边包含用户基本信息和用户所有的权限。最后调用方法attemptAuthorization(object, attributes, authenticated)进行鉴权。
private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,
Authentication authenticated) {
try {
//使用投票器投票来决定用户是否有资源访问权限,鉴权入口
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException ex) {
// 2. 访问被拒绝。抛出AccessDeniedException异常
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Failed to authorize %s with attributes %s using %s", object,
attributes, this.accessDecisionManager));
}
else if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Failed to authorize %s with attributes %s", object, attributes));
}
//发送鉴权失败事件
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex));
throw ex;
}
}
最底层还是使用的投票器进行鉴权,默认实现是AffirmativeBased,一票通过,只要有一票通过就算通过。
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
if (deny > 0) {
throw new AccessDeniedException(
this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
可以看到是遍历所有的投票器,根据具体的逻辑执行相应的逻辑。
Spring Security中的投票器(Voter)是用于决定用户是否有权限访问特定资源的一种机制。投票器实现了AccessDecisionVoter接口,并根据配置的规则对用户进行投票。
投票器实现原理如下:
投票器的实现有好多种,我们可以选择其中一种或多种投票器,也可以自定义投票器,默认的投票器是 WebExpressionVoter。
Spring Security中的AccessDecisionVoter接口有多个实现,每个实现都有不同的含义和功能。以下是一些常见的AccessDecisionVoter实现及其含义:
RoleVoter:基于用户角色进行投票判断。它会检查用户是否具有所需的角色来访问资源。
AuthenticatedVoter:判断用户是否已经通过认证。它会检查用户是否已经进行了身份验证。
WebExpressionVoter:基于Web表达式进行投票判断。它可以使用SpEL表达式来定义授权规则,例如基于URL路径、HTTP方法、请求参数等进行判断。
Jsr250Voter:基于JSR-250注解进行投票判断。它会检查方法或类上的注解,例如@RolesAllowed、@PermitAll、@DenyAll等。
PreInvocationAuthorizationAdviceVoter:基于方法调用前的注解进行投票判断。它会检查方法上的注解,例如@PreAuthorize、@PostAuthorize等。
PostInvocationAuthorizationAdviceVoter:基于方法调用后的注解进行投票判断。它会检查方法上的注解,例如@PostAuthorize。
在默认的决策类AffirmativeBased中,我们没有做特殊配置的话,投票器会包含默认投票器:WebExpressionVoter。我们以WebExpressionVoter为例子看下代码。
public class WebExpressionVoter implements AccessDecisionVoter<FilterInvocation> {
private final Log logger = LogFactory.getLog(getClass());
private SecurityExpressionHandler<FilterInvocation> expressionHandler = new org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler();
@Override
public int vote(Authentication authentication, FilterInvocation filterInvocation,
Collection<ConfigAttribute> attributes) {
//参数校验
Assert.notNull(authentication, "authentication must not be null");
Assert.notNull(filterInvocation, "filterInvocation must not be null");
Assert.notNull(attributes, "attributes must not be null");
//获取http配置参数
org.springframework.security.web.access.expression.WebExpressionConfigAttribute webExpressionConfigAttribute = findConfigAttribute(attributes);
if (webExpressionConfigAttribute == null) {
this.logger
.trace("Abstained since did not find a config attribute of instance WebExpressionConfigAttribute");
return ACCESS_ABSTAIN;
}
//对EL表达式进行处理
EvaluationContext ctx = webExpressionConfigAttribute.postProcess(
this.expressionHandler.createEvaluationContext(authentication, filterInvocation), filterInvocation);
boolean granted = ExpressionUtils.evaluateAsBoolean(webExpressionConfigAttribute.getAuthorizeExpression(), ctx);
if (granted) {
//符合条件,赞成
return ACCESS_GRANTED;
}
this.logger.trace("Voted to deny authorization");
//反对
return ACCESS_DENIED;
}
可以看到这个是对使用了默认标签且使用EL表达式的处理。
权限定义和管理:
访问控制:
权限注解:
过滤器链:
安全配置:
需要注意的是,Spring Security的源码非常庞大且复杂,涉及到很多细节和设计模式。上述总结只是对权限相关部分的概括,并不能详尽地涵盖所有内容。