问题
I am using Spring Boot 1.3, Spring 4.2 and Spring Security 4.0. I am running integration tests using MockMvc, for example:
mockMvc = webAppContextSetup(webApplicationContext).build();
MvcResult result = mockMvc.perform(get("/"))
.andExpect(status().isOk())
.etc;
In my tests I am simulating a user login like this:
CurrentUser principal = new CurrentUser(user);
Authentication auth =
new UsernamePasswordAuthenticationToken(principal, "dummypassword",
principal.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
This works fine for my methods that are annotated with @PreAuthorize
, for example when calling a method like this from a test:
@PreAuthorize("@permissionsService.canDoThisThing(principal)")
public void mySecuredMethod()
the principle, my CurrentUser object, is non-null in PermissionsService#canDoThisThing
.
I have a class annotated with @ControllerAdvice that adds the currently logged-in user to the model so it can be accessed in every view:
@ControllerAdvice
public class CurrentUserControllerAdvice {
@ModelAttribute("currentUser")
public CurrentUser getCurrentUser(Authentication authentication) {
if (authentication == null) {
return null;
}
return (CurrentUser) authentication.getPrincipal();
}
}
This works fine when running the application, however (and this is my problem) - when running my tests the authentication parameter passed in to the getCurrentUser method above is always null. This means any references to the currentUser attribute in my view templates cause errors, so those tests fail.
I know I could get round this by retrieving the principle like this:
authentication = SecurityContextHolder.getContext().getAuthentication();
but I would rather not change my main code just so the tests work.
回答1:
Setting the SecurityContextHolder
does not work when using Spring Security with MockMvc
. The reason is that Spring Security's SecurityContextPersistenceFilter
attempts to resolve the SecurityContext
from the HttpServletRequest
. By default this is done using HttpSessionSecurityContextRepository
by retrieving at the HttpSession
attribute named SPRING_SECURITY_CONTEXT
. The attribute name is defined by the constant HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY
. Whatever SecurityContext
is found in the HttpSession
will then be set on the SecurityContextHolder
which overrides the value you previously set.
Manually Solving the Issue
The fix that involves the least amount of change is to set the SecurityContext
in the HttpSession
. You can do this using something like this:
MvcResult result = mockMvc.perform(get("/").sessionAttr(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, securityContext))
.andExpect(status().isOk())
.etc;
The key is to ensure that you set the HttpSession
attribute named SPRING_SECURITY_CONTEXT
to the SecurityContext
. In our example, we leverage the constant HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY
to define the attribute name.
Spring Security Test
Spring Security 4.0 has officially added test support. This is by far the easiest and most flexible way to test your application with Spring Security.
Add spring-security-test
Since you are using Spring Boot, the easiest way to ensure you have this dependency is to include spring-security-test in your Maven pom. Spring Boot manages the version, so there is no need to specify a version.
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
Naturally, Gradle inclusion would be very similar.
Setting up MockMvc
In order to integrate with MockMvc there are some steps you must perform outlined in the reference.
The first step is to ensure you use @RunWith(SpringJUnit4ClassRunner.class)
. This should come as no surprise since this is a standard step when testing with Spring applications.
The next step is to ensure you build your MockMvc
instance using SecurityMockMvcConfigurers.springSecurity()
. For example:
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class MyTests {
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@Before
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity()) // sets up Spring Security with MockMvc
.build();
}
...
Running as a User
Now you can easily run with a specific user. There are two ways of accomplishing this with MockMvc.
Using a RequestPostProcessor
The first option is using a RequestPostProcessor. For your example, you could do something like this:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
// use this if CustomUser (principal) implements UserDetails
mvc
.perform(get("/").with(user(principal)))
...
// otherwise use this
mvc
.perform(get("/").with(authentication(auth)))
...
Using Annotations
You can also use annotations to specify the user. Since you use a custom user object (i.e. CurrentUser), you would probably consider using @WithUserDetails or @WithSecurityContext.
@WithUserDetails makes sense if you expose the UserDetailsService
as a bean and you are alright with the user being looked up (i.e. it must exist). An example of @WithUserDetails
might look like:
@Test
@WithUserDetails("usernameThatIsFoundByUserDetailsService")
public void run() throws Exception {
MvcResult result = mockMvc.perform(get("/"))
.andExpect(status().isOk())
.etc;
}
The alternative is to use @WithSecurityContext. This makes sense if you do not want to require the user to actually exist (as is necessary for WithUserDetails). I won't elaborate on this as it is well documented and without more details about your object model, I cannot provide a concrete example of this.
来源:https://stackoverflow.com/questions/34224706/authentication-token-passed-to-controlleradvice-is-null-when-running-through-moc