spring Security 投票器

来源:互联网 发布:泰迪体质 知乎 编辑:程序博客网 时间:2024/06/11 23:38

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

spring security 的配置文件如下:

@Overridepublic void configure(HttpSecurity http) throws Exception {super.configure(http);// @formatter:offString[] 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<SecurityAuthority> 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<SecurityAuthority> 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<SecurityAuthority> 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;}@Overridepublic Collection<ConfigAttribute> getAttributes(Object object) {// TODO fast use cache.Collection<ConfigAttribute> 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<SecurityAuthority> 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;}@Overridepublic Collection<ConfigAttribute> getAllConfigAttributes() {return securityMetadataSource.getAllConfigAttributes();}@Overridepublic boolean supports(Class<?> clazz) {return securityMetadataSource.supports(clazz);}

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

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

@Beanpublic ObjectPostProcessor<FilterSecurityInterceptor> objectPostProcessorFilterSecurityInterceptor() {return new ObjectPostProcessor<FilterSecurityInterceptor>() {@Overridepublic <O extends FilterSecurityInterceptor> 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;@Configurablepublic class SecurityAuthorityVoter implements AccessDecisionVoter<Object> {@Overridepublic boolean supports(ConfigAttribute attribute) {if (attribute.getAttribute() != null && attribute instanceof SecurityAuthority) {return true;}return false;}@Overridepublic boolean supports(Class<?> clazz) {return true;}@Overridepublic int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {String prefix = "AUTH_";Collection<? extends GrantedAuthority> userAuthorities = authentication.getAuthorities();for (ConfigAttribute neededAuthority : attributes) {if (this.supports(neededAuthority)) {// Attempt to find a matching granted authorityfor (GrantedAuthority ownAuthority : userAuthorities) {if ((prefix + neededAuthority.getAttribute()).equals(ownAuthority.getAuthority())) {return ACCESS_GRANTED;}}} else {return ACCESS_ABSTAIN;}}return ACCESS_DENIED;}}

配置文件代码:
@Bean@Lazypublic SecurityAuthorityVoter autoMatchVoter() {SecurityAuthorityVoter autoMatchVoter = new SecurityAuthorityVoter();return autoMatchVoter;}/** * Used by Security load authority from database. * * @return */@Bean@Lazypublic AccessDecisionManager accessDecisionManager() {List<AccessDecisionVoter<? extends Object>> decisionVoters = new ArrayList<AccessDecisionVoter<? extends Object>>();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:全票通过才算认证成功

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