Spring Test & Security:如何模拟身份验证?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/15203485/
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
Spring Test & Security: How to mock authentication?
提问by Martin Becker
I was trying to figure out how to unit test if my the URLs of my controllers are properly secured. Just in case someone changes things around and accidentally removes security settings.
我试图弄清楚如何对我的控制器的 URL 进行单元测试是否得到了适当的保护。以防万一有人改变了事情并意外删除了安全设置。
My controller method looks like this:
我的控制器方法如下所示:
@RequestMapping("/api/v1/resource/test")
@Secured("ROLE_USER")
public @ResonseBody String test() {
return "test";
}
I set up a WebTestEnvironment like so:
我像这样设置了一个 WebTestEnvironment:
import javax.annotation.Resource;
import javax.naming.NamingException;
import javax.sql.DataSource;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.ActiveProfiles;
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;
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({
"file:src/main/webapp/WEB-INF/spring/security.xml",
"file:src/main/webapp/WEB-INF/spring/applicationContext.xml",
"file:src/main/webapp/WEB-INF/spring/servlet-context.xml" })
public class WebappTestEnvironment2 {
@Resource
private FilterChainProxy springSecurityFilterChain;
@Autowired
@Qualifier("databaseUserService")
protected UserDetailsService userDetailsService;
@Autowired
private WebApplicationContext wac;
@Autowired
protected DataSource dataSource;
protected MockMvc mockMvc;
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
protected UsernamePasswordAuthenticationToken getPrincipal(String username) {
UserDetails user = this.userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
user,
user.getPassword(),
user.getAuthorities());
return authentication;
}
@Before
public void setupMockMvc() throws NamingException {
// setup mock MVC
this.mockMvc = MockMvcBuilders
.webAppContextSetup(this.wac)
.addFilters(this.springSecurityFilterChain)
.build();
}
}
In my actual test I tried to do something like this:
在我的实际测试中,我尝试做这样的事情:
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import eu.ubicon.webapp.test.WebappTestEnvironment;
public class CopyOfClaimTest extends WebappTestEnvironment {
@Test
public void signedIn() throws Exception {
UsernamePasswordAuthenticationToken principal =
this.getPrincipal("test1");
SecurityContextHolder.getContext().setAuthentication(principal);
super.mockMvc
.perform(
get("/api/v1/resource/test")
// .principal(principal)
.session(session))
.andExpect(status().isOk());
}
}
I picked this up here:
我在这里捡到了这个:
- http://java.dzone.com/articles/spring-test-mvc-junit-testinghere:
- http://techdive.in/solutions/how-mock-securitycontextholder-perfrom-junit-tests-spring-controlleror here:
- How to JUnit tests a @PreAuthorize annotation and its spring EL specified by a spring MVC Controller?
- http://java.dzone.com/articles/spring-test-mvc-junit-testing在这里:
- http://techdive.in/solutions/how-mock-securitycontextholder-perfrom-junit-tests-spring-controller或这里:
- 如何 JUnit 测试 @PreAuthorize 注释及其由 spring MVC 控制器指定的 spring EL?
Yet if one looks closely this only helps when not sending actual requests to URLs, but only when testing services on a function level. In my case an "access denied" exception was thrown:
然而,如果仔细观察,这只会在不向 URL 发送实际请求时有所帮助,而仅在功能级别上测试服务时才有帮助。在我的情况下,抛出了“访问被拒绝”异常:
org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:83) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:206) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:60) ~[spring-security-core-3.1.3.RELEASE.jar:3.1.3.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) ~[spring-aop-3.2.1.RELEASE.jar:3.2.1.RELEASE]
...
The following two log messages are noteworthy basically saying that no user was authenticated indicating that setting the Principaldid not work, or that it was overwritten.
以下两条日志消息值得注意,基本上是说没有用户通过身份验证,表明设置Principal不起作用,或者它被覆盖了。
14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Secure object: ReflectiveMethodInvocation: public java.util.List test.TestController.test(); target is of class [test.TestController]; Attributes: [ROLE_USER]
14:20:34.454 [main] DEBUG o.s.s.a.i.a.MethodSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken@9055e4a6: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@957e: RemoteIpAddress: 127.0.0.1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
采纳答案by Martin Becker
It turned out that the SecurityContextPersistenceFilter, which is part of the Spring Security filter chain, always resets my SecurityContext, which I set calling SecurityContextHolder.getContext().setAuthentication(principal)(or by using the .principal(principal)method). This filter sets the SecurityContextin the SecurityContextHolderwith a SecurityContextfrom a SecurityContextRepositoryOVERWRITINGthe one I set earlier. The repository is a HttpSessionSecurityContextRepositoryby default. The HttpSessionSecurityContextRepositoryinspects the given HttpRequestand tries to access the corresponding HttpSession. If it exists, it will try to read the SecurityContextfrom the HttpSession. If this fails, the repository generates an empty SecurityContext.
事实证明,作为SecurityContextPersistenceFilterSpring Security 过滤器链一部分的 总是重置 my SecurityContext,我设置了调用SecurityContextHolder.getContext().setAuthentication(principal)(或使用.principal(principal)方法)。该过滤器设置SecurityContext在SecurityContextHolder同一个SecurityContext从SecurityContextRepository覆盖一个我先前设置。HttpSessionSecurityContextRepository默认情况下,存储库是一个。在HttpSessionSecurityContextRepository检查给定的HttpRequest,并试图访问相应HttpSession。如果存在,它将尝试SecurityContext从HttpSession. 如果失败,存储库会生成一个空的SecurityContext.
Thus, my solution is to pass a HttpSessionalong with the request, which holds the SecurityContext:
因此,我的解决方案是将 aHttpSession与请求一起传递,其中包含SecurityContext:
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.Test;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import eu.ubicon.webapp.test.WebappTestEnvironment;
public class Test extends WebappTestEnvironment {
public static class MockSecurityContext implements SecurityContext {
private static final long serialVersionUID = -1386535243513362694L;
private Authentication authentication;
public MockSecurityContext(Authentication authentication) {
this.authentication = authentication;
}
@Override
public Authentication getAuthentication() {
return this.authentication;
}
@Override
public void setAuthentication(Authentication authentication) {
this.authentication = authentication;
}
}
@Test
public void signedIn() throws Exception {
UsernamePasswordAuthenticationToken principal =
this.getPrincipal("test1");
MockHttpSession session = new MockHttpSession();
session.setAttribute(
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
new MockSecurityContext(principal));
super.mockMvc
.perform(
get("/api/v1/resource/test")
.session(session))
.andExpect(status().isOk());
}
}
回答by EliuX
Seaching for answer I couldn't find any to be easy and flexible at the same time, then I found the Spring Security Referenceand I realized there are near to perfect solutions. AOP solutions often are the greatest ones for testing, and Spring provides it with @WithMockUser, @WithUserDetailsand @WithSecurityContext, in this artifact:
寻找答案我找不到任何既简单又灵活的答案,然后我找到了Spring Security Reference,我意识到有接近完美的解决方案。AOP 解决方案通常是最适合测试的解决方案,Spring 为它提供了@WithMockUser,@WithUserDetails和@WithSecurityContext, 在这个工件中:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>4.2.2.RELEASE</version>
<scope>test</scope>
</dependency>
In most cases, @WithUserDetailsgathers the flexibility and power I need.
在大多数情况下,@WithUserDetails集合了我需要的灵活性和功能。
How @WithUserDetails works?
@WithUserDetails 如何工作?
Basically you just need to create a custom UserDetailsServicewith all the possible users profiles you want to test. E.g
基本上,您只需要UserDetailsService使用您想要测试的所有可能的用户配置文件创建一个自定义。例如
@TestConfiguration
public class SpringSecurityWebAuxTestConfig {
@Bean
@Primary
public UserDetailsService userDetailsService() {
User basicUser = new UserImpl("Basic User", "[email protected]", "password");
UserActive basicActiveUser = new UserActive(basicUser, Arrays.asList(
new SimpleGrantedAuthority("ROLE_USER"),
new SimpleGrantedAuthority("PERM_FOO_READ")
));
User managerUser = new UserImpl("Manager User", "[email protected]", "password");
UserActive managerActiveUser = new UserActive(managerUser, Arrays.asList(
new SimpleGrantedAuthority("ROLE_MANAGER"),
new SimpleGrantedAuthority("PERM_FOO_READ"),
new SimpleGrantedAuthority("PERM_FOO_WRITE"),
new SimpleGrantedAuthority("PERM_FOO_MANAGE")
));
return new InMemoryUserDetailsManager(Arrays.asList(
basicActiveUser, managerActiveUser
));
}
}
Now we have our users ready, so imagine we want to test the access control to this controller function:
现在我们已经准备好了我们的用户,所以假设我们要测试对这个控制器函数的访问控制:
@RestController
@RequestMapping("/foo")
public class FooController {
@Secured("ROLE_MANAGER")
@GetMapping("/salute")
public String saluteYourManager(@AuthenticationPrincipal User activeUser)
{
return String.format("Hi %s. Foo salutes you!", activeUser.getUsername());
}
}
Here we have a get mapped functionto the route /foo/saluteand we are testing a role based security with the @Securedannotation, although you can test @PreAuthorizeand @PostAuthorizeas well.
Let's create two tests, one to check if a valid user can see this salute response and the other to check if it's actually forbidden.
在这里,我们有一个被映射功能的路线/富/敬礼,我们正在测试与基于角色的安全性@Secured注解,但你可以测试@PreAuthorize和@PostAuthorize也。让我们创建两个测试,一个是检查有效用户是否可以看到此致敬响应,另一个是检查它是否实际上被禁止。
@RunWith(SpringRunner.class)
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = SpringSecurityWebAuxTestConfig.class
)
@AutoConfigureMockMvc
public class WebApplicationSecurityTest {
@Autowired
private MockMvc mockMvc;
@Test
@WithUserDetails("[email protected]")
public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
{
mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
.accept(MediaType.ALL))
.andExpect(status().isOk())
.andExpect(content().string(containsString("[email protected]")));
}
@Test
@WithUserDetails("[email protected]")
public void givenBasicUser_whenGetFooSalute_thenForbidden() throws Exception
{
mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
.accept(MediaType.ALL))
.andExpect(status().isForbidden());
}
}
As you see we imported SpringSecurityWebAuxTestConfigto provide our users for testing. Each one used on its corresponding test case just by using a straightforward annotation, reducing code and complexity.
如您所见,我们导入SpringSecurityWebAuxTestConfig以提供给我们的用户进行测试。每一个都用于其相应的测试用例,只需使用一个简单的注释,减少代码和复杂性。
Better use @WithMockUser for simpler Role Based Security
更好地使用 @WithMockUser 以获得更简单的基于角色的安全性
As you see @WithUserDetailshas all the flexibility you need for most of your applications. It allows you to use custom users with any GrantedAuthority, like roles or permissions. But if you are just working with roles, testing can be even easier and you could avoid constructing a custom UserDetailsService. In such cases, specify a simple combination of user, password and roles with @WithMockUser.
如您所见,@WithUserDetails拥有大多数应用程序所需的所有灵活性。它允许您使用具有任何 GrantedAuthority 的自定义用户,例如角色或权限。但是,如果您只是使用角色,测试会更容易,并且您可以避免构建自定义UserDetailsService. 在这种情况下,请使用@WithMockUser指定用户、密码和角色的简单组合。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@WithSecurityContext(
factory = WithMockUserSecurityContextFactory.class
)
public @interface WithMockUser {
String value() default "user";
String username() default "";
String[] roles() default {"USER"};
String password() default "password";
}
The annotation defines default values for a very basic user. As in our case the route we are testing just requires that the authenticated user be a manager, we can quit using SpringSecurityWebAuxTestConfigand do this.
注释为非常基本的用户定义了默认值。在我们的例子中,我们正在测试的路由只需要经过身份验证的用户是管理员,我们可以退出使用SpringSecurityWebAuxTestConfig并执行此操作。
@Test
@WithMockUser(roles = "MANAGER")
public void givenManagerUser_whenGetFooSalute_thenOk() throws Exception
{
mockMvc.perform(MockMvcRequestBuilders.get("/foo/salute")
.accept(MediaType.ALL))
.andExpect(status().isOk())
.andExpect(content().string(containsString("user")));
}
Notice that now instead of the user [email protected]we are getting the default provided by @WithMockUser: user; yet it won't matter because what we really care about is his role: ROLE_MANAGER.
请注意,现在我们得到的不是用户[email protected],而是由@WithMockUser以下用户提供的默认值:user; 不过没关系,因为我们真正关心的是他的角色:ROLE_MANAGER。
Conclusions
结论
As you see with annotations like @WithUserDetailsand @WithMockUserwe can switch between different authenticated users scenarioswithout building classes alienated from our architecture just for making simple tests. Its also recommended you to see how @WithSecurityContextworks for even more flexibility.
正如你看到的注释@WithUserDetails,@WithMockUser我们可以在不同的经过身份验证的用户场景之间切换,而无需为了进行简单的测试而构建与我们的架构疏远的类。它还建议您查看@WithSecurityContext如何工作以获得更大的灵活性。
回答by GummyBear21
Since Spring 4.0+, the best solution is to annotate the test method with @WithMockUser
从 Spring 4.0+ 开始,最好的解决方案是使用 @WithMockUser 注释测试方法
@Test
@WithMockUser(username = "user1", password = "pwd", roles = "USER")
public void mytest1() throws Exception {
mockMvc.perform(get("/someApi"))
.andExpect(status().isOk());
}
Remember to add the following dependency to your project
记得在你的项目中添加以下依赖
'org.springframework.security:spring-security-test:4.2.3.RELEASE'
回答by Grigory Kislin
Add in pom.xml:
在 pom.xml 中添加:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>4.0.0.RC2</version>
</dependency>
and use org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessorsfor authorization request.
See the sample usage at https://github.com/rwinch/spring-security-test-blog(https://jira.spring.io/browse/SEC-2592).
并org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors用于授权请求。请参阅https://github.com/rwinch/spring-security-test-blog( https://jira.spring.io/browse/SEC-2592) 上的示例用法。
Update:
更新:
4.0.0.RC2 works for spring-security 3.x. For spring-security 4 spring-security-test become part of spring-security (http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test, version is the same).
4.0.0.RC2 适用于 spring-security 3.x。对于 spring-security 4 spring-security-test 成为 spring-security 的一部分(http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test,版本是一样的)。
Setting Up is changed: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-mockmvc
设置已更改:http: //docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-mockmvc
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
Sample for basic-authentication: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication.
基本身份验证示例:http: //docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#testing-http-basic-authentication。
回答by Jay
Here is an example for those who want to Test Spring MockMvc Security Config using Base64 basic authentication.
这是一个示例,适用于想要使用 Base64 基本身份验证测试 Spring MockMvc 安全配置的人。
String basicDigestHeaderValue = "Basic " + new String(Base64.encodeBase64(("<username>:<password>").getBytes()));
this.mockMvc.perform(get("</get/url>").header("Authorization", basicDigestHeaderValue).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());
Maven Dependency
Maven 依赖
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.3</version>
</dependency>
回答by Nagy Attila
Short answer:
简短的回答:
@Autowired
private WebApplicationContext webApplicationContext;
@Autowired
private Filter springSecurityFilterChain;
@Before
public void setUp() throws Exception {
final MockHttpServletRequestBuilder defaultRequestBuilder = get("/dummy-path");
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext)
.defaultRequest(defaultRequestBuilder)
.alwaysDo(result -> setSessionBackOnRequestBuilder(defaultRequestBuilder, result.getRequest()))
.apply(springSecurity(springSecurityFilterChain))
.build();
}
private MockHttpServletRequest setSessionBackOnRequestBuilder(final MockHttpServletRequestBuilder requestBuilder,
final MockHttpServletRequest request) {
requestBuilder.session((MockHttpSession) request.getSession());
return request;
}
After perform formLoginfrom spring security test each of your requests will be automatically called as logged in user.
formLogin从 spring 安全测试执行后,您的每个请求都将自动以登录用户的身份调用。
Long answer:
长答案:
Check this solution (the answer is for spring 4): How to login a user with spring 3.2 new mvc testing
检查此解决方案(答案适用于 spring 4):How to login a user with spring 3.2 new mvc testing
回答by Pavla Nováková
Options to avoid using SecurityContextHolder in tests:
避免在测试中使用 SecurityContextHolder 的选项:
- Option 1: use mocks - I mean mock
SecurityContextHolderusing some mock library - EasyMockfor example - Option 2: wrap call
SecurityContextHolder.get...in your code in some service - for example inSecurityServiceImplwith methodgetCurrentPrincipalthat implementsSecurityServiceinterface and then in your tests you can simply create mock implementation of this interface that returns the desired principal without access toSecurityContextHolder.
- 选项 1:使用模拟 - 我的意思是
SecurityContextHolder使用一些模拟库模拟 -例如EasyMock - 选项 2:将调用包装
SecurityContextHolder.get...在某些服务中的代码中 - 例如在实现接口的SecurityServiceImpl方法中getCurrentPrincipal,SecurityService然后在您的测试中,您可以简单地创建此接口的模拟实现,该实现返回所需的主体而无需访问SecurityContextHolder.

