簡體   English   中英

微服務 Spring Cloud Gateway + Spring Security LDAP 作為 SSO + JWT - 請求/響應之間丟失令牌

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

我正在使用 spring-boot 開發微服務生態系統。 目前已經到位的微服務:

  • Spring Cloud Gateway - Zuul(還負責微服務下游的授權請求 - 從請求中提取令牌並驗證用戶是否具有執行請求的正確角色),
  • SSO 使用 spring security LDAP(負責驗證用戶並生成 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 上使用 Handler 重定向請求根本不起作用,因為 spring 的“重定向”會在重定向之前從標頭中刪除令牌。 但是我不知道在 Spring 從請求中刪除它之后是否還有另一種方法可以在標頭上再次設置 JWT。

在架構方面有任何概念上的問題嗎? 如何將 JWT 轉發到網關進行檢查?

單點登錄

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

}

網關

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

有一個關於 JWT 的問題。 它被稱為“注銷問題”。 首先你需要了解它是什么。

然后,檢查TokenRelay過濾器(TokenRelayGatewayFilterFactory),它負責將授權頭傳遞給下游。

如果您查看該過濾器,您將看到 JWT 存儲在 ConcurrentHashMap (InMemoryReactiveOAuth2AuthorizedClientService) 中。 鍵是會話,值是 JWT。 因此,作為提供的響應,將返回 session-id 而不是 JWT 標頭

到這里為止,一切正常,但是當請求到達網關時,請求中沒有“授權”標頭,這意味着沒有 JWT 令牌。

是的 當請求到達網關時,TokenRelay 過濾器從請求中獲取 session-id 並從 ConcurrentHashMap 中找到 JWT,然后在下游傳遞給 Authorization 標頭。

可能這個流程是由 spring 安全團隊設計的,用於解決 JWT 注銷問題。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM