【发布时间】:2010-12-29 21:04:55
【问题描述】:
我正在使用 spring-security-tiger-2.0.5。
有没有办法以编程方式向 Spring Bean 添加安全代理?
我正在通过 BeanDefinitionBuilder 构造 bean,并且我想添加与 @Secured 注释相同的行为。
roleName 的 @Secured 等效项将作为参数传递。
【问题讨论】:
标签: java spring spring-security
我正在使用 spring-security-tiger-2.0.5。
有没有办法以编程方式向 Spring Bean 添加安全代理?
我正在通过 BeanDefinitionBuilder 构造 bean,并且我想添加与 @Secured 注释相同的行为。
roleName 的 @Secured 等效项将作为参数传递。
【问题讨论】:
标签: java spring spring-security
感谢您的回答,但我终于让它以完全程序化的方式工作。这不是一个简单的解决方案,但我想分享它。
首先,我可以把问题分成两部分:
1) 为我自己的 bean 创建代理。
2) 将安全角色添加到每个 bean。不幸的是,我无法添加基于 xml 的切入点,因为 bean 都是同一个类(通用服务)。我想要一种以不同方式保护每个 bean 的方法,尽管它们都是同一个类。 我决定使用与 aopalliance 相同的声明((执行 . 等...)
对于可能感兴趣的人,我是这样做的:
1) 使用 spring 的 BeanNameAutoProxyCreator 类,用于围绕我手动添加的类创建代理:
private void addProxies(ConfigurableListableBeanFactory beanFactory, List<String> exportedServices) {
List<String> interceptorNames = findInterceptorNames(beanFactory);
BeanNameAutoProxyCreator beanPostProcessor = new BeanNameAutoProxyCreator();
beanPostProcessor.setBeanNames(exportedServices.toArray(new String[exportedServices.size()]));
beanPostProcessor.setInterceptorNames(interceptorNames.toArray(new String[interceptorNames.size()]));
beanPostProcessor.setBeanFactory(beanFactory);
beanPostProcessor.setOrder(Ordered.LOWEST_PRECEDENCE);
beanFactory.addBeanPostProcessor(beanPostProcessor);
}
@SuppressWarnings("unchecked")
private List<String> findInterceptorNames(ConfigurableListableBeanFactory beanFactory) {
List<String> interceptorNames = new ArrayList<String>();
List<? extends Advisor> list = new BeanFactoryAdvisorRetrievalHelper(beanFactory).findAdvisorBeans();
for (Advisor ad : list) {
Advice advice = ad.getAdvice();
if (advice instanceof MethodInterceptor) {
Map<String, ?> beansOfType = beanFactory.getBeansOfType(advice.getClass());
for (String name : beansOfType.keySet()) {
interceptorNames.add(name);
}
}
}
return interceptorNames;
}
2) 对于可参数化的类安全部分:
创建了 spring 的 MethodDefinitionSource 的实现。 MethodSecurityProxy 使用此接口的实现者在运行时获取securedObjects。
@SuppressWarnings("unchecked")
@Override
public ConfigAttributeDefinition getAttributes(Object object) throws IllegalArgumentException {
if (!(object instanceof ReflectiveMethodInvocation)) {
return null;
}
ReflectiveMethodInvocation invokation = (ReflectiveMethodInvocation) object;
if (!(invokation.getThis() instanceof MyService)) {
return null;
}
MyService service = (MyService) invokation.getThis();
Map<String, String> map =getProtectedServiceMethodMap(service);
String roles = map.get(MethodKeyGenerator.generate(invokation.getMethod()));
return roles == null ? null : new ConfigAttributeDefinition(StringUtils.delimitedListToStringArray(roles, ","));
}
最棘手的部分是将我的 MethodDefinitionSource impl 注册到 applicationContext 中,以便 MethodSecurityInterceptor 意识到它:
private void addCrudDefinitionSource() {
DelegatingMethodDefinitionSource source = (DelegatingMethodDefinitionSource) beanFactory.getBean(BeanIds.DELEGATING_METHOD_DEFINITION_SOURCE);
try {
Field field = source.getClass().getDeclaredField("methodDefinitionSources");
field.setAccessible(true);
List list = (List) field.get(source);
list.add(new MyMethodDefinitionSource());
} catch (Exception e) {
e.printStackTrace();
}
}
最后,为了检查 aop 样式的方法入口点,我使用了如下代码:
private void addToCache(){ // Ommiting parameters
PointcutExpression expression = parser.parsePointcutExpression(fullExpression);
Method[] methods = clazzToCheck.getMethods();
for (int i = 0; i < methods.length; i++) {
Method currentMethod = methods[i];
boolean matches = expression.matchesMethodExecution(currentMethod).alwaysMatches();
if (matches){
//Add to Cache
}
}
【讨论】:
要以编程方式将 Spring Security 功能注入现有 bean,您可能需要使用 Spring Security 应用程序上下文并在那里注册您的 bean:
@Test
public void testSpringSecurity() throws Exception {
InMemoryXmlApplicationContext ctx = initSpringAndSpringSecurity();
// Creates new instance
IMyService secured = (IMyService) ctx.getAutowireCapableBeanFactory()
.initializeBean(new MyService(), "myService");
assertTrue(AopUtils.isAopProxy(secured));
fakeSecurityContext("ROLE_USER");
secured.getCustomers(); // Works: @Secured("ROLE_USER")
fakeSecurityContext("ROLE_FOO");
try {
secured.getCustomers(); // Throws AccessDenied Exception
fail("AccessDeniedException expected");
} catch (AccessDeniedException expected) {
}
}
private InMemoryXmlApplicationContext initSpringAndSpringSecurity() {
InMemoryXmlApplicationContext ctx = new InMemoryXmlApplicationContext(
"<b:bean id='authenticationManager' class='org.springframework.security.MockAuthenticationManager' /> "
+ "<b:bean id='accessDecisionManager' class='org.springframework.security.vote.UnanimousBased'><b:property name='decisionVoters'><b:list><b:bean class='org.springframework.security.vote.RoleVoter'/></b:list></b:property></b:bean>"
+ "<b:bean id='objectDefinitionSource' class='org.springframework.security.annotation.SecuredMethodDefinitionSource' /> "
+ "<b:bean id='autoproxy' class='org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator'/>"
+ "<b:bean id='methodSecurityAdvisor' class='org.springframework.security.intercept.method.aopalliance.MethodDefinitionSourceAdvisor' autowire='constructor'/>"
+ "<b:bean id='securityInterceptor' class='org.springframework.security.intercept.method.aopalliance.MethodSecurityInterceptor'><b:property name='authenticationManager' ref='authenticationManager' /><b:property name='accessDecisionManager' ref='accessDecisionManager' /><b:property name='objectDefinitionSource' ref='objectDefinitionSource' /></b:bean>");
return ctx;
}
我使用了内存中的应用程序上下文,正如 MethodDefinitionSourceAdvisor 在其文档中指出的那样,仅对 ApplicationContexts 启用了自动代理。因此,如果您不为每个服务对象使用单独的ProxyFactoryBean,我相信您需要一个用于自动代理的应用程序上下文。但是由于您使用的是@Secured 注释,我怀疑这与自动代理相同。
fakeSecurityContext() 只是将具有给定角色的Authentication 对象设置到SecurityContextHolder 中以用于测试目的。
您可以自己使用 Spring Core 功能来完成。假设您有一个返回客户列表的服务,并且当前用户只能查看具有特定收入限制的客户。下面的测试用例将是我们的开始:
@Test
public void testSecurity() throws Exception {
ClassPathXmlApplicationContext appCtx = new ClassPathXmlApplicationContext(
"spring.xml");
IMyService service = (IMyService) appCtx.getBean("secured",
IMyService.class);
assertEquals(1, service.getCustomers().size());
}
这是原来的服务实现:
public class MyService implements IMyService {
public List<Customer> getCustomers() {
return Arrays.asList(new Customer(100000), new Customer(5000));
}
}
在spring.xml中配置你的服务对象并添加方法拦截器:
<bean id="service" class="de.mhaller.spring.MyService"></bean>
<bean id="securityInterceptor" class="de.mhaller.spring.MyServiceInterceptor">
<property name="revenueLimit" value="50000"/>
</bean>
<bean id="secured" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="targetName" value="service" />
<property name="interceptorNames">
<list>
<value>securityInterceptor</value>
</list>
</property>
</bean>
安全拦截器实现:
public class MyServiceInterceptor implements MethodInterceptor {
private int revenueLimit = 10000;
public void setRevenueLimit(int revenueLimit) {
this.revenueLimit = revenueLimit;
}
@SuppressWarnings("unchecked")
public Object invoke(MethodInvocation mi) throws Throwable {
List<Customer> filtered = new ArrayList<Customer>();
List<Customer> result = (List<Customer>) mi.proceed();
for (Customer customer : result) {
if (customer.isRevenueBelow(revenueLimit)) {
filtered.add(customer);
}
}
return filtered;
}
}
使用这种方法的优势在于,您不仅可以以声明方式检查当前用户的角色,还可以以动态方式执行公司策略,例如根据业务价值限制返回的 Objects。
【讨论】:
您应该查看 org.springframework.aop.framework.ProxyFactory 班级。用 ProxyFactory 包装你的 bean 并添加安全拦截器
【讨论】: