Java 微服务Spring云网关+;Spring安全LDAP作为SSO+;JWT-请求/响应之间的令牌丢失

Java 微服务Spring云网关+;Spring安全LDAP作为SSO+;JWT-请求/响应之间的令牌丢失,java,spring-boot,spring-security,jwt,microservices,Java,Spring Boot,Spring Security,Jwt,Microservices,我正在使用SpringBoot开发一个微服务生态系统。目前到位的微服务: Spring Cloud Gateway-Zuul(还负责微服务下游的授权请求-从请求中提取令牌并验证用户是否具有执行请求的正确角色) SSO使用SpringSecurityLDAP(负责验证用户身份并生成JWT令牌),SSO也只有一个使用thymeleaf的登录页面 使用Thymeleaf而不使用登录页面的Web界面(目前不确定是否应该在此处使用spring security) 另一种基于浏览器请求向web ui提供数

我正在使用SpringBoot开发一个微服务生态系统。目前到位的微服务:

  • Spring Cloud Gateway-Zuul(还负责微服务下游的授权请求-从请求中提取令牌并验证用户是否具有执行请求的正确角色)
  • SSO使用SpringSecurityLDAP(负责验证用户身份并生成JWT令牌),SSO也只有一个使用thymeleaf的登录页面
  • 使用Thymeleaf而不使用登录页面的Web界面(目前不确定是否应该在此处使用spring security)
  • 另一种基于浏览器请求向web ui提供数据的微服务
  • 使用Eureka的发现服务
其思想是过滤网关上的所有请求,以验证和转发请求。如果用户未经身份验证或使用令牌,则将用户转发给SSO进行登录。 防火墙将只公开网关端的端口,然后其他端口将使用防火墙规则阻止它们的端口

现在我被阻止了,不知道该去哪里,也不知道是否应该将SSO与网关一起移动(概念上是错误的,但如果我找不到任何解决方案,这可能是一种解决方法)

问题发生后:用户点击网关(例如。http://localhost:7070/web)然后网关将用户转发到(例如。http://localhost:8080/sso/login),在验证凭据后,SSO将创建JWT令牌并将其添加到响应的头中。 然后,SSO将请求重定向回网关(例如。http://localhost:7070/web)

在此之前,一切正常,但当请求到达网关时,请求上没有“授权”头,这意味着没有JWT令牌。

因此网关应该提取令牌,检查凭据并将请求转发到Web接口(例如。http://localhost:9090)

我知道使用SSO上的处理程序重定向请求根本不起作用,因为来自spring的“重定向”将在重定向之前从头中删除令牌。 但是我不知道在Spring将JWT从请求中移除之后,是否有其他方法可以再次在头上设置JWT

架构方面是否存在任何概念上的问题?如何将JWT转发到网关进行检查

单点登录

网关

    @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);
    }
}

有一个关于JWT的问题。这被称为“注销问题”。首先你需要了解它是什么

然后,检查负责将授权头传递给下游的令牌中继过滤器(令牌中继网关过滤器工厂)

如果查看该过滤器,您将看到JWT存储在ConcurrentHashMap(InMemoryReactiveAuth2AuthorizedClient服务)中。键是session,值是JWT。因此,将返回会话id,而不是作为响应提供的JWT头

在此之前,一切正常,但当请求到达 网关请求时没有“授权”标头,这意味着没有 JWT令牌

。当请求到达网关时,TokenRelay筛选器从请求中获取会话id,并从ConcurrentHashMap中找到JWT,然后在下行过程中传递到授权头

这个流可能是由spring安全团队设计的,用于解决JWT注销问题

    @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;
    }
}