Spring Security Inside Request Request Path
- From the sandbox
- Tutorial
Most developers have only an approximate idea of what is happening inside Spring Security, which is dangerous and can lead to vulnerabilities.
In this article, we will go step by step along the path of the http request, which will help with understanding to configure and solve Spring Security problems.
First, prepare the project, go to https://start.spring.io/ , check the boxes opposite Web> web, and Core> Security.
Add a controller:
Add rest-assured:
Add grooves:
Let's write a test:
Run the test. What's in the logs?
Inquiry:
Answer:
SS, without any additional settings, has already begun to protect method calls, since the configuration -
Some of them can be influenced through the settings:
docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
look for "# SECURITY PROPERTIES", you can also look at the code:
Donot configure spring boot configuration for completeness:
Spring Security in a web application starts with a servlet filter.
We’ll try to make a deal, but first we’ll add a test with successful authorization.
Put the break and run the test.
fig. 1 - get method
go down the huge stack of calls
pic. 2 - call stack
Let's see what lies in the variable
fig. 3 - application filter chain
An interesting filter here is
Itself is
fig. 4 - filter chain proxy
He delegates his work to the class
First of all, let's look at the method
Inside the getFilters method, take the first one
Let's go to the debugger and see which list iterates.
fig. 5 - security filter chains
What does this list tell us?
In both sheets lies
The first element of the list has an empty filter list, so there will be no additional filtering, and as a result there will be no protection.
Check in practice.
Let's write a test:
Much more interesting is the second SecurityFilterChain which will match any url "/ **"
In our case, there is the following list of filters.
This list may vary depending on the settings and dependencies added.
For example, with this configuration:
Filters would be added to this list:
In what order the filters go by default can be seen here: FilterComparator
We are not very interested, according to the documentation, it “integrates” SecurityContext with WebAsyncManager which is responsible for asynchronous requests.
Searches for the SecurityContext in the session and populates the SecurityContextHolder if it finds.
By default, ThreadLocalSecurityContextHolderStrategy is used which stores the SecurityContext in the ThreadLocal variable.
Just adds headers to response.
Disable cache:
- Cache-Control: no-cache, no-store, max-age = 0, must-revalidate
- Pragma: no-cache
- Expires: 0
We do not allow browsers to automatically determine the type of content:
- X-Content-Type- Options: nosnif
Do not allow iframe
- X-Frame-Options: DENY
Enable built-in protection in the browser from cross-site scripting (XSS)
- X-XSS-Protection: 1; mode = block
Perhaps there is not a single developer who, when getting acquainted with SS, would not encounter the error "lack of csrf token."
Why haven't we seen this error before? It's simple, we ran methods on which there is no csrf protection.
Let's try to add a POST method
Test:
The test was successful, we returned a 403 error, csrf protection in place.
Next comes the logout filter, it checks if the url matches the pattern
and starts the logout procedure
by default, the following occurs:
Now we get directly to authentication. What is going on inside?
The filter checks to see if there is an Authorization header with a value starting in Basic.
If it finds it, it extracts the username \ password and passes them to
Inside something like this:
AuthenticationManager is an interface that accepts Authentication and returns Authentication too.
In our case, the implementation of Authentication is UsernamePasswordAuthenticationToken.
It would be possible to implement AuthenticationManager itself, but it makes little sense, there is a default implementation - ProviderManager.
The ProviderManager delegates authorization to another interface:
When we pass an object
AuthenticationProvider supports this Authentication implementation
As a result, inside
Next, from the concrete implementation we get the credit cards.
If authentication fails, it
The process is described in more detail and with pictures here:
https://spring.io/guides/topicals/spring-security-architecture/
Next, it
SecurityContextHolder.getContext (). SetAuthentication (authResult);
The authentication process is now complete.
If an AuthenticationException is thrown, the
The task of AuthenticationEntryPoint is to record in response information that authentication failed.
In the case of basic authentication, this will be:
As a result, the browser displays the basic authorization window.
What is this filter for? Imagine a scenario:
1. A user logs on to a secure url.
2. He throws it to the login page.
3. After successful authorization, the user is redirected to the page that he requested at the beginning.
It is for the restoration of the original request that this filter exists.
Inside, it is checked if there is a saved request, if it is, the current request is substituted for it.
The request is saved in the session, at what stage it is saved will be written below.
Let's try to reproduce.
Add a method:
Add a test:
As we see in the second request, the header that we passed in the first request returned to us. The filter is working.
Wraps an existing request in SecurityContextHolderAwareRequestWrapper
Implementation may vary depending on servlet api version of servlet 2.5 / 3
If by the time this filter is executed, the SecurityContextHolder is empty, i.e. authentication failed; the filter fills the SecurityContextHolder object with anonymous authentication - AnonymousAuthenticationToken with the role "ROLE_ANONYMOUS".
This ensures that there will be an object in the SecurityContextHolder, this allows you to not be afraid of NP, as well as a more flexible approach to setting access for unauthorized users.
At this stage, actions associated with the session are performed.
This can be:
- changing the session identifier
- limiting the number of simultaneous sessions
- saving the SecurityContext in securityContextRepository
In our case, the following happens:
Called
Inside sessionAuthenticationStrategy lies:
2 things happen:
1. By default, protection from session fixation attack is enabled, that is, after authentication, the session id changes.
2. If a csrf token was transferred, a new csrf token is generated.
Let's try to check the first item:
К этому моменту SecurityContext должен содеражть анонимную, либо нормальную аутентификацию.
ExceptionTranslationFilter прокидывает запрос и ответ по filter chain и обрабатывает возможные ошибки авторизации.
SS различает 2 случая:
1. AuthenticationException
Вызывается
2. AccessDeniedException
Тут опять возможны 2 случая:
1. A user with anonymous authentication, or with authentication using the rememberMe token
is called sendStartAuthentication
2. A user with full, non-anonymous authentication is called:
accessDeniedHandler.handle (request, response, (AccessDeniedException) exception)
which defaults the forbidden 403 response
At the last stage, authorization based on the url of the request occurs.
FilterSecurityInterceptor inherits from AbstractSecurityInterceptor and decides whether the current user has access to the current url.
There is another implementation of MethodSecurityInterceptor which is responsible for the admission to the method call when using @Secured \ @PreAuthorize annotations.
AccessDecisionManager
is called internally. There are several strategies for deciding whether to allow or not, the default is: AffirmativeBased
code inside is very simple:
In other words, if someone votes in favor, skip it, if at least 1 vote against we do not let it go, if no one votes we do not let it go.
To summarize:
An example of a set of filters for basic authorization:
this will avoid unpleasant surprises.
Authentication is not a very usable object per se. Almost all methods return Object, and to get the necessary information you need to cast it to a specific implementation.
Better get an interface, make an implementation depending on your needs, write HandlerMethodArgumentResolver.
Code with this approach is better to read, test, maintain.
Spring security contains many interfaces that you can implement, but most likely there is an abstract class that does 99% what you need.
For example, for the Authentication interface, exists
If you have fully custom authentication, most likely you had to do the following:
1. Create an Authentication implementation
2. Create an AuthenticationProvider that supports your Authentication implementation
3. Add a filter that started the authentication process.
It makes sense to combine them all in one place. Look at
Write a test that will pass through all the methods of the controller and check for the presence of @Secured \ @PreAuthorize annotations.
When configuring WebSecurityConfigurerAdapter, require authorization for all urls. Add exceptions if necessary. Exceptions should be as strict as possible.
Explicitly specify the type of the http method, and the url should be as complete as possible.
It is better to explicitly indicate the full path to the method, even if at the time of writing other api with such an endpoint was not there.
For example if there is a controller with two methods GET:
It is not necessary to do so:
Better write:
Spring Security has quite detailed debug logs, often they are enough to understand the essence of the problem.
In the case of “ignored_url” it will be checked at the stage of selecting the security filter chain, and if the url matches, an empty filter will be used.
In the case of permit_all_url, verification will take place at the AccessDecisionManager stage.
In this article, we will go step by step along the path of the http request, which will help with understanding to configure and solve Spring Security problems.
Project preparation
First, prepare the project, go to https://start.spring.io/ , check the boxes opposite Web> web, and Core> Security.
Add a controller:
@RestController
public class Controller {
@GetMapping
public String get() {
return String.valueOf(System.currentTimeMillis());
}
}
Add rest-assured:
testCompile('io.rest-assured:rest-assured:3.0.2')
Add grooves:
apply plugin: 'groovy'
Let's write a test:
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);
}
}
Run the test. What's in the logs?
Inquiry:
Request method: GET
Request URI: http://localhost:51213/
Answer:
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": "/"
}
SS, without any additional settings, has already begun to protect method calls, since the configuration -
SpringBootWebSecurityConfiguration
supplied by the spring boot has worked . Inside this class lies ApplicationNoWebSecurityConfigurerAdapter
which sets defaults. Some of them can be influenced through the settings:
docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
look for "# SECURITY PROPERTIES", you can also look at the code:
SecurityProperties
Donot configure spring boot configuration for completeness:
@TestPropertySource(properties = [
"security.user.password=pass",
"security.enable-csrf=true",
"security.sessions=if_required"
])
Filters
Spring Security in a web application starts with a servlet filter.
We’ll try to make a deal, but first we’ll add a test with successful authorization.
@Test
void 'api call with authentication must succeed'() {
given()
.auth().preemptive().basic("user", "pass")
.when()
.get("/")
.then()
.statusCode(HttpStatus.SC_OK);
}
Put the break and run the test.
fig. 1 - get method
go down the huge stack of calls
(new Exception().getStackTrace().length == 91)
and find the first mention of spring pic. 2 - call stack
Let's see what lies in the variable
filterChain
fig. 3 - application filter chain
An interesting filter here is
springSecurityFilterChain
that it does all the work of SS in the web part. Itself is
DelegatingFilterProxyRegistrationBean
not very interesting, let's see to whom he delegates his work fig. 4 - filter chain proxy
He delegates his work to the class
FilterChainProxy
. Inside it, several interesting things happen. First of all, let's look at the method
FilterChainProxy#doFilterInternal
. What's going on here? We get the filters, create VirtualFilterChain
and run a request and response on them.List filters = getFilters(fwRequest);
...
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
Inside the getFilters method, take the first one
SecurityFilterChain
that matches the request.private List getFilters(HttpServletRequest request) {
for (SecurityFilterChain chain : filterChains) {
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}
Let's go to the debugger and see which list iterates.
fig. 5 - security filter chains
What does this list tell us?
In both sheets lies
OrRequestMatcher
that will try to match the current url with at least one pattern from the list. The first element of the list has an empty filter list, so there will be no additional filtering, and as a result there will be no protection.
Check in practice.
Any url that matches these patterns 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";
}
Let's write a test:
@Test
void 'get css/hello must succeed'() {
when()
.get("css/hello")
.then()
.statusCode(HttpStatus.SC_OK);
}
Much more interesting is the second SecurityFilterChain which will match any url "/ **"
In our case, there is the following list of filters.
0 = {WebAsyncManagerIntegrationFilter}
1 = {SecurityContextPersistenceFilter}
2 = {HeaderWriterFilter}
3 = {CsrfFilter}
4 = {LogoutFilter}
5 = {BasicAuthenticationFilter}
6 = {RequestCacheAwareFilter}
7 = {SecurityContextHolderAwareRequestFilter}
8 = {AnonymousAuthenticationFilter}
9 = {SessionManagementFilter}
10 = {ExceptionTranslationFilter}
11 = {FilterSecurityInterceptor}
This list may vary depending on the settings and dependencies added.
For example, with this configuration:
http
.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
Filters would be added to this list:
UsernamePasswordAuthenticationFilter
DefaultLoginPageGeneratingFilter
In what order the filters go by default can be seen here: FilterComparator
0 = {WebAsyncManagerIntegrationFilter}
We are not very interested, according to the documentation, it “integrates” SecurityContext with WebAsyncManager which is responsible for asynchronous requests.
1 = {SecurityContextPersistenceFilter}
Searches for the SecurityContext in the session and populates the SecurityContextHolder if it finds.
By default, ThreadLocalSecurityContextHolderStrategy is used which stores the SecurityContext in the ThreadLocal variable.
2 = {HeaderWriterFilter}
Just adds headers to response.
Disable cache:
- Cache-Control: no-cache, no-store, max-age = 0, must-revalidate
- Pragma: no-cache
- Expires: 0
We do not allow browsers to automatically determine the type of content:
- X-Content-Type- Options: nosnif
Do not allow iframe
- X-Frame-Options: DENY
Enable built-in protection in the browser from cross-site scripting (XSS)
- X-XSS-Protection: 1; mode = block
3 = {CsrfFilter}
Perhaps there is not a single developer who, when getting acquainted with SS, would not encounter the error "lack of csrf token."
Why haven't we seen this error before? It's simple, we ran methods on which there is no csrf protection.
Let's try to add a POST method
@PostMapping("post")
public String testPost() {
return "Hello it is post request";
}
Test:
@Test
void 'POST without CSRF token must return 403'() {
given()
.auth().preemptive().basic("user", "pass")
.when()
.post("/post")
.then()
.statusCode(HttpStatus.SC_FORBIDDEN);
}
The test was successful, we returned a 403 error, csrf protection in place.
4 = {LogoutFilter}
Next comes the logout filter, it checks if the url matches the pattern
Ant [pattern='/logout', POST] - по умолчанию
and starts the logout procedure
handler = {CompositeLogoutHandler}
logoutHandlers = {ArrayList} size = 2
0 = {CsrfLogoutHandler}
1 = {SecurityContextLogoutHandler}
by default, the following occurs:
- The Csrf token is removed.
- The session ends
- Brushed SecurityContextHolder
5 = {BasicAuthenticationFilter}
Now we get directly to authentication. What is going on inside?
The filter checks to see if there is an Authorization header with a value starting in Basic.
If it finds it, it extracts the username \ password and passes them to
AuthenticationManager
Inside something like this:
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);
}
AuthenticationManager
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationManager is an interface that accepts Authentication and returns Authentication too.
In our case, the implementation of Authentication is UsernamePasswordAuthenticationToken.
It would be possible to implement AuthenticationManager itself, but it makes little sense, there is a default implementation - ProviderManager.
The ProviderManager delegates authorization to another interface:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class authentication);
}
When we pass an object
Authentication
to ProviderManager
, it AuthenticationProvider
iterates over the existing -rys and checks if the AuthenticationProvider supports this Authentication implementation
public boolean supports(Class authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
As a result, inside
AuthenticationProvider.authenticate
we can already cast the passed Authentication into the desired implementation without execution casts. Next, from the concrete implementation we get the credit cards.
If authentication fails, it
AuthenticationProvider
should quit the execution, ProviderManager
catch it and try the next AuthenticationProvider from the list, if no AuthenticationProvider returns successful authentication, the ProviderManager will forward the last caught execution. The process is described in more detail and with pictures here:
https://spring.io/guides/topicals/spring-security-architecture/
Next, it
BasicAuthenticationFilter
saves the received Authentication in SecurityContextHolder SecurityContextHolder.getContext (). SetAuthentication (authResult);
The authentication process is now complete.
If an AuthenticationException is thrown, the
SecurityContextHolder.clearContext();
context will be reset and an AuthenticationEntryPoint will be called.
public interface AuthenticationEntryPoint {
void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException;
}
The task of AuthenticationEntryPoint is to record in response information that authentication failed.
In the case of basic authentication, this will be:
response.addHeader("WWW-Authenticate", "Basic realm=\"" + realmName + "\"");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
As a result, the browser displays the basic authorization window.
6 = {RequestCacheAwareFilter}
What is this filter for? Imagine a scenario:
1. A user logs on to a secure url.
2. He throws it to the login page.
3. After successful authorization, the user is redirected to the page that he requested at the beginning.
It is for the restoration of the original request that this filter exists.
Inside, it is checked if there is a saved request, if it is, the current request is substituted for it.
The request is saved in the session, at what stage it is saved will be written below.
Let's try to reproduce.
Add a method:
@GetMapping("customHeader")
public String customHeader(@RequestHeader("x-custom-header") String customHeader) {
return customHeader;
}
Add a test:
@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"));
}
As we see in the second request, the header that we passed in the first request returned to us. The filter is working.
7 = {SecurityContextHolderAwareRequestFilter}
Wraps an existing request in SecurityContextHolderAwareRequestWrapper
chain.doFilter(this.requestFactory.create((HttpServletRequest) req, (HttpServletResponse) res), res);
Implementation may vary depending on servlet api version of servlet 2.5 / 3
8 = {AnonymousAuthenticationFilter}
If by the time this filter is executed, the SecurityContextHolder is empty, i.e. authentication failed; the filter fills the SecurityContextHolder object with anonymous authentication - AnonymousAuthenticationToken with the role "ROLE_ANONYMOUS".
This ensures that there will be an object in the SecurityContextHolder, this allows you to not be afraid of NP, as well as a more flexible approach to setting access for unauthorized users.
9 = {SessionManagementFilter}
At this stage, actions associated with the session are performed.
This can be:
- changing the session identifier
- limiting the number of simultaneous sessions
- saving the SecurityContext in securityContextRepository
In our case, the following happens:
SecurityContextRepository
with the default implementation, HttpSessionSecurityContextRepository saves the SecurityContext into the session. Called
sessionAuthenticationStrategy.onAuthentication
Inside sessionAuthenticationStrategy lies:
sessionAuthenticationStrategy = {CompositeSessionAuthenticationStrategy}
delegateStrategies
0 = {ChangeSessionIdAuthenticationStrategy}
1 = {CsrfAuthenticationStrategy}
2 things happen:
1. By default, protection from session fixation attack is enabled, that is, after authentication, the session id changes.
2. If a csrf token was transferred, a new csrf token is generated.
Let's try to check the first item:
@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)
}
10 = {ExceptionTranslationFilter}
К этому моменту SecurityContext должен содеражть анонимную, либо нормальную аутентификацию.
ExceptionTranslationFilter прокидывает запрос и ответ по filter chain и обрабатывает возможные ошибки авторизации.
SS различает 2 случая:
1. AuthenticationException
Вызывается
sendStartAuthentication
, внутри которого происходит следующиее:SecurityContextHolder.getContext().setAuthentication(null);
— отчищает SecurityContextHolderrequestCache.saveRequest(request, response);
— сохраняет в requestCache текущий запрос, чтобы RequestCacheAwareFilter было что восстанавливать.authenticationEntryPoint.commence(request, response, reason);
— вызывает authenticationEntryPoint — который записывает в ответ сигнал о том что необходимо произвести аутентификацию (заголовки \ редирект) 2. AccessDeniedException
Тут опять возможны 2 случая:
if (authenticationTrustResolver.isAnonymous(authentication) ||
authenticationTrustResolver.isRememberMe(authentication)) {
...
} else {
...
}
1. A user with anonymous authentication, or with authentication using the rememberMe token
is called sendStartAuthentication
2. A user with full, non-anonymous authentication is called:
accessDeniedHandler.handle (request, response, (AccessDeniedException) exception)
which defaults the forbidden 403 response
11 = {FilterSecurityInterceptor}
At the last stage, authorization based on the url of the request occurs.
FilterSecurityInterceptor inherits from AbstractSecurityInterceptor and decides whether the current user has access to the current url.
There is another implementation of MethodSecurityInterceptor which is responsible for the admission to the method call when using @Secured \ @PreAuthorize annotations.
AccessDecisionManager
is called internally. There are several strategies for deciding whether to allow or not, the default is: AffirmativeBased
code inside is very simple:
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();
In other words, if someone votes in favor, skip it, if at least 1 vote against we do not let it go, if no one votes we do not let it go.
To summarize:
springSecurityFilterChain
- a set of spring security filters. An example of a set of filters for basic authorization:
WebAsyncManagerIntegrationFilter
- Integrates SecurityContext with WebAsyncManager SecurityContextPersistenceFilter
- Looks for SecurityContext in the session and fills in SecurityContextHolder if it finds HeaderWriterFilter
- Adds “security” headers in response CsrfFilter
- Checks for csrf token LogoutFilter
- Performs logout BasicAuthenticationFilter
- Performs basic authentication RequestCacheAwareFilter
- Restores the request saved before authentication, if there is one SecurityContextHolderAwareRequestFilter
- Wraps an existing request in SecurityContextHolderAwareRequestWrapperAnonymousAuthenticationFilter
- Fills the SecurityContext with anonymous authentication SessionManagementFilter
- Performs session-related actions ExceptionTranslationFilter
- Handles the AuthenticationException \ AccessDeniedException that occurs down 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
- the interface responsible for authentication ProviderManager
- the AuthenticationManager implementation that uses internally uses AuthenticationProvider
AuthenticationProvider
- the interface is responsible for authenticating the specific implementation Authentication
. 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 it has Authentication
access to some resource. AffirmativeBased
- The default strategy used by AccessDecisionManager.Recommendations
Write simple tests that test the order of filters and their settings
this will avoid unpleasant surprises.
FilterChainIT.groovy
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class FilterChainIT {
@Autowired
FilterChainProxy filterChainProxy;
@Autowired
List 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);
}
}
Do not call SecurityContextHolder.getContext (). GetAuthentication (); to get the current user
Authentication is not a very usable object per se. Almost all methods return Object, and to get the necessary information you need to cast it to a specific implementation.
Better get an interface, make an implementation depending on your needs, write HandlerMethodArgumentResolver.
Code with this approach is better to read, test, maintain.
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();
}
Extend existing implementations
Spring security contains many interfaces that you can implement, but most likely there is an abstract class that does 99% what you need.
For example, for the Authentication interface, exists
AbstractAuthenticationToken
, and it is reasonable to inherit the authentication filter fromAbstractAuthenticationProcessingFilter
Use SecurityConfigurerAdapter to Configure Your Authentication
If you have fully custom authentication, most likely you had to do the following:
1. Create an Authentication implementation
2. Create an AuthenticationProvider that supports your Authentication implementation
3. Add a filter that started the authentication process.
It makes sense to combine them all in one place. Look at
HttpBasicConfigurer, OpenIDLoginConfigurer
they do the same thing.
class MyConfigurer extends SecurityConfigurerAdapter {
@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())
}
}
To limit the method call by role, use @Secured \ @PreAuthorize
Write a test that will pass through all the methods of the controller and check for the presence of @Secured \ @PreAuthorize annotations.
When configuring WebSecurityConfigurerAdapter, require authorization for all urls. Add exceptions if necessary. Exceptions should be as strict as possible.
Explicitly specify the type of the http method, and the url should be as complete as possible.
It is better to explicitly indicate the full path to the method, even if at the time of writing other api with such an endpoint was not there.
For example if there is a controller with two methods GET:
"url/methodOne", "url/methodTwo"
, It is not necessary to do so:
authorizeRequests().antMatchers(HttpMethod.GET, "url/**").permitAll().
Better write:
authorizeRequests().antMatchers(HttpMethod.GET, "url/methodOne", "url/methodTwo").permitAll().
In case of problems, enable org.springframework.security: debug
Spring Security has quite detailed debug logs, often they are enough to understand the essence of the problem.
Distinguish between antMatchers ("permit_all_url"). PermitAll () and web.ignoring (). AntMatchers ("ignored_url")
@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");
}
In the case of “ignored_url” it will be checked at the stage of selecting the security filter chain, and if the url matches, an empty filter will be used.
In the case of permit_all_url, verification will take place at the AccessDecisionManager stage.