
@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 spring
filterChain
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 AuthenticationProviderAuthenticationProvider 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 AbstractAuthenticationProcessingFilterHttpBasicConfigurer, 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