简体   繁体   中英

Microservices Spring Cloud Gateway + Spring Security LDAP as SSO + JWT - Token lost between request/response

I am developing a microservice ecosystem using spring-boot. The microservices which are in place at the moment :

  • Spring Cloud Gateway - Zuul (responsible also for authorization requests downstream for microservices - extracting tokens from requests and validates whether the user has the right role to perform requests),
  • SSO using spring security LDAP ( responsible for authenticate user and generate JWT tokens) , SSO has also just a login page using thymeleaf
  • Web interface using Thymeleaf without login page ( not sure if I should use here spring security, at the moment)
  • Another microservice which provides data to web ui based on request from the browser
  • Discovery services using Eureka

The idea is filtering all the requests on the gateway for validating and forward the requests. If the user is not authenticated or token is experied then forward the user to SSO for login. The firewall will expose only the port on Gateway side then others one will be theirs ports blocked using firewall rules.

Now i am blocked without knowing where to go or if I should move the SSO together with the gateway ( conceptually wrong but it might be a workaround if i do not find any solution)

Following the issue : The user hits the gateway (ex. http://localhost:7070/web) then the gateway forward the user to (ex. http://localhost:8080/sso/login), after the credentials have been validated , the SSO creates the JWT tokens and add it to the Header of response. Afterwards the SSO redirect the request back to the gateway (ex. http://localhost:7070/web).

Until here, everything works fine but when the request reaches the gateway there is no 'Authorization' header on request which means NO JWT token.

So the gateway should extract the token, check the credentials and forward the request to the Web interface (ex. http://localhost:9090)

I am aware that using Handler on SSO to redirect request won't work at all due to 'Redirect' from spring will remove the token from the header before redirect. But I do not know whether there is another way to set again the JWT on the header after Spring has removed it from the request or not.

Is there any conceptually issue on the architecture side? How can I forward the JWT to the gateway for being checked?

SSO

@EnableWebSecurity
public class SecurityCredentialsConfig extends WebSecurityConfigurerAdapter {

    @Value("${ldap.url}")
    private String ldapUrl;

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

        http
            .csrf().disable()

            // Stateless session; session won't be used to store user's state.
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

            .and()

            .formLogin()
            .loginPage("/login")
            // Add a handler to add token in the response header and forward the response
            .successHandler(jwtAuthenticationSuccessHandler())
            .failureUrl("/login?error")
            .permitAll()

            .and()

            // handle an authorized attempts
            .exceptionHandling()
            .accessDeniedPage("/login?error")

            .and()

            .authorizeRequests()
            .antMatchers( "/dist/**", "/plugins/**").permitAll()
            .anyRequest().authenticated();

    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .ldapAuthentication()
            .userDnPatterns("uid={0},ou=people")
            .groupSearchBase("ou=groups")
            .userSearchFilter("uid={0}")
            .groupSearchBase("ou=groups")
            .groupSearchFilter("uniqueMember={0}")
            .contextSource()
            .url(ldapUrl);
    }



    @Bean
    public AuthenticationSuccessHandler jwtAuthenticationSuccessHandler() {
        return new JwtAuthenticationSuccessHandler();
    }
}

    public class JwtAuthenticationSuccessHandler extends  SimpleUrlAuthenticationSuccessHandler  {

    @Autowired
    private JwtConfig jwtConfig;
    @Autowired
    private JwtTokenService jwtTokenService;

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

        String token = jwtTokenService.expiring(ImmutableMap.of(
                                                            "email", auth.getName(),
                                                            "authorities", auth.getAuthorities()
                                                                    .stream()
                                                                    .map(GrantedAuthority::getAuthority)
                                                                    .map(Object::toString)
                                                                    .collect(Collectors.joining(","))));

        response.addHeader(jwtConfig.getHeader(), jwtConfig.getPrefix() + token);

        DefaultSavedRequest defaultSavedRequest = (DefaultSavedRequest) request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST");

        if(defaultSavedRequest != null){
            getRedirectStrategy().sendRedirect(request, response, defaultSavedRequest.getRedirectUrl());
        }else{
            getRedirectStrategy().sendRedirect(request, response, "http://localhost:7070/web");
        }
    }

}

Gateway

    @EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtConfig jwtConfig;

    @Value("${accessDeniedPage.url}")
    private String accessDeniedUrl;

    @Override
    protected void configure(final HttpSecurity http) throws Exception {

        http
                .csrf().disable() // Disable CSRF (cross site request forgery)

                // we use stateless session; session won't be used to store user's state.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()

                .formLogin()
                .loginPage("/sso/login")
                .permitAll()

                .and()

                // handle an authorized attempts
                // If a user try to access a resource without having enough permissions
                .exceptionHandling()
                .accessDeniedPage(accessDeniedUrl)
                //.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))

                .and()

                // Add a filter to validate the tokens with every request
                .addFilterBefore(new JwtTokenAuthenticationFilter(jwtConfig), UsernamePasswordAuthenticationFilter.class)

                // authorization requests config
                .authorizeRequests()

                .antMatchers("/web/**").hasAuthority("ADMIN")

                // Any other request must be authenticated
                .anyRequest().authenticated();
    }

}

@RequiredArgsConstructor
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {

    private final JwtConfig jwtConfig;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        // 1. get the authentication header. Tokens are supposed to be passed in the authentication header
        String header = request.getHeader(jwtConfig.getHeader());

        // 2. validate the header and check the prefix
        if(header == null || !header.startsWith(jwtConfig.getPrefix())) {
            chain.doFilter(request, response);        // If not valid, go to the next filter.
            return;
        }

        // If there is no token provided and hence the user won't be authenticated.
        // It's Ok. Maybe the user accessing a public path or asking for a token.

        // All secured paths that needs a token are already defined and secured in config class.
        // And If user tried to access without access token, then he/she won't be authenticated and an exception will be thrown.

        // 3. Get the token
        String token = header.replace(jwtConfig.getPrefix(), "");

        try {  // exceptions might be thrown in creating the claims if for example the token is expired


            // 4. Validate the token
            Claims claims = Jwts.parser()
                                        .setSigningKey(jwtConfig.getSecret().getBytes())
                                        .parseClaimsJws(token)
                                        .getBody();

            String email = claims.get("email").toString();

            if(email != null) {

                String[] authorities = ((String) claims.get("authorities")).split(",");
                final List<String> listAuthorities = Arrays.stream(authorities).collect(Collectors.toList());

                // 5. Create auth object
                // UsernamePasswordAuthenticationToken: A built-in object, used by spring to represent the current authenticated / being authenticated user.
                // It needs a list of authorities, which has type of GrantedAuthority interface, where SimpleGrantedAuthority is an implementation of that interface
                final UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
                        email, null, listAuthorities
                        .stream()
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList()));

                // 6. Authenticate the user
                // Now, user is authenticated
                SecurityContextHolder.getContext().setAuthentication(auth);
            }

        } catch (Exception e) {
            // In case of failure. Make sure it's clear; so guarantee user won't be authenticated
            SecurityContextHolder.clearContext();
        }

        // go to the next filter in the filter chain
        chain.doFilter(request, response);
    }
}

@Component
public class AuthenticatedFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {

        final Object object = SecurityContextHolder.getContext().getAuthentication();
        if (object == null || !(object instanceof UsernamePasswordAuthenticationToken)) {
            return null;
        }

        final UsernamePasswordAuthenticationToken user = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();

        final RequestContext requestContext = RequestContext.getCurrentContext();

        /*
        final AuthenticationDto authenticationDto = new AuthenticationDto();
        authenticationDto.setEmail(user.getPrincipal().toString());
        authenticationDto.setAuthenticated(true);

        authenticationDto.setRoles(user.getAuthorities()
                .stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList())); */

        try {
            //requestContext.addZuulRequestHeader(HttpHeaders.AUTHORIZATION, (new ObjectMapper()).writeValueAsString(authenticationDto));
            requestContext.addZuulRequestHeader(HttpHeaders.AUTHORIZATION, (new ObjectMapper()).writeValueAsString("authenticationDto"));
        } catch (JsonProcessingException e) {
            throw new ZuulException("Error on JSON processing", 500, "Parsing JSON");
        }

        return null;
    }
}

There is an issue about JWT. It is called "Logout Problem". First you need to understand what it is.

Then, check TokenRelay filter (TokenRelayGatewayFilterFactory) which is responsible for passing authorization header to downstream.

If you look at that filter, you will see that JWTs are stored in ConcurrentHashMap (InMemoryReactiveOAuth2AuthorizedClientService). The key is session, the value is JWT. So, session-id is returned instead of JWT header as the response provided.

Until here, everything works fine but when the request reaches the gateway there is no 'Authorization' header on request which means NO JWT token.

Yes . When the request comes to gateway, TokenRelay filter takes session-id from request and find JWT from ConcurrentHashMap, then it passes to Authorization header during downstream.

Probably, this flow is designed by spring security team to address JWT logout problem.

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