Java 使用 Spring Security 进行单元测试
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/360520/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Unit testing with Spring Security
提问by matt b
My company has been evaluating Spring MVC to determine if we should use it in one of our next projects. So far I love what I've seen, and right now I'm taking a look at the Spring Security module to determine if it's something we can/should use.
我的公司一直在评估 Spring MVC 以确定我们是否应该在我们的下一个项目之一中使用它。到目前为止,我喜欢我所看到的,现在我正在查看 Spring Security 模块,以确定它是否是我们可以/应该使用的东西。
Our security requirements are pretty basic; a user just needs to be able to provide a username and password to be able to access certain parts of the site (such as to get info about their account); and there are a handful of pages on the site (FAQs, Support, etc) where an anonymous user should be given access.
我们的安全要求非常基本;用户只需提供用户名和密码即可访问网站的某些部分(例如获取有关其帐户的信息);并且该站点上有一些页面(常见问题解答、支持等),应授予匿名用户访问权限。
In the prototype I've been creating, I have been storing a "LoginCredentials" object (which just contains username and password) in Session for an authenticated user; some of the controllers check to see if this object is in session to get a reference to the logged-in username, for example. I'm looking to replace this home-grown logic with Spring Security instead, which would have the nice benefit of removing any sort of "how do we track logged in users?" and "how do we authenticate users?" from my controller/business code.
在我创建的原型中,我一直在 Session 中为经过身份验证的用户存储一个“LoginCredentials”对象(它只包含用户名和密码);例如,一些控制器检查此对象是否在会话中以获取对登录用户名的引用。我正在寻求用 Spring Security 替换这个自有逻辑,这将有一个很好的好处,即删除任何类型的“我们如何跟踪登录用户?” 以及“我们如何对用户进行身份验证?” 从我的控制器/业务代码。
It seems like Spring Security provides a (per-thread) "context" object to be able to access the username/principal info from anywhere in your app...
似乎 Spring Security 提供了一个(每线程)“上下文”对象,以便能够从应用程序的任何位置访问用户名/主体信息......
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
... which seems very un-Spring like as this object is a (global) singleton, in a way.
...这似乎非常不像 Spring,因为这个对象在某种程度上是一个(全局)单例。
My question is this: if this is the standard way to access information about the authenticated user in Spring Security, what is the accepted way to inject an Authentication object into the SecurityContext so that it is available for my unit tests when the unit tests require an authenticated user?
我的问题是:如果这是在 Spring Security 中访问有关经过身份验证的用户的信息的标准方法,那么将 Authentication 对象注入到 SecurityContext 中的可接受方法是什么,以便在单元测试需要认证用户?
Do I need to wire this up in the initialization method of each test case?
我需要在每个测试用例的初始化方法中连接它吗?
protected void setUp() throws Exception {
...
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(testUser.getLogin(), testUser.getPassword()));
...
}
This seems overly verbose. Is there an easier way?
这似乎过于冗长。有更容易的方法吗?
The SecurityContextHolder
object itself seems very un-Spring-like...
该SecurityContextHolder
对象本身似乎非常不像 Spring...
采纳答案by cliff.meyers
The problem is that Spring Security does not make the Authentication object available as a bean in the container, so there is no way to easily inject or autowire it out of the box.
问题是 Spring Security 不会使 Authentication 对象作为容器中的 bean 可用,因此无法轻松地将其注入或自动装配开箱即用。
Before we started to use Spring Security, we would create a session-scoped bean in the container to store the Principal, inject this into an "AuthenticationService" (singleton) and then inject this bean into other services that needed knowledge of the current Principal.
在我们开始使用 Spring Security 之前,我们将在容器中创建一个会话范围的 bean 来存储 Principal,将其注入“AuthenticationService”(单例),然后将此 bean 注入其他需要了解当前 Principal 的服务。
If you are implementing your own authentication service, you could basically do the same thing: create a session-scoped bean with a "principal" property, inject this into your authentication service, have the auth service set the property on successful auth, and then make the auth service available to other beans as you need it.
如果您正在实现自己的身份验证服务,您基本上可以做同样的事情:创建一个具有“主体”属性的会话范围 bean,将其注入到您的身份验证服务中,让身份验证服务将属性设置为成功身份验证,然后根据需要使身份验证服务可用于其他 bean。
I wouldn't feel too bad about using SecurityContextHolder. though. I know that it's a static / Singleton and that Spring discourages using such things but their implementation takes care to behave appropriately depending on the environment: session-scoped in a Servlet container, thread-scoped in a JUnit test, etc. The real limiting factor of a Singleton is when it provides an implementation that is inflexible to different environments.
我不会对使用 SecurityContextHolder 感到太糟糕。尽管。我知道这是一个静态/单例,Spring 不鼓励使用这些东西,但它们的实现会根据环境注意适当的行为:Servlet 容器中的会话范围,JUnit 测试中的线程范围等。真正的限制因素单例的特点是它提供了一个对不同环境不灵活的实现。
回答by digitalsanctum
回答by Pavel
You are quite right to be concerned - static method calls are particularly problematic for unit testing as you cannot easily mock your dependencies. What I am going to show you is how to let the Spring IoC container do the dirty work for you, leaving you with neat, testable code. SecurityContextHolder is a framework class and while it may be ok for your low-level security code to be tied to it, you probably want to expose a neater interface to your UI components (i.e. controllers).
您担心是完全正确的 - 静态方法调用对于单元测试尤其成问题,因为您无法轻松模拟您的依赖项。我将向您展示的是如何让 Spring IoC 容器为您完成繁琐的工作,为您留下整洁、可测试的代码。SecurityContextHolder 是一个框架类,虽然您的低级安全代码可以绑定到它,但您可能希望向您的 UI 组件(即控制器)公开一个更简洁的界面。
cliff.meyers mentioned one way around it - create your own "principal" type and inject an instance into consumers. The Spring <aop:scoped-proxy/> tag introduced in 2.x combined with a request scope bean definition, and the factory-method support may be the ticket to the most readable code.
cliff.meyers 提到了一种解决方法 - 创建您自己的“主体”类型并将实例注入消费者。2.x 中引入的 Spring < aop:scoped-proxy/> 标签与请求作用域 bean 定义相结合,工厂方法支持可能是最易读代码的门票。
It could work like following:
它可以像下面这样工作:
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
}
Nothing complicated so far, right? In fact you probably had to do most of this already. Next, in your bean context define a request-scoped bean to hold the principal:
到目前为止没有什么复杂的,对吧?事实上,您可能已经完成了大部分工作。接下来,在您的 bean 上下文中定义一个请求范围的 bean 来保存主体:
<bean id="userDetails" class="MyUserHolder" factory-method="getUserDetails" scope="request">
<aop:scoped-proxy/>
</bean>
<bean id="controller" class="MyUserAwareController">
<property name="currentUser" ref="userDetails"/>
<!-- other props -->
</bean>
Thanks to the magic of the aop:scoped-proxy tag, the static method getUserDetails will be called every time a new HTTP request comes in and any references to the currentUser property will be resolved correctly. Now unit testing becomes trivial:
多亏了 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);
}
Hope this helps!
希望这可以帮助!
回答by Scott Bale
I asked the same question myself over here, and just posted an answer that I recently found. Short answer is: inject a SecurityContext
, and refer to SecurityContextHolder
only in your Spring config to obtain the SecurityContext
我自己在这里问了同样的问题,并刚刚发布了我最近找到的答案。简短的回答是:注入 a SecurityContext
,并SecurityContextHolder
仅在您的 Spring 配置中引用以获取SecurityContext
回答by Michael Bushe
Using a static in this case is the best way to write secure code.
在这种情况下使用静态是编写安全代码的最佳方式。
Yes, statics are generally bad - generally, but in this case, the static is what you want. Since the security context associates a Principal with the currently running thread, the most secure code would access the static from the thread as directly as possible. Hiding the access behind a wrapper class that is injected provides an attacker with more points to attack. They wouldn't need access to the code (which they would have a hard time changing if the jar was signed), they just need a way to override the configuration, which can be done at runtime or slipping some XML onto the classpath. Even using annotation injection would be overridable with external XML. Such XML could inject the running system with a rogue principal.
是的,静态通常是不好的——一般来说,但在这种情况下,静态就是你想要的。由于安全上下文将 Principal 与当前运行的线程相关联,因此最安全的代码将尽可能直接地从线程访问静态。将访问隐藏在注入的包装类后面,为攻击者提供了更多攻击点。他们不需要访问代码(如果 jar 被签名,他们将很难更改代码),他们只需要一种覆盖配置的方法,这可以在运行时完成或将一些 XML 滑到类路径上。即使使用注解注入也可以被外部 XML 覆盖。这样的 XML 可能会向正在运行的系统注入流氓主体。
回答by Michael Bushe
Personally I would just use Powermock along with Mockito or Easymock to mock the static SecurityContextHolder.getSecurityContext() in your unit/integration test e.g.
就我个人而言,我只会使用 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);
...
}
}
Admittedly there is quite a bit of boiler plate code here i.e. mock an Authentication object, mock a SecurityContext to return the Authentication and finally mock the SecurityContextHolder to get the SecurityContext, however its very flexible and allows you to unit test for scenarios like null Authentication objects etc. without having to change your (non test) code
不可否认,这里有相当多的样板代码,即模拟 Authentication 对象,模拟 SecurityContext 以返回 Authentication,最后模拟 SecurityContextHolder 以获取 SecurityContext,但是它非常灵活,允许您对空 Authentication 对象等场景进行单元测试等而无需更改您的(非测试)代码
回答by Leonardo Eloy
Just do it the usual way and then insert it using SecurityContextHolder.setContext()
in your test class, for example:
只需按照通常的方式进行,然后将其插入SecurityContextHolder.setContext()
到您的测试类中,例如:
Controller:
控制器:
Authentication a = SecurityContextHolder.getContext().getAuthentication();
Test:
测试:
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);
回答by Pavel Horal
Authentication is a property of a thread in server environment in the same way as it is a property of a process in OS. Having a bean instance for accessing authentication information would be inconvenient configuration and wiring overhead without any benefit.
身份验证是服务器环境中线程的属性,就像它是操作系统中进程的属性一样。拥有用于访问身份验证信息的 bean 实例将是不方便的配置和布线开销,没有任何好处。
Regarding test authentication there are several ways how you can make your life easier. My favourite is to make a custom annotation @Authenticated
and test execution listener, which manages it. Check DirtiesContextTestExecutionListener
for inspiration.
关于测试身份验证,有几种方法可以让您的生活更轻松。我最喜欢的是制作一个自定义注释@Authenticated
和测试执行侦听器,它管理它。检查DirtiesContextTestExecutionListener
灵感。
回答by borjab
After quite a lot of work I was able to reproduce the desired behavior. I had emulated the login through MockMvc. It is too heavy for most unit tests but helpful for integration tests.
经过大量工作,我能够重现所需的行为。我已经通过 MockMvc 模拟了登录。对于大多数单元测试来说太重了,但对集成测试很有帮助。
Of course I am willing to see those new features in Spring Security 4.0 that will make our testing easier.
当然,我愿意看到 Spring Security 4.0 中的那些新特性,它们将使我们的测试更容易。
package [myPackage]
import static org.junit.Assert.*;
import javax.inject.Inject;
import javax.servlet.http.HttpSession;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
@ContextConfiguration(locations={[my config file locations]})
@WebAppConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public static class getUserConfigurationTester{
private MockMvc mockMvc;
@Autowired
private FilterChainProxy springSecurityFilterChain;
@Autowired
private MockHttpServletRequest request;
@Autowired
private WebApplicationContext webappContext;
@Before
public void init() {
mockMvc = MockMvcBuilders.webAppContextSetup(webappContext)
.addFilters(springSecurityFilterChain)
.build();
}
@Test
public void testTwoReads() throws Exception{
HttpSession session = mockMvc.perform(post("/j_spring_security_check")
.param("j_username", "admin_001")
.param("j_password", "secret007"))
.andDo(print())
.andExpect(status().isMovedTemporarily())
.andExpect(redirectedUrl("/index"))
.andReturn()
.getRequest()
.getSession();
request.setSession(session);
SecurityContext securityContext = (SecurityContext) session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
SecurityContextHolder.setContext(securityContext);
// Your test goes here. User is logged with
}
回答by yankee
General
一般的
In the meantime (since version 3.2, in the year 2013, thanks to SEC-2298) the authentication can be injected into MVC methods using the annotation @AuthenticationPrincipal:
同时(从 3.2 版开始,在 2013 年,感谢SEC-2298)可以使用注解@AuthenticationPrincipal将身份验证注入到 MVC 方法中:
@Controller
class Controller {
@RequestMapping("/somewhere")
public void doStuff(@AuthenticationPrincipal UserDetails myUser) {
}
}
Tests
测试
In your unit test you can obviously call this Method directly. In integration tests using org.springframework.test.web.servlet.MockMvc
you can use org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user()
to inject the user like this:
在您的单元测试中,您显然可以直接调用此方法。在集成测试中,org.springframework.test.web.servlet.MockMvc
您可以org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user()
像这样注入用户:
mockMvc.perform(get("/somewhere").with(user(myUserDetails)));
This will however just directly fill the SecurityContext. If you want to make sure that the user is loaded from a session in your test, you can use this:
然而,这将直接填充 SecurityContext。如果要确保用户是从测试中的会话加载的,可以使用以下命令:
mockMvc.perform(get("/somewhere").with(sessionUser(myUserDetails)));
/* ... */
private static RequestPostProcessor sessionUser(final UserDetails userDetails) {
return new RequestPostProcessor() {
@Override
public MockHttpServletRequest postProcessRequest(final MockHttpServletRequest request) {
final SecurityContext securityContext = new SecurityContextImpl();
securityContext.setAuthentication(
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities())
);
request.getSession().setAttribute(
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, securityContext
);
return request;
}
};
}