项目有这样一个需求:要求访问项目的所有路径(也可以说资源)都要有相应的权限,应该说要为每一个路径、不同的请求(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);
}
使用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
@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:全票通过才算认证成功
对于以上三种方式,需要根据自己项目的需求选择不同的投票方式。在此就不多说了。