@RestController public class Controller { @GetMapping public String get() { return String.valueOf(System.currentTimeMillis()); } }
testCompile('io.rest-assured:rest-assured:3.0.2')
apply plugin: 'groovy'
ControllerIT.groovy
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestPropertySource(properties = "security.user.password=pass") class ControllerIT { @LocalServerPort private int serverPort; @Before void initRestAssured() { RestAssured.port = serverPort; RestAssured.filters(new ResponseLoggingFilter()); RestAssured.filters(new RequestLoggingFilter()); } @Test void 'api call without authentication must fail'() { when() .get("/") .then() .statusCode(HttpStatus.SC_UNAUTHORIZED); } }
Request method: GET Request URI: http://localhost:51213/
HTTP/1.1 401 X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Frame-Options: DENY Strict-Transport-Security: max-age=31536000 ; includeSubDomains WWW-Authenticate: Basic realm="Spring" Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Sun, 22 Oct 2017 11:53:00 GMT { "timestamp": 1508673180745, "status": 401, "error": "Unauthorized", "message": "Full authentication is required to access this resource", "path": "/" }
SpringBootWebSecurityConfiguration
supplied by spring boot. Inside this class is the ApplicationNoWebSecurityConfigurerAdapter
which sets defaults.SecurityProperties
@TestPropertySource(properties = [ "security.user.password=pass", "security.enable-csrf=true", "security.sessions=if_required" ])
@Test void 'api call with authentication must succeed'() { given() .auth().preemptive().basic("user", "pass") .when() .get("/") .then() .statusCode(HttpStatus.SC_OK); }
(new Exception().getStackTrace().length == 91)
and find the first mention of the springfilterChain
springSecurityFilterChain
filter is interesting here springSecurityFilterChain
it is he who does all the SS work in the web part.DelegatingFilterProxyRegistrationBean
itself is not very interesting, let's see to whom it delegates its work.FilterChainProxy
class. Inside it happens a few interesting things.FilterChainProxy#doFilterInternal
. What's going on here? We VirtualFilterChain
filters, create VirtualFilterChain
and run the request and response on them. List<Filter> filters = getFilters(fwRequest); ... VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters); vfc.doFilter(fwRequest, fwResponse);
SecurityFilterChain
that matches the request. private List<Filter> getFilters(HttpServletRequest request) { for (SecurityFilterChain chain : filterChains) { if (chain.matches(request)) { return chain.getFilters(); } } return null; }
OrRequestMatcher
, which will try to match the current url with at least one pattern from the list.Any url that matches this pattern will not be protected by SS by default.Add a method:"/css/**", "/js/**", "/images/**", "/webjars/**", "/**/favicon.ico", "/error"
@GetMapping("css/hello") public String cssHello() { return "Hello I'm secret data"; }
@Test void 'get css/hello must succeed'() { when() .get("css/hello") .then() .statusCode(HttpStatus.SC_OK); }
0 = {WebAsyncManagerIntegrationFilter} 1 = {SecurityContextPersistenceFilter} 2 = {HeaderWriterFilter} 3 = {CsrfFilter} 4 = {LogoutFilter} 5 = {BasicAuthenticationFilter} 6 = {RequestCacheAwareFilter} 7 = {SecurityContextHolderAwareRequestFilter} 8 = {AnonymousAuthenticationFilter} 9 = {SessionManagementFilter} 10 = {ExceptionTranslationFilter} 11 = {FilterSecurityInterceptor}
http .authorizeRequests().anyRequest().authenticated() .and() .formLogin() .and() .httpBasic();
UsernamePasswordAuthenticationFilter DefaultLoginPageGeneratingFilter
@PostMapping("post") public String testPost() { return "Hello it is post request"; }
@Test void 'POST without CSRF token must return 403'() { given() .auth().preemptive().basic("user", "pass") .when() .post("/post") .then() .statusCode(HttpStatus.SC_FORBIDDEN); }
Ant [pattern='/logout', POST] -
handler = {CompositeLogoutHandler} logoutHandlers = {ArrayList} size = 2 0 = {CsrfLogoutHandler} 1 = {SecurityContextLogoutHandler}
AuthenticationManager
if (headers.get("Authorization").startsWith("Basic")) { try { UsernamePasswordAuthenticationToken token = extract(header); Authentication authResult = authenticationManager.authenticate(token); } catch (AuthenticationException failed) { SecurityContextHolder.clearContext(); this.authenticationEntryPoint.commence(request, response, failed); return; } } else { chain.doFilter(request, response); }
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
public interface AuthenticationProvider { Authentication authenticate(Authentication authentication) throws AuthenticationException; boolean supports(Class<?> authentication); }
Authentication
object to the ProviderManager
, it iterates through the existing AuthenticationProvider
and checks whether public boolean supports(Class<?> authentication) { return (UsernamePasswordAuthenticationToken.class .isAssignableFrom(authentication)); }
AuthenticationProvider.authenticate
we can already transfer the passed Authentication to the desired implementation without caste exection.AuthenticationProvider
should throw an exception, the ProviderManager
will catch it and try the next AuthenticationProvider from the list, if none of the AuthenticationProvider returns successful authentication, the ProviderManager will forward the last caught event.BasicAuthenticationFilter
saves the resulting Authentication to the SecurityContextHolderSecurityContextHolder.clearContext();
will be reset SecurityContextHolder.clearContext();
context and will be called AuthenticationEntryPoint. public interface AuthenticationEntryPoint { void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException; }
response.addHeader("WWW-Authenticate", "Basic realm=\"" + realmName + "\""); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
@GetMapping("customHeader") public String customHeader(@RequestHeader("x-custom-header") String customHeader) { return customHeader; }
@Test void 'passed x-custom-header must be returned'() { def sessionCookie = given() .header("x-custom-header", "hello") .when() .get("customHeader") .then() .statusCode(HttpStatus.SC_UNAUTHORIZED) .extract().cookie("JSESSIONID") given() .auth().basic("user", "pass") .cookie("JSESSIONID", sessionCookie) .when() .get("customHeader") .then() .statusCode(HttpStatus.SC_OK) .body(equalTo("hello")); }
chain.doFilter(this.requestFactory.create((HttpServletRequest) req, (HttpServletResponse) res), res);
SecurityContextRepository
with the default implementation of the HttpSessionSecurityContextRepository saves the SecurityContext to the session.sessionAuthenticationStrategy.onAuthentication
sessionAuthenticationStrategy = {CompositeSessionAuthenticationStrategy} delegateStrategies 0 = {ChangeSessionIdAuthenticationStrategy} 1 = {CsrfAuthenticationStrategy}
@Test void 'JSESSIONID must be changed after login'() { def sessionCookie = when() .get("/") .then() .statusCode(HttpStatus.SC_UNAUTHORIZED) .extract().cookie("JSESSIONID") def newCookie = given() .auth().basic("user", "pass") .cookie("JSESSIONID", sessionCookie) .when() .get("/") .then() .statusCode(HttpStatus.SC_OK) .extract().cookie("JSESSIONID") Assert.assertNotEquals(sessionCookie, newCookie) }
sendStartAuthentication
, inside of which the following occurs:SecurityContextHolder.getContext().setAuthentication(null);
- clears SecurityContextHolderrequestCache.saveRequest(request, response);
- saves the current request to requestCache so that RequestCacheAwareFilter has something to recover.authenticationEntryPoint.commence(request, response, reason);
- calls authenticationEntryPoint - which records in response a signal that it is necessary to perform authentication (headers \ redirect) if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) { ... } else { ... }
for (AccessDecisionVoter voter : getDecisionVoters()) { int result = voter.vote(authentication, object, configAttributes); switch (result) { case AccessDecisionVoter.ACCESS_GRANTED: return; case AccessDecisionVoter.ACCESS_DENIED: deny++; break; default: break; } } if (deny > 0) { throw new AccessDeniedException(); } checkAllowIfAllAbstainDecisions();
springSecurityFilterChain
is a set of spring security filters.WebAsyncManagerIntegrationFilter
- Integrates SecurityContext with WebAsyncManagerSecurityContextPersistenceFilter
- Looks for the SecurityContext in a session and fills in the SecurityContextHolder if it findsHeaderWriterFilter
- Adds “security” headers to the responseCsrfFilter
- Checks for the presence of csrf tokenLogoutFilter
- Performs logoutBasicAuthenticationFilter
- Performs basic authentication.RequestCacheAwareFilter
- Restores the request saved prior to authentication, if anySecurityContextHolderAwareRequestFilter
- Wraps an existing request in SecurityContextHolderAwareRequestWrapperAnonymousAuthenticationFilter
- Fills SecurityContext with Anonymous AuthenticationSessionManagementFilter
- Performs session related actionsExceptionTranslationFilter
- Handles AuthenticationException \ AccessDeniedException that occur below the stack.FilterSecurityInterceptor
- Checks if the current user has access to the current url.FilterComparator
- here you can see the list of filters and their possible order.AuthenticationManager
- interface responsible for authenticationProviderManager
- An implementation of an AuthenticationManager that uses an internally uses AuthenticationProvider
AuthenticationProvider
is an interface responsible for authenticating a specific Authentication
implementation.SecurityContextHolder
- stores authentication usually in a ThreadLocal variable.AuthenticationEntryPoint
- modifies the response to make it clear to the client that authentication is required (headers, redirect to login page, etc.)AccessDecisionManager
decides whether Authentication
access to some resource.AffirmativeBased
is the default strategy used by AccessDecisionManager.FilterChainIT.groovy
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class FilterChainIT { @Autowired FilterChainProxy filterChainProxy; @Autowired List<Filter> filters; @Test void 'test main filter chain'() { assertEquals(5, filters.size()); assertEquals(OrderedCharacterEncodingFilter, filters[0].getClass()) assertEquals(OrderedHiddenHttpMethodFilter, filters[1].getClass()) assertEquals(OrderedHttpPutFormContentFilter, filters[2].getClass()) assertEquals(OrderedRequestContextFilter, filters[3].getClass()) assertEquals("springSecurityFilterChain", filters[4].filterName) } @Test void 'test security filter chain order'() { assertEquals(2, filterChainProxy.getFilterChains().size()); def chain = filterChainProxy.getFilterChains().get(1); assertEquals(chain.filters.size(), 11) assertEquals(WebAsyncManagerIntegrationFilter, chain.filters[0].getClass()) assertEquals(SecurityContextPersistenceFilter, chain.filters[1].getClass()) } @Test void 'test ignored patterns'() { def chain = filterChainProxy.getFilterChains().get(0); assertEquals("/css/**", chain.requestMatcher.requestMatchers[0].pattern); assertEquals("/js/**", chain.requestMatcher.requestMatchers[1].pattern); assertEquals("/images/**", chain.requestMatcher.requestMatchers[2].pattern); } }
interface Auth { ... } public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.getParameterType().equals(Auth.class); } @Override public Auth resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); return toAuth(principal) } } @GetMapping public String get(Auth auth) { return "hello " + auth.getId(); }
AbstractAuthenticationToken
, and an authentication filter is reasonable to inherit from AbstractAuthenticationProcessingFilter
HttpBasicConfigurer, OpenIDLoginConfigurer
they do the same. class MyConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Override public void configure(HttpSecurity http) throws Exception { AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class); MyAuthenticationProvider myAuthenticationProvider = http.getSharedObject(MyAuthenticationProvider.class); MyAuthenticationFilter filter = new MyAuthenticationFilter(authenticationManager); http .authenticationProvider(myAuthenticationProvider) .addFilterBefore(postProcess(filter), AbstractPreAuthenticatedProcessingFilter.class); } } public class SecurityConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests().anyRequest().authenticated() .and() .apply(new MyConfigurer()) } }
"url/methodOne", "url/methodTwo"
, authorizeRequests().antMatchers(HttpMethod.GET, "url/**").permitAll().
authorizeRequests().antMatchers(HttpMethod.GET, "url/methodOne", "url/methodTwo").permitAll().
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests(). anyRequest() .authenticated() .antMatchers("permit_all_url") .permitAll(); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("ignored_url"); }
Source: https://habr.com/ru/post/346628/
All Articles