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

前端 未结 5 1870
轻奢々
轻奢々 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);
    }
    
    0 讨论(0)
  • 2020-12-22 18:18

    Another Example Project which uses JWT - Jhipster

    Try Generating a Microservice application using JHipster. It generates a template with out of the box integration between Spring Security and JWT.

    https://jhipster.github.io/security/

    0 讨论(0)
  • 2020-12-22 18:27

    My sample app does exactly this - securing REST endpoints using Spring Security in a stateless scenario. Individual REST calls are authenticated using an HTTP header. Authentication information is stored on the server side in an in-memory cache and provides the same semantics as those offered by the HTTP session in a typical web application. The app uses the full Spring Security infrastructure with very minimum custom code. No bare filters, no code outside of the Spring Security infrastructure.

    The basic idea is to implement the following four Spring Security components:

    1. org.springframework.security.web.AuthenticationEntryPoint to trap REST calls requiring authentication but missing the required authentication token and thereby deny the requests.
    2. org.springframework.security.core.Authentication to hold the authentication information required for the REST API.
    3. org.springframework.security.authentication.AuthenticationProvider to perform the actual authentication (against a database, an LDAP server, a web service, etc.).
    4. org.springframework.security.web.context.SecurityContextRepository to hold the authentication token in between HTTP requests. In the sample, the implementation saves the token in an EHCACHE instance.

    The sample uses XML configuration but you can easily come up with the equivalent Java config.

    0 讨论(0)
  • 2020-12-22 18:35

    I recommend JSON Web Tokens http://jwt.io/ , it's stateless and scalable.

    Here is an example project, https://github.com/brahalla/Cerberus

    0 讨论(0)
  • 2020-12-22 18:43

    The code secure all endpoints - but I'm sure that you can play with that :). The token is stored in Redis using Spring Boot Starter Security and you have to define our own UserDetailsService which you pass into AuthenticationManagerBuilder.

    Long story short - copy paste EmbeddedRedisConfiguration and SecurityConfig and replace AuthenticationManagerBuilder to your logic.

    HTTP:

    Requesting token - sending basic HTTP auth content in a request header. A token is given back in a response header.

    http --print=hH -a user:password localhost:8080/v1/users
    
    GET /v1/users HTTP/1.1
    Accept: */*
    Accept-Encoding: gzip, deflate
    Authorization: Basic dXNlcjpwYXNzd29yZA==
    Connection: keep-alive
    Host: localhost:8080
    User-Agent: HTTPie/0.9.3
    
    HTTP/1.1 200 OK
    Cache-Control: no-cache, no-store, max-age=0, must-revalidate
    Content-Length: 4
    Content-Type: text/plain;charset=UTF-8
    Date: Fri, 06 May 2016 09:44:23 GMT
    Expires: 0
    Pragma: no-cache
    Server: Apache-Coyote/1.1
    X-Application-Context: application
    X-Content-Type-Options: nosniff
    X-Frame-Options: DENY
    X-XSS-Protection: 1; mode=block
    x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af
    

    Same request but using token:

    http --print=hH localhost:8080/v1/users 'x-auth-token: cacf4a97-75fe-464d-b499-fcfacb31c8af'
    
    GET /v1/users HTTP/1.1
    Accept: */*
    Accept-Encoding: gzip, deflate
    Connection: keep-alive
    Host: localhost:8080
    User-Agent: HTTPie/0.9.3
    x-auth-token:  cacf4a97-75fe-464d-b499-fcfacb31c8af
    
    HTTP/1.1 200 OK
    Cache-Control: no-cache, no-store, max-age=0, must-revalidate
    Content-Length: 4
    Content-Type: text/plain;charset=UTF-8
    Date: Fri, 06 May 2016 09:44:58 GMT
    Expires: 0
    Pragma: no-cache
    Server: Apache-Coyote/1.1
    X-Application-Context: application
    X-Content-Type-Options: nosniff
    X-Frame-Options: DENY
    X-XSS-Protection: 1; mode=block
    

    If you pass wrong username/password or token you get 401.

    JAVA

    I added those dependencies into build.gradle

    compile("org.springframework.session:spring-session-data-redis:1.0.1.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-security")
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("com.github.kstyrc:embedded-redis:0.6")
    

    Then Redis configration

    @Configuration
    @EnableRedisHttpSession
    public class EmbeddedRedisConfiguration {
    
        private static RedisServer redisServer;
    
        @Bean
        public JedisConnectionFactory connectionFactory() throws IOException {
            redisServer = new RedisServer(Protocol.DEFAULT_PORT);
            redisServer.start();
            return new JedisConnectionFactory();
        }
    
        @PreDestroy
        public void destroy() {
            redisServer.stop();
        }
    
    }
    

    Security config:

    @Configuration
    @EnableWebSecurity
    @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        UserService userService;
    
        @Override
        protected void configure(AuthenticationManagerBuilder builder) throws Exception {
            builder.userDetailsService(userService);
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .requestCache()
                    .requestCache(new NullRequestCache())
                    .and()
                    .httpBasic();
        }
    
        @Bean
        public HttpSessionStrategy httpSessionStrategy() {
            return new HeaderHttpSessionStrategy();
        }
    }
    

    Usually in tutorials you find AuthenticationManagerBuilder using inMemoryAuthentication but there is a lot more choices (LDAP, ...) Just take a look into class definition. I'm using userDetailsService which requires UserDetailsService object.

    And finally my user service using CrudRepository.

    @Service
    public class UserService implements UserDetailsService {
    
        @Autowired
        UserRepository userRepository;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            UserAccount userAccount = userRepository.findByEmail(username);
            if (userAccount == null) {
                return null;
            }
            return new User(username, userAccount.getPassword(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
        }
    }
    
    0 讨论(0)
提交回复
热议问题