Spring Security - API token authentication implementation

Common permission authentication is done by providing a "username password". There are some APIs in the business, and we want to verify them in the form of API Tokens. For example, adding a token /api?token=xxxxto the URL allows API access. The logic behind this design is that usernames and passwords have higher permissions, and API tokens can only give permissions to a certain subsystem.

Spring Security 

Java Servlet and Spring Security uses a design pattern Chain of Responsibility pattern . Simply put, they all define many filters, and each request will be processed by layers of filters and finally returned.


Spring Security registers a filter in the filter chain of the servlet FilterChainProxy, which will proxy requests to multiple filter chains maintained by Spring Security itself, and each filter chain will match some URLs, as shown in the figure /foo/**, If it matches, the corresponding filter is executed. Filter chains are sequential, and a request will only execute the first matching filter chain. The configuration of Spring Security is essentially adding, deleting, and modifying filters. The figure is arranged http.formLogin()a filter chain:

You can see the default filter contains a lot of content, such as CsrfFilterto generate and verify CSRF Token, UsernamePasswordAuthenticationFilterto handle username and password authentication, and SessionManagementFilterto manage the Session, and so on. The "authorization authentication" we care about is actually divided into two parts:

Authentication: It means "you are you". If the username and password match, the operator is considered to be the user.
Authorization (Authorization): That is to say, "Do you qualify?" For example, the "Delete" function is only allowed for administrators.

Authentication

Take username and password as an example. To authenticate whether a user is a system user, we need two steps:

One extracts authentication information such as username and password information from the requested message. Authentication information need to implement Authenticationthe interface.
The other is used to verify that the authentication information is correct, such as whether the password is correct and the API token is correct.
In addition, it is determined whether the user is qualified to access a URL, which belongs to authorization.
User authentication password generally require custom logic and often complex, Spring Security is AuthenticationManagerdefined validation interface:

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

If authentication is passed, return authentication information (such as authentication information after erasing password)
If the authentication fails, throw AuthenticationExceptionan exception.
If it cannot be decided, returns null.
The most commonly used internal implementation of Spring Security is ProviderManager, and it uses an authentication chain internally, which contains multiple AuthenticationProvier, ProviderManagerwill be called one by one until a provider returns successfully.

public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
    boolean supports(Class<?> authentication);
}

The AuthenticationManagerdifference is that it is more a supportsmethod used to determine whether the Provider supports the current authentication information. For example, an API token authenticator does not support authentication information for username and password.

In addition, the ProviderManager also defines a parent-child relationship. If all the Provers in the ProviderManager cannot authenticate a certain information, it will let the parent ProviderManager determine. As shown in the figure: 

In theory, we don't need to understand these things, we can write a filter to handle all the requirements ourselves. If you only use this interface will be able to enjoy some "infrastructure" Spring Security, such as throwing AuthenticationException, the ExceptionTranslationFiltercalls configured authenticationEntryPoint.commence()methods for processing and returns 401 and so on.


To determine "You have not qualified", we must first know information about "you", that is to say before a section of Authenticationthe interface; secondly need to know the allocation of resources and access to resources, such as access to URL, which can be What role access. Similarly, Spring Security has defined the relevant interface, authorization will FilterSecurityInterceptorstart in.

public  interface  AccessDecisionManager  {

    void decide(Authentication authentication,
                Object object,
                Collection<ConfigAttribute> configAttributes)
            throws AccessDeniedException, InsufficientAuthenticationException;

    boolean supports(ConfigAttribute attribute);

    boolean supports(Class<?> clazz);
}

Function decidewill determine whether authorization is successful, if the permissions you throw AccessDeniedExceptionan exception. Function parameter description:

authentication Represents "authentication information", from which information such as the role of the current user can be obtained
object The resource to be accessed, such as a URL or a function
configAttributesRepresents the configuration of the resource, such as the URL can only be accessed by the "Administrator" role ( ROLE_ADMIN).
Spring Security, a specific authorization policy is "voting mechanism", each AccessDecisionVoter able to vote, and finally how the statistical results from AccessDecisionManagerthe specific implementation decisions. As AffirmativeBasedjust need someone to agree to; ConsensusBasedrequired majority in favor; UnanimousBasedthe needs of all in favor. Used by default AffirmativeBased.

Like Authentication, following this set of logic, Spring Security's default configuration can reduce our workload. For example, the voting mechanism mentioned above, as well as processing such as returning 403 when throwing an AccessDeniedException.

Configuration

The working principle of Spring Security is not difficult to understand, but how to achieve the desired configuration has always been a pain point in my studies. Here is only a brief explanation, the specific configuration can not be explained in a few words. Here is a simple example to illustrate some correspondences:

@Configuration
@Order(1)
public class TokenSecurityConfig extends WebSecurityConfigurerAdapter { // ①

    // ②
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(new TokenAuthenticationProvider(tokenService));
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .antMatcher("/api/v1/square/**") // ③
                .addFilterAfter(new TokenAuthenticationFilter(), BasicAuthenticationFilter.class) // ④
                .authorizeRequests()
                .anyRequest().hasRole("API"); // ⑤
    }
}

Inheritance WebSecurityConfigurerAdapterbegins. Spring Security previously mentioned filter may comprise a plurality of chains, each WebSecurityConfigurerAdaptercorresponding to a filter chain. ③ specified URL pattern to match, by the order @Orderspecified.
Overload configure(AuthenticationManagerBuilder auth)method for authentication logic configuration, a WebSecurityConfigurerAdapterconfiguration will generate a ProviderManager, and this configuremethod may provide a plurality AuthenticationProvier.
Specifies the URL pattern to be matched by the current filter chain. Used antMatcherto specify a mode, requestMatcheror requestMatchersto perform advanced configuration, such as specifying a plurality of modes.
By addFiltercan add filters in the current filter chain related methods, but it seems there is no way to delete.
hasRoleEtc. are used to specify the "authorization" of logic, such as the bank said URL access all need API role.

API Token implementation

To achieve the authorization authentication of the API Token mentioned at the beginning, we need the following things:

A Authenticationrealization for storing authentication token related information.
A filter to extract the token information in the request
A AuthenticationProvierused to confirm the authentication token information is correct.
When authentication fails, we want to return a custom error message, so we need a filter.
Certification Information
Since the API token only needs to store the token itself, the implementation is as follows:

public class TokenAuthentication implements Authentication {
    private String token;

    private TokenAuthentication(String token) {
        this.token = token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }

    // ... omit other methods
 }

Filter for extracting tokens

Because the token information is specified in the URL, this filter reads the parameters in the URL and generates the definition in the previous section TokenAuthentication:

public  class  TokenAuthenticationFilter  extends  OncePerRequestFilter  { // ①

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain fc)
            throws ServletException, IOException {

        SecurityContext context = SecurityContextHolder.getContext();
        if (context.getAuthentication() != null && context.getAuthentication().isAuthenticated()) {
            // do nothing
        } else {
            // ②
            Map<String, String[]> params = req.getParameterMap();
            if (!params.isEmpty() && params.containsKey("token")) {
                String token = params.get("token")[0];
                if (token != null) {
                    Authentication auth = new TokenAuthentication(token);
                    SecurityContextHolder.getContext().setAuthentication(auth);
                }
            }
            req.setAttribute("me.lotabout.springsecurityexample.security.TokenAuthenticationFilter.FILTERED", true); //③
        }

        fc.doFilter(req, res); //④
    }
}
① inherited from OncePerRequestFilterno particular intention, its function is to prevent the filter is called multiple times
② Obtain the token in the URL and store the generated Authentication in the SecurityContext for subsequent logic use
After the attribute is set in ③, the filter will not be called again
④ execute the following filter

The above will get the Token in the URL. We need to compare it with the token in the database to see if it is consistent. Here we use the comparison in memory instead:

public class TokenAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        if (authentication.isAuthenticated()) {
            return authentication;
        }

        // 从 TokenAuthentication 中获取 token
        String token = authentication.getCredentials().toString();
        if (Strings.isNullOrEmpty(token)) {
            return authentication;
        }

        if (!token.equals("abcdefg")) {
            throw ResultException.of(MyError.TOKEN_NOT_FOUND).errorData(token);
        }

        User user = User.builder()
                    .username("api")
                    .password("")
                    .authorities(Role.API)
                    .build();

        // Return the new authentication information, with the token and         undetected user information
         Authentication auth = new PreAuthenticatedAuthenticationToken (user, token, user.getAuthorities ()); 
auth.setAuthenticated ( true ); return auth;     }
        


    @Override
    public boolean supports(Class<?> aClass) {
        return (TokenAuthenticationFilter.TokenAuthentication.class.isAssignableFrom(aClass));
    }
}


Error handling

We hope that when the error, return a 200 status code, while the body contains "success": falseand specific error messages.

public class ResultExceptionTranslationFilter extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain fc) throws IOException, ServletException {
        try {
            fc.doFilter(request, response);
        } catch (ResultException ex) {
            response.setContentType("application/json; charset=UTF-8");
            response.setCharacterEncoding("UTF-8");
            response.getWriter().println(JsonUtil.toJson(Response.of(ex)));
            response.getWriter().flush();
        }
    }
}

Assembly configuration

The specific configuration is similar to that mentioned above. Note that we also closed CSRF and Session.

@Configuration
@Order(1)
public class PredictorSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(new TokenAuthenticationProvider(tokenService));
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .antMatcher(PATTERN_SQUARE)
                .addFilterAfter(new TokenAuthenticationFilter(), BasicAuthenticationFilter.class)
                .addFilterAfter(new ResultExceptionTranslationFilter(), ExceptionTranslationFilter.class)
                .authorizeRequests()
                .anyRequest().hasRole("API")
                .and()
                .csrf()
                .disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}
The complete code can be found in the Spring Security Example .

Comments

Popular posts from this blog

Today Walkin 14th-Sept

Spring Elasticsearch Operations

Hibernate Search - Elasticsearch with JSON manipulation