当前位置:  开发笔记 > 编程语言 > 正文

使用Spring Security进行单元测试

如何解决《使用SpringSecurity进行单元测试》经验,为你挑选了6个好方法。

我的公司一直在评估Spring MVC,以确定我们是否应该在下一个项目中使用它.到目前为止,我喜欢我所看到的内容,现在我正在查看Spring Security模块,以确定它是否可以/应该使用.

我们的安全要求非常基本; 用户只需提供用户名和密码即可访问网站的某些部分(例如获取有关其帐户的信息); 并且网站上有一些页面(常见问题解答,支持等),应该授予匿名用户访问权限.

在我创建的原型中,我一直在Session中为经过身份验证的用户存储"LoginCredentials"对象(其中只包含用户名和密码); 例如,某些控制器检查此对象是否在会话中以获取对登录用户名的引用.我正在寻找用Spring Security取代这个本土逻辑,这将有很好的好处,可以删除任何类型的"我们如何跟踪登录用户?" 和"我们如何验证用户?" 来自我的控制器/业务代码.

似乎Spring Security提供了一个(每个线程)"上下文"对象,可以从应用程序的任何位置访问用户名/主体信息...

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

...在某种程度上,这个对象是一个(全局)单例,这似乎非常不像Spring.

我的问题是:如果这是在Spring Security中访问有关经过身份验证的用户的信息的标准方法,那么将Authentication对象注入SecurityContext的可接受方式是什么,以便在单元测试需要时可用于我的单元测试认证用户?

我是否需要在每个测试用例的初始化方法中进行连接?

protected void setUp() throws Exception {
    ...
    SecurityContextHolder.getContext().setAuthentication(
        new UsernamePasswordAuthenticationToken(testUser.getLogin(), testUser.getPassword()));
    ...
}

这似乎过于冗长.有没有更简单的方法?

SecurityContextHolder物体本身似乎非常联合国春天般的...



1> 小智..:

只需按常规方式执行,然后SecurityContextHolder.setContext()在测试类中使用它,例如:

控制器:

Authentication a = SecurityContextHolder.getContext().getAuthentication();

测试:

Authentication authentication = Mockito.mock(Authentication.class);
// Mockito.whens() for your authorization object
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
Mockito.when(securityContext.getAuthentication()).thenReturn(authentication);
SecurityContextHolder.setContext(securityContext);


@Leonardo应该在控制器中添加`Authentication a`?我在每个方法调用中都能理解吗?"弹簧方式"只是添加它而不是注入是否可以?

2> cliff.meyers..:

问题是Spring Security不会将Authentication对象作为容器中的bean使用,因此无法轻易地将其注入或自动装入盒中.

在我们开始使用Spring Security之前,我们将在容器中创建一个会话范围的bean来存储Principal,将其注入"AuthenticationService"(单例),然后将此bean注入需要了解当前Principal的其他服务.

如果您正在实现自己的身份验证服务,您基本上可以做同样的事情:创建一个具有"principal"属性的会话范围的bean,将其注入您的身份验证服务,让auth服务在成功的身份验证中设置该属性,然后根据需要将auth服务提供给其他bean.

使用SecurityContextHolder我不会感觉太糟糕.虽然.我知道它是一个静态/ Singleton,并且Spring不鼓励使用这些东西,但是它们的实现需要根据环境进行适当的操作:在Servlet容器中使用会话作用域,在JUnit测试中使用线程作用,等等.真正的限制因素Singleton的用途是它提供了一种对不同环境不灵活的实现.


虽然只有一个注意事项 - 我不认为ServletContextHolder有任何HttpSession的概念或者知道它是否在Web服务器环境中运行的方式 - 它使用ThreadLocal,除非你将它配置为使用其他东西(唯一的其他两个内置模式是InheritableThreadLocal和全球)

3> Pavel..:

你很关心 - 静态方法调用对于单元测试尤其有问题,因为你不能轻易地模拟你的依赖项.我要向您展示的是如何让Spring IoC容器为您完成脏工作,为您提供整洁,可测试的代码.SecurityContextHolder是一个框架类,虽然您可以将低级安全代码绑定到它,但您可能希望为UI组件(即控制器)公开更整洁的接口.

cliff.meyers提到了一种解决方法 - 创建自己的"主体"类型并向消费者注入实例.2.x中引入的Spring < aop:scoped-proxy />标记与请求范围bean定义相结合,而工厂方法支持可能是最易读代码的票证.

它可以像下面这样工作:

public class MyUserDetails implements UserDetails {
    // this is your custom UserDetails implementation to serve as a principal
    // implement the Spring methods and add your own methods as appropriate
}

public class MyUserHolder {
    public static MyUserDetails getUserDetails() {
        Authentication a = SecurityContextHolder.getContext().getAuthentication();
        if (a == null) {
            return null;
        } else {
            return (MyUserDetails) a.getPrincipal();
        }
    }
}

public class MyUserAwareController {        
    MyUserDetails currentUser;

    public void setCurrentUser(MyUserDetails currentUser) { 
        this.currentUser = currentUser;
    }

    // controller code
}

到目前为止没有什么复杂的,对吧?事实上,你可能已经完成了大部分工作.接下来,在bean上下文中定义一个请求范围的bean来保存主体:


    



    
    

由于aop:scoped-proxy标记的神奇之处,每次有新的HTTP请求进入时都会调用静态方法getUserDetails,并且正确解析对currentUser属性的任何引用.现在单元测试变得微不足道了:

protected void setUp() {
    // existing init code

    MyUserDetails user = new MyUserDetails();
    // set up user as you wish
    controller.setCurrentUser(user);
}

希望这可以帮助!



4> matsev..:

如果不回答有关如何创建和注入身份验证对象的问题,Spring Security 4.0在测试时提供了一些受欢迎的替代方案.该@WithMockUser注释使开发人员可以指定一个模拟用户(可选配主管部门,用户名,密码和角色)一种巧妙的方法:

@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {
    String message = messageService.getMessage();
    ...
}

还可以选择用来@WithUserDetails模拟UserDetails从中返回的内容UserDetailsService,例如

@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
    String message = messageService.getMessage();
    ...
}

更多细节可以在Spring Security参考文档中的@WithMockUser和@WithUserDetails章节中找到(从上面复制的例子)



5> 小智..:

就个人而言,我只会使用Powermock和Mockito或Easymock来模拟单元/集成测试中的静态SecurityContextHolder.getSecurityContext(),例如

@RunWith(PowerMockRunner.class)
@PrepareForTest(SecurityContextHolder.class)
public class YourTestCase {

    @Mock SecurityContext mockSecurityContext;

    @Test
    public void testMethodThatCallsStaticMethod() {
        // Set mock behaviour/expectations on the mockSecurityContext
        when(mockSecurityContext.getAuthentication()).thenReturn(...)
        ...
        // Tell mockito to use Powermock to mock the SecurityContextHolder
        PowerMockito.mockStatic(SecurityContextHolder.class);

        // use Mockito to set up your expectation on SecurityContextHolder.getSecurityContext()
        Mockito.when(SecurityContextHolder.getSecurityContext()).thenReturn(mockSecurityContext);
        ...
    }
}

不可否认,这里有相当多的样板代码,即模拟一个Authentication对象,模拟一个SecurityContext来返回Authentication,最后模拟SecurityContextHolder来获取SecurityContext,但它非常灵活,允许你对null认证对象等场景进行单元测试等,而无需更改您的(非测试)代码



6> Michael Bush..:

在这种情况下使用静态是编写安全代码的最佳方法.

是的,静态通常很糟糕 - 通常,但在这种情况下,静态就是你想要的.由于安全上下文将Principal与当前运行的线程相关联,因此最安全的代码将尽可能直接地从线程访问静态.隐藏注入的包装类后面的访问权限会为攻击者提供更多攻击点.他们不需要访问代码(如果jar被签名,他们将很难改变它们),他们只需要一种覆盖配置的方法,这可以在运行时完成或将一些XML滑入类路径.即使使用注释注入也可以使用外部XML覆盖.这样的XML可能会为正在运行的系统注入一个流氓主体.

推荐阅读
小色米虫_524
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有