目录

  • 1. 用户权限
  • 2. 资源权限
  • 2.1 自定义资源权限
  • 2.2 权限注解
  • 2.2.1 默认注解
  • 2.2.2 自定义注解
  • 2.2.3 注解初始化和保存
  • 3. 代码示例
  • 4. 源码解析
  • 4.1 投票器
  • 4.1.1 投票器不同实现类的含义
  • 4.1.2 默认投票器WebExpressionVoter
  • 5. 总结



SpringSecurity的授权是依赖于过滤器FilterSecurityInterceptor实现的。授权的步骤主要分为三步:

  1. 获取当前用户所具有的权限。比如管理员,具有所有页面,所有接口的权限。
  2. 当前请求路径所需要的权限。比如修改用户信息接口,需要管理员角色,修改接口权限。
  3. 当前请求路径所取权限是否在当前用户已有权限中。也就是判断下步骤1中用户权限集合是否包含步骤2中url所需的权限。

1. 用户权限

用户权限一般我们会保存在数据库,当用户信息UserDetails初始化的时候设置到GrantedAuthority中。这一步一般在我们自定义实现接口UserDetailsService中做。通过此操作,用户全部权限加载到了Authentication,并存到了全局上下文SecurityContextHolder中。后续请求可直接从SecurityContextHolder中获取用户权限信息。

public class UserDetailsServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username){
			//省略其他逻辑
			...
            //根据用户名从数据库加载用户权限
			
			//设置到UserDetails
            
        }
    }

以上只是伪代码,具体逻辑要根据自己的项目编写代码。

2. 资源权限

所谓的资源权限就是所有我们的菜单,接口,按钮等。比如修改用户部门这个接口只能提供给管理员,客服只能拥有客服的权限,不能拥有修改其他用户的权限。每个资源都对应一个权限,下边我们介绍两个常用的权限设计。

2.1 自定义资源权限

一般情况下我们要把资源的权限配置到数据库中,一般会配置具体的页面权限,接口权限等。我们依托于数据库做资源权限管理时,是要实现接口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与权限的映射关系。

2.2 权限注解

SpringSecurity不仅提供了自带的注解来设置接口权限,也可以自定义注解

2.2.1 默认注解

在Spring Security中,有几个常用的自带注解用于定义资源权限,它们的含义如下:

  1. @PreAuthorize :在方法执行前进行权限验证。如果验证失败,将抛出 AccessDeniedException 异常。
  2. @PostAuthorize :在方法执行后进行权限验证。如果验证失败,将抛出 AccessDeniedException 异常。
  3. @Secured :在方法执行前进行角色验证。只有具有指定角色的用户才能访问该方法。
  4. @RolesAllowed :在方法执行前进行角色验证。只有具有指定角色的用户才能访问该方法。

以下是一个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表达式校验逻辑。

2.2.2 自定义注解

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();
    }
}

2.2.3 注解初始化和保存

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的源码中找到,包括注解的解析和验证过程。

3. 代码示例

以下是一个示例的Spring Boot项目中的Java代码,演示如何自定义用户权限存储到数据库并替换原有的FilterSecurityInterceptor过滤器,并分别实现AccessDecisionManager、InvocationSecurityMetadataSourceService和UserDetailsService接口。

此代码示例只是一个简单的流程展示,具体到项目中要根据实际业务做调整。

  1. 创建一个用于存储用户权限的数据库表
    实际项目中权限表会很复杂,此代码示例中权限用一个字段role代替。
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
);
  1. 创建一个UserEntity类来表示用户实体
@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);
}
  1. 实现UserDetailsService接口,从数据库加载用户信息并与Spring Security集成
@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();
    }
}
  1. 实现InvocationSecurityMetadataSourceService接口
@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);
    }
}
  1. 实现AccessDecisionManager接口
@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;
    }
}
  1. 创建一个CustomFilterSecurityInterceptor类来替代FilterSecurityInterceptor过滤器
@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();
    }
}
  1. 在Spring Security配置类中,配置自定义的UserDetailsService、FilterSecurityInterceptor和AccessDecisionManager。
@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接口的操作。

请注意,这只是一个示例,具体的实现细节可能因您的应用程序架构和需求而有所不同。

4. 源码解析

上文中是具体在项目中我们需要配置和自定义的业务逻辑,下边我们看下在源码中它们的使用。
我们先从过滤器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();
	}

可以看到是遍历所有的投票器,根据具体的逻辑执行相应的逻辑。

4.1 投票器

Spring Security中的投票器(Voter)是用于决定用户是否有权限访问特定资源的一种机制。投票器实现了AccessDecisionVoter接口,并根据配置的规则对用户进行投票。
投票器实现原理如下:

  1. 当用户请求访问资源时,Spring Security会调用AccessDecisionManager来进行决策。
  2. AccessDecisionManager会遍历所有配置的投票器,并调用它们的vote方法进行投票。
  3. 每个投票器会根据自身的逻辑判断用户是否有权限访问资源,并返回投票结果。
  4. 投票结果可以是ACCESS_GRANTED(允许访问)、ACCESS_DENIED(拒绝访问)或ACCESS_ABSTAIN(弃权)。
  5. AccessDecisionManager会根据投票结果进行最终的决策,决定用户是否有权限访问资源。
    Spring Security提供了多个默认的投票器实现,例如RoleVoter、AuthenticatedVoter等。开发人员也可以自定义投票器来实现特定的授权逻辑。
    注意:以上是Spring Security投票器的一般实现原理,具体实现细节可能会有所不同。

投票器的实现有好多种,我们可以选择其中一种或多种投票器,也可以自定义投票器,默认的投票器是 WebExpressionVoter。

4.1.1 投票器不同实现类的含义

Spring Security中的AccessDecisionVoter接口有多个实现,每个实现都有不同的含义和功能。以下是一些常见的AccessDecisionVoter实现及其含义:

  1. RoleVoter:基于用户角色进行投票判断。它会检查用户是否具有所需的角色来访问资源。
  2. AuthenticatedVoter:判断用户是否已经通过认证。它会检查用户是否已经进行了身份验证。
  3. WebExpressionVoter:基于Web表达式进行投票判断。它可以使用SpEL表达式来定义授权规则,例如基于URL路径、HTTP方法、请求参数等进行判断。
  4. Jsr250Voter:基于JSR-250注解进行投票判断。它会检查方法或类上的注解,例如@RolesAllowed、@PermitAll、@DenyAll等。
  5. PreInvocationAuthorizationAdviceVoter:基于方法调用前的注解进行投票判断。它会检查方法上的注解,例如@PreAuthorize、@PostAuthorize等。
  6. PostInvocationAuthorizationAdviceVoter:基于方法调用后的注解进行投票判断。它会检查方法上的注解,例如@PostAuthorize。

4.1.2 默认投票器WebExpressionVoter

在默认的决策类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表达式的处理。

5. 总结

  1. 权限定义和管理:
  • 权限定义通常通过角色或权限字符串进行管理,可以在数据库或配置文件中进行配置。
  • 用户的角色和权限信息可以通过实现UserDetailsService接口从数据库加载。
  1. 访问控制:
  • 访问控制是通过AccessDecisionManager接口实现的,它决定了用户是否有权访问特定的资源。
  • AccessDecisionManager使用AccessDecisionVoter实现投票机制,根据用户的角色和权限进行决策。
  1. 权限注解:
  • Spring Security提供了注解来简化权限控制,如@PreAuthorize和@PostAuthorize。
  • 这些注解可以直接应用在方法或类上,用于限制访问权限。
  1. 过滤器链:
  • Spring Security使用过滤器链来对请求进行安全处理。
  • FilterSecurityInterceptor是Spring Security中负责访问控制的核心过滤器,它基于配置的拦截规则进行访问控制。
  1. 安全配置:
  • 安全配置是通过实现WebSecurityConfigurerAdapter类来完成的。
  • 在安全配置中,可以定义访问规则、用户认证方式、密码加密方式等。

需要注意的是,Spring Security的源码非常庞大且复杂,涉及到很多细节和设计模式。上述总结只是对权限相关部分的概括,并不能详尽地涵盖所有内容。