简体   繁体   中英

Spring Security Cookie + JWT authentication

I must say I am very confused about the entire model and I need help gluing all the floating pieces together.

I am not doing Spring REST, just plain WebMVC controllers.

My mission: I want a form login with a username + pass authentication. I want to authenticate against a 3rd party service. Upon success I want to return a cookie but NOT use the default cookie token mechanism. I want the cookie to have a JWT token instead. By leveraging the cookie mechanism every request will be sent with the JWT.

So to break it down I have the following modules to take care of:

  1. do authentication against a 3rd party service when doing a user + pas logi n
  2. replace cookie session token with my custom implementation upon successful auth

  3. upon every request parse the JWT from the cookie ( using a filter )

  4. extract user details / data from the JWT to be accessible to the controllers

What's confusing? ( please correct me where I am wrong )

3rd party authentication

to authenticate against a 3rd party I will need to have a custom provider by extending AuthenticationProvider

public class JWTTokenAuthenticationProvider implements AuthenticationProvider { 

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

          // auth against 3rd party

          // return Authentication
          return new UsernamePasswordAuthenticationToken( name, password, new ArrayList<>() );

      }

      @Override
      public boolean supports(Class<?> authentication) {
          return authentication.equals( UsernamePasswordAuthenticationToken.class );
      }

}

Questions:

  • is this provider executed upon successful authentication / login when user submits a form user + pass? if so that how is that related to AbstractAuthenticationProcessingFilter#successfulAuthentication?
  • do I have to return an instance of UsernamePasswordAuthenticationToken?
  • do I have to support UsernamePasswordAuthenticationToken to get user + pass here?

replace cookie token with a JWT

No idea how to do this gracefully, I can think of a number of ways but they not Spring Security ways and I don't want to break out of the flow. Would be thankful for any suggestions here!

parse the JWT with every request from a cookie

From what I understand I need to extend AbstractAuthenticationProcessingFilter like so

public class CookieAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    @Override
    public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response )
            throws AuthenticationException, IOException, ServletException {

        String token = "";

        // get token from a Cookie

        // create an instance to Authentication
        TokenAuthentication authentication = new TokenAuthentication(null, null);

        return getAuthenticationManager().authenticate(tokenAuthentication);

    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res,
                     FilterChain chain) throws IOException, ServletException {
        super.doFilter(req, res, chain);
    }

}

Questions:

  • when is AbstractAuthenticationProcessingFilter#successfulAuthentication called? is it called with the user logs in or when the JWT token been validated successfully?
  • is there any relation between this filter and the custom provider I posted previously? The manager will supposedly call the custom provider based on the token instance which is matched with what the provider supports via the support method?

It seems as if I have all the pieces I need, except the cookie session replacement, but I cannot put them into a single coherent model and I need from somebody who understand the mechanics well enough so I can glue all of this into a single module.

UPDATE 1

OK, I think I am getting where this is starting... https://github.com/spring-projects/spring-security/blob/master/web/src/main/java/org/springframework/security/web/authentication/UsernamePasswordAuthenticationFilter.java

This Filter registers itself to POST -> "/login" and than creates an instance of UsernamePasswordAuthenticationToken and passes the control to the next filter.

Question is where the cookie session is set....

UPDATE 2

This section of the dos gives the top level flow that I was missing, for whoever is going through this take a look here... http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#tech-intro-authentication

This section regarding the AuthenticationProvider... http://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#core-services-authentication-manager

UPDATE 3 - working case, is this the best way??

So after digging through the Spring Security docs and their sources I got the initial model to work. Now, doing this, I realized there is more than one way to do it. Any advice of why picking this way VS what Denys proposed below?

Working example below...

To get this to work the way described in the original post, this is what needs to happen...

Custom Filter

public class CookieAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

        public CookieAuthenticationFilter( RequestMatcher requestMatcher ) {

            super( requestMatcher );
            setAuthenticationManager( super.getAuthenticationManager() );

        }

        @Override
        public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response )
            throws AuthenticationException, IOException, ServletException {

            String token = "";

            // get token from a Cookie
            Cookie[] cookies = request.getCookies();

            if( cookies == null || cookies.length < 1 ) {
                throw new AuthenticationServiceException( "Invalid Token" );
            }

            Cookie sessionCookie = null;
            for( Cookie cookie : cookies ) {
                if( ( "someSessionId" ).equals( cookie.getName() ) ) {
                sessionCookie = cookie;
                break;
                }
            }

            // TODO: move the cookie validation into a private method
            if( sessionCookie == null || StringUtils.isEmpty( sessionCookie.getValue() ) ) {
                throw new AuthenticationServiceException( "Invalid Token" );
            }

            JWTAuthenticationToken jwtAuthentication = new JWTAuthenticationToken( sessionCookie.getValue(), null, null );

            return jwtAuthentication;

        }


        @Override
        public void doFilter(ServletRequest req, ServletResponse res,
                 FilterChain chain) throws IOException, ServletException {
            super.doFilter(req, res, chain);
        }

}

Authentication Provider

attach the provider to the UsernamePasswordAuthenticationToken which is generated by UsernamePasswordAuthenticationFilter, which attaches itself to "/login" POST. For formlogin with POST to "/login" will generate UsernamePasswordAuthenticationToken and your provider will be triggered

@Component
public class ApiAuthenticationProvider implements AuthenticationProvider {

        @Autowired
        TokenAuthenticationService tokenAuthService;

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

            String login = authentication.getName();
            String password = authentication.getCredentials().toString();

            // perform API call to auth against a 3rd party

            // get User data
            User user = new User();

            // create a JWT token
            String jwtToken = "some-token-123"

            return new JWTAuthenticationToken( jwtToken, user, new ArrayList<>() );

        }

        @Override
        public boolean supports( Class<?> authentication ) {
                return authentication.equals( UsernamePasswordAuthenticationToken.class );
        }
}

Custom Authentication object

For the JWT we want to have our own Authentication token object to carry the data that we want along the stack.

public class JWTAuthenticationToken extends AbstractAuthenticationToken {

        User principal;
        String token;

        public JWTAuthenticationToken( String token, User principal, Collection<? extends GrantedAuthority> authorities ) {
            super( authorities );
            this.token = token;
            this.principal = principal;
        }

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

        @Override
        public Object getPrincipal() {
            return principal;
        }

        public void setToken( String token ) {
            this.token = token;
        }

        public String getToken() {
            return token;
        }
}

Authentication Success Handler

This is getting called when our custom provider did it's job by authenticating the user against a 3rd party and generating a JWT token, This is the place where the Cookie gets into the Response.

@Component
public class AuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(
                HttpServletRequest request, 
                HttpServletResponse response, 
                Authentication authentication) throws IOException, ServletException {

        if( !(authentication instanceof JWTAuthenticationToken) ) {
            return;
        }

        JWTAuthenticationToken jwtAuthenticaton =    (JWTAuthenticationToken) authentication;

        // Add a session cookie
        Cookie sessionCookie = new Cookie( "someSessionId", jwtAuthenticaton.getToken() );
        response.addCookie( sessionCookie );

        //clearAuthenticationAttributes(request);

        // call the original impl
        super.onAuthenticationSuccess( request, response, authentication );
}

}

Hooking This All Together

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired @Required
    ApiAuthenticationProvider apiAuthProvider;

    @Autowired @Required
    AuthenticationSuccessHandler authSuccessHandler;

    @Autowired @Required
    SimpleUrlAuthenticationFailureHandler authFailureHandler;

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

    @Override
    protected void configure( HttpSecurity httpSecurity ) throws Exception {

            httpSecurity

            // don't create session
            .sessionManagement()
                .sessionCreationPolicy( SessionCreationPolicy.STATELESS )
                .and()

            .authorizeRequests()
                .antMatchers( "/", "/login", "/register" ).permitAll()
                .antMatchers( "/js/**", "/css/**", "/img/**" ).permitAll()
                .anyRequest().authenticated()
                .and()

            // login
            .formLogin()
                .failureHandler( authFailureHandler )
                //.failureUrl( "/login" )
                .loginPage("/login")
                .successHandler( authSuccessHandler )
                        .and()

            // JWT cookie filter
            .addFilterAfter( getCookieAuthenticationFilter(
                    new AndRequestMatcher( new AntPathRequestMatcher( "/account" ) )
            ) , UsernamePasswordAuthenticationFilter.class );
    }


    @Bean
    SimpleUrlAuthenticationFailureHandler getAuthFailureHandler() {

            SimpleUrlAuthenticationFailureHandler handler = new SimpleUrlAuthenticationFailureHandler( "/login" );
            handler.setDefaultFailureUrl( "/login" );
            //handler.setUseForward( true );

            return handler;

    }

    CookieAuthenticationFilter getCookieAuthenticationFilter( RequestMatcher requestMatcher ) {

            CookieAuthenticationFilter filter = new CookieAuthenticationFilter( requestMatcher );
            filter.setAuthenticationFailureHandler( authFailureHandler );
            return filter;
    }
}

最简单的方法是将Spring Session添加到项目中并扩展HttpSessionStrategy ,它为会话创建/销毁事件提供方便的钩子,并具有从HttpServletRequest中提取会话的方法。

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM