Securing REST API using custom tokens (stateless, no UI, no cookies, no basic authentication, no OAuth, no login page)

前端 未结 5 1967
轻奢々
轻奢々 2020-12-22 18:05

There are lots of guidelines, sample codes that show how to secure REST API with Spring Security, but most of them assume a web client and talk about login page, redirection

5条回答
  •  陌清茗
    陌清茗 (楼主)
    2020-12-22 18:18

    You're right, it isn't easy and there aren't many good examples out there. Examples i saw made it so you couldn't use other spring security stuff side by side. I did something similar recently, here's what i did.

    You need a custom token to hold your header value

    public class CustomToken extends AbstractAuthenticationToken {
      private final String value;
    
      //Getters and Constructor.  Make sure getAutheticated returns false at first.
      //I made mine "immutable" via:
    
          @Override
    public void setAuthenticated(boolean isAuthenticated) {
        //It doesn't make sense to let just anyone set this token to authenticated, so we block it
        //Similar precautions are taken in other spring framework tokens, EG: UsernamePasswordAuthenticationToken
        if (isAuthenticated) {
    
            throw new IllegalArgumentException(MESSAGE_CANNOT_SET_AUTHENTICATED);
        }
    
        super.setAuthenticated(false);
    }
    }
    

    You need a spring security filter to extract the header and ask the manager to authenticate it, something like thisemphasized text

    public class CustomFilter extends AbstractAuthenticationProcessingFilter {
    
    
        public CustomFilter(RequestMatcher requestMatcher) {
            super(requestMatcher);
    
            this.setAuthenticationSuccessHandler((request, response, authentication) -> {
            /*
             * On success the desired action is to chain through the remaining filters.
             * Chaining is not possible through the success handlers, because the chain is not accessible in this method.
             * As such, this success handler implementation does nothing, and chaining is accomplished by overriding the successfulAuthentication method as per:
             * http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
             * "Subclasses can override this method to continue the FilterChain after successful authentication."
             */
            });
    
        }
    
    
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
                throws AuthenticationException, IOException, ServletException {
    
    
            String tokenValue = request.getHeader("SOMEHEADER");
    
            if(StringUtils.isEmpty(tokenValue)) {
                //Doing this check is kinda dumb because we check for it up above in doFilter
                //..but this is a public method and we can't do much if we don't have the header
                //also we can't do the check only here because we don't have the chain available
               return null;
            }
    
    
            CustomToken token = new CustomToken(tokenValue);
            token.setDetails(authenticationDetailsSource.buildDetails(request));
    
            return this.getAuthenticationManager().authenticate(token);
        }
    
    
    
        /*
         * Overriding this method to maintain the chaining on authentication success.
         * http://docs.spring.io/autorepo/docs/spring-security/3.2.4.RELEASE/apidocs/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.html#successfulAuthentication(javax.servlet.http.HttpServletRequest,%20javax.servlet.http.HttpServletResponse,%20javax.servlet.FilterChain,%20org.springframework.security.core.Authentication)
         * "Subclasses can override this method to continue the FilterChain after successful authentication."
         */
        @Override
        protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
    
    
            //if this isn't called, then no auth is set in the security context holder
            //and subsequent security filters can still execute.  
            //so in SOME cases you might want to conditionally call this
            super.successfulAuthentication(request, response, chain, authResult);
    
            //Continue the chain
            chain.doFilter(request, response);
    
        }
    
    
    }
    

    Register your custom filter in spring security chain

     @Configuration
     public static class ResourceEndpointsSecurityConfig extends WebSecurityConfigurerAdapter {        
    
          //Note, we don't register this as a bean as we don't want it to be added to the main Filter chain, just the spring security filter chain
          protected AbstractAuthenticationProcessingFilter createCustomFilter() throws Exception {
            CustomFilter filter = new CustomFilter( new RegexRequestMatcher("^/.*", null));
            filter.setAuthenticationManager(this.authenticationManagerBean());
            return filter;
          }
    
           @Override
           protected void configure(HttpSecurity http) throws Exception {                  
    
                http
                //fyi: This adds it to the spring security proxy filter chain
                .addFilterBefore(createCustomFilter(), AnonymousAuthenticationFilter.class)
           }
    }
    

    A custom auth provider to validate that token extracted with the filter.

    public class CustomAuthenticationProvider implements AuthenticationProvider {
    
    
        @Override
        public Authentication authenticate(Authentication auth)
                throws AuthenticationException {
    
            CustomToken token = (CustomToken)auth;
    
            try{
               //Authenticate token against redis or whatever you want
    
                //This i found weird, you need a Principal in your Token...I use User
                //I found this to be very redundant in spring security, but Controller param resolving will break if you don't do this...anoying
                org.springframework.security.core.userdetails.User principal = new User(...); 
    
                //Our token resolved to a username so i went with this token...you could make your CustomToken take the principal.  getCredentials returns "NO_PASSWORD"..it gets cleared out anyways.  also the getAuthenticated for the thing you return should return true now
                return new UsernamePasswordAuthenticationToken(principal, auth.getCredentials(), principal.getAuthorities());
            } catch(Expection e){
                //TODO throw appropriate AuthenticationException types
                throw new BadCredentialsException(MESSAGE_AUTHENTICATION_FAILURE, e);
            }
    
    
        }
    
        @Override
        public boolean supports(Class authentication) {
            return CustomToken.class.isAssignableFrom(authentication);
        }
    
    
    }
    

    Finally, register your provider as a bean so the authentication manager finds it in some @Configuration class. You probably could just @Component it too, i prefer this method

    @Bean
    public AuthenticationProvider createCustomAuthenticationProvider(injectedDependencies)  {
        return new CustomAuthenticationProvider(injectedDependencies);
    }
    

提交回复
热议问题