spring-security 嵌入式Tomcat的Sping Boot 身份验证成功,但Open/WAS Liberty返回403

jdgnovmf  于 2022-11-11  发布在  Spring
关注(0)|答案(1)|浏览(156)

我使用Spring Security对Active Directory进行身份验证/授权。如果我在Spring Embedded Tomcat中运行以下代码,它就可以正常工作。
但是当我切换到Open/WAS Liberty服务器时,我在身份验证(/auth endpoint)上得到403:
我的WebSecurityConfiguration类如下所示:

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Value("${active.dir.domain}")
    private String domain;

    @Value("${active.dir.url}")
    private String url;

    @Value("${active.dir.userDnPattern}")
    private String userDnPattern;

    private final Environment environment;

    public WebSecurityConfiguration(Environment environment) {
        this.environment = environment;
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors(Customizer.withDefaults())
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/auth").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .addFilter(getAuthenticationFilter())
                .addFilter(new AuthorizationFilter(authenticationManager()))
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); 
    }

    @Bean
    public AuthenticationProvider activeDirectoryAuthenticationProvider() {

        String adSearchFilter = "(&(sAMAccountName={1})(objectClass=user))";

        ActiveDirectoryLdapAuthenticationProvider ad = new ActiveDirectoryLdapAuthenticationProvider(domain, url, userDnPattern);
        ad.setConvertSubErrorCodesToExceptions(true);
        ad.setUseAuthenticationRequestCredentials(true);
        ad.setSearchFilter(adSearchFilter);

        return ad;
    }

    //CORS configuration source
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {

        final CorsConfiguration configuration = new CorsConfiguration();

        configuration.setAllowedOrigins(Arrays.asList("http://some.url"));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        configuration.setAllowedHeaders(Arrays.asList("*"));

        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }

    //Customize the Spring default /login url to overwrite it with /auth.
    private AuthenticationFilter getAuthenticationFilter() throws Exception {
        final AuthenticationFilter filter = new AuthenticationFilter(authenticationManager());
        filter.setFilterProcessesUrl("/auth");
        return filter;
    }
}

下面是我的授权过滤器类:

public class AuthorizationFilter extends BasicAuthenticationFilter {

    public AuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

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

        String authorizationHeader = request.getHeader("Authorization");
        if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return;
        }

        UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(request, response);
    }

    //Extracts username from Jwt token
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {

        String token = request.getHeader("Authorization");
        if (token != null) {
            token = token.replace("Bearer ", "");

            String username = Jwts.parser()
                    .setSigningKey("somesecret")
                    .parseClaimsJws(token)
                    .getBody()
                    .getSubject();

            if (username != null) {
                return new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
            }
        }

        return null;
    }
}

下面是我的身份验证过滤器类:

public class AuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;

    public AuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

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

        UserLoginRequestModel userLoginRequestModel = extractCredentials(request);
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                userLoginRequestModel.getUsername()
                , userLoginRequestModel.getPassword()
                , new ArrayList<>());

        return authenticationManager.authenticate(token);
    }

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

        String userId = ((UserDetails)auth.getPrincipal()).getUsername(); 
        Instant now = Instant.now();

        String jwtToken = Jwts.builder()
                .setSubject(userId)
                .setIssuer("me")
                .setAudience("myapp")
                .setId(UUID.randomUUID().toString())
                .setIssuedAt(Date.from(now))
                .setExpiration(Date.from(now.plus(30000)))
                .signWith(SignatureAlgorithm.HS512, SecurityConstants.getTokenSecret())
                .compact();

        response.addHeader("Authorization", "Bearer " + jwtToken);
        response.addHeader("Access-Control-Expose-Headers", accessControlHeaders.toString());
    }

    private UserLoginRequestModel extractCredentials(HttpServletRequest request) {

        UserLoginRequestModel userLoginRequestModel = new UserLoginRequestModel();
        String authorizationHeader = request.getHeader("Authorization");

        try {
            if (authorizationHeader != null && authorizationHeader.toLowerCase().startsWith("basic")) {
                String base64Credentials = authorizationHeader.substring("Basic".length()).trim();
                byte[] decodedCredentials = Base64.getDecoder().decode(base64Credentials);
                String headerCredentials = new String(decodedCredentials, StandardCharsets.UTF_8);
                final String[] credentialsValues = headerCredentials.split(":", 2);
                userLoginRequestModel.setUsername(credentialsValues[0]);
                userLoginRequestModel.setPassword(credentialsValues[1]);
            } else {
                userLoginRequestModel = new ObjectMapper().readValue(request.getInputStream(), UserLoginRequestModel.class);
            }

            return userLoginRequestModel;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

在 Postman ,我叫:

POST: http://localhost/myapi/v1/auth

我把用户名和密码传给它BasicAuth。
如果我在Open/WAS Liberty上运行,我会得到403 Forbidden。同样的代码,没有任何变化,在Spring附带的嵌入式Tomcat中运行良好,我得到200 OK

jpfvwuh4

jpfvwuh41#

我遇到这种情况的原因是在我的Liberty server.xml中,我缺少定义的context-path。看起来,Liberty并不考虑在application.properties文件中设置的context-path
下面是我的application.properties文件中的context-path。不幸的是,Liberty没有读取(或考虑)它,只是使用应用程序名称作为context-path,而不是使用application.propertiesapplication.yml文件中的设置:

server.servlet.context-path=/myapi/v1

因此,如果部署在Sping Boot 嵌入式Tomcat容器中,而不是部署在Liberty容器中,上面的context-path将工作得很好。
当您将其部署到OpenLiberty/WASLiberty时,您可能会发现您的端点将停止工作,并且您会收到403和/或404错误。
在上面的例子中,我有getAuthenticationFilter()方法,在我的WebSecurityConfiguration类中。下面,我添加了一些注解来解释它:

//Customize the /login url to overwrite the Spring default provided /login url.
private AuthenticationFilter getAuthenticationFilter() throws Exception {
    final AuthenticationFilter filter = new AuthenticationFilter(authenticationManager());
    // This works fine on embedded tomcat, but not in Liberty where it returns 403.  
    // To fix, in server.xml <appllication> block, add 
    // <application context-root="/myapi/v1" ... and then both
    // auth and other endpoints will work fine in Liberty.
    filter.setFilterProcessesUrl("/auth"); 
    // This is temporary "fix" that creates rather more issues, as it 
    // works fine with Tomcat but fails in Liberty and all other
    // endpoints still return 404
    //filter.setFilterProcessesUrl("/v1/auth"); 
    return filter;
}

基于上面的context-path,在Tomcat上,它变成了/myapi/v1/auth,而在Liberty上,它最终只是/myapi/auth,这是错误的。我认为Liberty所做的,它只会取API的名称,并向其添加端点,因此忽略了版本控制。
因此,AntPathRequestMatcher类的matches()方法将导致一个不匹配的/auth端点,您将得到403错误。而其他端点将导致404错误。

解决方案

1.在您的应用程序.属性中,保留:
服务器.servlet.上下文路径=/myapi/v1
,这将被嵌入式Tomcat拾取,您的应用程序将继续按预期工作。
1.在Open/WAS Liberty的server.xml配置中,将匹配的context-root添加到如下部分:

<application context-root="/myapi/v1" id="myapi" location="location\of\your\myapi-0.0.1.war" name="myapi" type="war">

,这将由Open/WASLiberty拾取,您的应用程序也将继续在Liberty容器上按预期工作。

相关问题