spring Security 投票器

项目有这样一个需求:要求访问项目的所有路径(也可以说资源)都要有相应的权限,应该说要为每一个路径、不同的请求(get,post,put,delete等)都加上访问权限。使用security 自己的配置文件当然可以管理所有的路径,并为每一个路径都加上访问权限。但是这样做便缺乏了一定的灵活性,因为一旦想改动某个路径(资源)的权限就不得不去改动配置文件,当然服务也得跟着重启。所以便有了通过web管理界面,将所有的资源都配置到数据库的需求,再配合权限管理,实现对资源的权限的灵活配置。

spring security 的配置文件如下:

		@Override
		public void configure(HttpSecurity http) throws Exception {
			super.configure(http);
			// @formatter:off
			String[] urls = {
				"/api/**",
				"/security/**",
			//	"/security/me/**",
				"/report/**",
				"/jms/**",
				"/webgis/**"
			};
			http
				.sessionManagement()
					.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
					.and()
				.requestMatchers()
					.antMatchers(urls)
					.and()
				.authorizeRequests()
					.withObjectPostProcessor(objectPostProcessorFilterSecurityInterceptor())
					.accessDecisionManager(accessDecisionManager)
					.antMatchers("/").permitAll()
					.antMatchers(urls).access("#oauth2.hasScope('read') or (!#oauth2.isOAuth() and hasRole('ROLE_USER'))")
					.and()
				.exceptionHandling()
					.defaultAuthenticationEntryPointFor(ajaxAuthenticationEntryPoint, ajaxRequestMatcher)
					.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
					.and()
			// @formatter:on
			;
		}
	}
以上的配置都是基于javaConfig,对于你要保护的路径,必须硬编码在配置文件里。


接下来是自己的实现,分为三步:

1.通过web端指定路径所需的权限

创建管理资源的数据库表,本项目中使jpa 自动创建表,相应的实体类如下:

package com.gpstogis.security.domain;

import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;

import com.gpstogis.domain.listener.PersistableEntityListener;
import com.gpstogis.persistence.Face;
import com.gpstogis.persistence.PartialIndex;

@Entity
@EntityListeners({ PersistableEntityListener.class })
public class SecurityResource extends AbstractSecurityPersistable {

	private static final long serialVersionUID = 1737376422799756876L;

	@Face
	@PartialIndex(where = "deleted = false", unique = true)
	@Column(name = "face", length = 128)
	private String face;

	@Column(name = "name", nullable = false, length = 64)
	private String name;

	@Column(name = "label", nullable = false, length = 64)
	private String label;

	@Column(name = "method", nullable = true, length = 64)
	private String method;

	@Column(name = "comments", length = 255)
	private String comments;

	@ManyToMany
	@JoinTable(name = "security_resource_authority", joinColumns = {
			@JoinColumn(name = "resource", referencedColumnName = "id") }, inverseJoinColumns = {
					@JoinColumn(name = "authority", referencedColumnName = "id") })
	private Set authorities;

	public SecurityResource() {
		super();
	}

	public void setName(String name) {
		this.name = name;
	}

	public void setLabel(String label) {
		this.label = label;
	}

	public void setFace(String face) {
		this.face = face;
	}

	public void setComments(String comments) {
		this.comments = comments;
	}

	public void setAuthorities(Set authorities) {
		this.authorities = authorities;
	}

	public String getName() {
		return name;
	}

	public String getLabel() {
		return label;
	}

	public String getFace() {
		return face;
	}

	public String getComments() {
		return comments;
	}

	public Set getAuthorities() {
		return authorities;
	}

	public String getMethod() {
		return method;
	}

	public void setMethod(String method) {
		this.method = method;
	}

}

如:通过get方法去请求“api/myresource/id” 这个路径,所需的权限为“Auth_Get_myresource”.则在表中相应的数据为:

name:“api/myresource/id” ,label:"我的资源“,method:"get”,authorities:{“Auth_Get_myresource”}.

2.拦截请求,定义自己的拦截器,去数据库中查询所需的权限

public class SecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

	private static final Log logger = LogFactory.getLog(WebSecurityConfigurerAdapter.class);

	private final SecurityResourceService securityResourceService;
	private final FilterInvocationSecurityMetadataSource securityMetadataSource;

	public SecurityMetadataSource(FilterInvocationSecurityMetadataSource securityMetadataSource,
			SecurityResourceService securityResourceService) {
		this.securityMetadataSource = securityMetadataSource;
		this.securityResourceService = securityResourceService;
	}

	@Override
	public Collection getAttributes(Object object) {
		// TODO fast use cache.
		Collection attributes = securityMetadataSource.getAttributes(object);
		FilterInvocation filterInvocation = (FilterInvocation) object;
		String name = filterInvocation.getRequestUrl();
		String method = filterInvocation.getRequest().getMethod();
		if (logger.isDebugEnabled()) {
			logger.debug("The " + method + " " + name + " code require authority: "
					+ StringUtils.collectionToCommaDelimitedString(attributes));
		}
		int index = name.indexOf("?");
		if (index > 0) {
			name = name.substring(0, index);
		}
		Collection dbAttributes = securityResourceService.loadConfigAttributesByNameAndMethod(name,
				method);
		if (dbAttributes != null) {
			attributes = new HashSet<>(dbAttributes);
			if (logger.isDebugEnabled()) {
				logger.debug("The " + method + " " + name + " db require authority: "
						+ StringUtils.collectionToCommaDelimitedString(attributes));
			}
		}
		return attributes;
	}

	@Override
	public Collection getAllConfigAttributes() {
		return securityMetadataSource.getAllConfigAttributes();
	}

	@Override
	public boolean supports(Class clazz) {
		return securityMetadataSource.supports(clazz);
	}

首先要去实现FilterInvocationSecurityMetadataSource接口,该接口负责去查找资源所需的权限。以上红色代码部分,是自己去数据库查找权限,如果在库中配置了权限,则会覆盖掉在配置文件中的权限。

使用javaConfig修改配置文件,使用对象后处理器,将拦截器中用来查找权限的FilterInvocationSecurityMetadataSource类改为自己的:

	@Bean
	public ObjectPostProcessor objectPostProcessorFilterSecurityInterceptor() {
		return new ObjectPostProcessor() {
			@Override
			public  O postProcess(O object) {
				FilterInvocationSecurityMetadataSource securityMetadataSource = new SecurityMetadataSource(
						object.getSecurityMetadataSource(), resourceService);
				object.setSecurityMetadataSource(securityMetadataSource);
				return object;
			}
		};
	}

3.实现自己的投票器,判断是否有权限访问资源

package com.gpstogis.security;

import java.util.Collection;

import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;

import com.gpstogis.security.domain.SecurityAuthority;

@Configurable
public class SecurityAuthorityVoter implements AccessDecisionVoter {

	@Override
	public boolean supports(ConfigAttribute attribute) {
		if (attribute.getAttribute() != null && attribute instanceof SecurityAuthority) {
			return true;
		}
		return false;
	}

	@Override
	public boolean supports(Class clazz) {
		return true;
	}

	@Override
	public int vote(Authentication authentication, Object object, Collection attributes) {
		String prefix = "AUTH_";
		Collection userAuthorities = authentication.getAuthorities();
		for (ConfigAttribute neededAuthority : attributes) {
			if (this.supports(neededAuthority)) {
				// Attempt to find a matching granted authority
				for (GrantedAuthority ownAuthority : userAuthorities) {
					if ((prefix + neededAuthority.getAttribute()).equals(ownAuthority.getAuthority())) {
						return ACCESS_GRANTED;
					}
				}
			} else {
				return ACCESS_ABSTAIN;
			}
		}
		return ACCESS_DENIED;
	}

}
 
  
配置文件代码:
	@Bean
	@Lazy
	public SecurityAuthorityVoter autoMatchVoter() {
		SecurityAuthorityVoter autoMatchVoter = new SecurityAuthorityVoter();
		return autoMatchVoter;
	}

	/**
	 * Used by Security load authority from database.
	 *
	 * @return
	 */
	@Bean
	@Lazy
	public AccessDecisionManager accessDecisionManager() {
		List> decisionVoters = new ArrayList>();
		decisionVoters.add(new AuthenticatedVoter());
		decisionVoters.add(new RoleVoter());
		decisionVoters.add(autoMatchVoter());
		WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
		webExpressionVoter.setExpressionHandler(expressionHandler);
		decisionVoters.add(webExpressionVoter);
		AffirmativeBased accessDecisionManager = new AffirmativeBased(decisionVoters);
		return accessDecisionManager;
	};
在访问决策管理器中加入自己的投票器,注意投票器的次序,在使用AffirmativeBased 访问决策管理器时,只要有一票通过则放权。所以为了让自定义投票起作用,次序最好放在前边。


好了,到此为止,自定义的投票器是实现就完成了。问题的关键就在于:对于secured object(资源)是在哪里找到他的对应的权限的,(FilterInvocationSecurityMetadataSource 负责查询资源的权限),还有就是通过配置加入自己的投票器。

访问决策管理器提供了三种投票方式:

1.AffirmativeBased :有一票通过即认证成功

2.ConsensusBased:多数通过即可

3.UnanimousBased:全票通过才算认证成功

对于以上三种方式,需要根据自己项目的需求选择不同的投票方式。在此就不多说了。

你可能感兴趣的:(spring,security)