Spring Boot Test: Spring Security с настраиваемым поставщиком аутентификации не отображается в springsecurityfilterchain

У меня довольно особые требования к аутентификации (имя пользователя, пароль и устройство или просто устройство для входа). Это заставило меня сделать вывод, что обычный UsernamePasswordAuthenticationFilter не будет работать, поэтому я настроил свой собственный фильтр, провайдера и токен, которые показаны ниже. Сначала провайдер:

@Service(value="customAuthenticationProvider")
public class DeviceUsernamePasswordAuthenticationProvider implements AuthenticationProvider {
    private static final Logger LOG = LoggerFactory.getLogger(DeviceUsernamePasswordAuthenticationProvider.class);

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Autowired
    private DeviceDetailsService deviceDetailsService;

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

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

        LOG.info("Authenticating device and user - assigning authorities...");
        DeviceUsernamePasswordAuthenticationToken auth = (DeviceUsernamePasswordAuthenticationToken) authentication;
        String name = auth.getName();
        String password = auth.getCredentials().toString();

        boolean isDeviceRequest = (name == null && password == null);
        LOG.debug("name is {}, password is {}", name, password);

        // (a) nothing, (b) hasToken|<token encoding>, or (c) getToken|<base64 encoded device request>
        String deviceToken = auth.getDeviceAuthorisation();

        if (deviceToken == null) {
            // very bad - set as anonymous
            LOG.error("missing.device.token");
            throw new BadCredentialsException("missing.device.token");
        }

        LOG.debug("deviceToken is {}", deviceToken);
        String[] deviceInformation = StringUtils.split(deviceToken,"|");


        DeviceDetails device = null;

        if(deviceInformation[0].equals("getToken")) {
            LOG.debug("getToken");
            // we expect the array to be of length 3, if not, the request is malformed
            if (deviceInformation.length < 3) {
                LOG.error("malformed.device.token");
                throw new BadCredentialsException("malformed.device.token");
            }

            device = deviceDetailsService.loadDeviceByDeviceId(deviceInformation[1]);

            if (device == null) {
                LOG.error("missing.device");
                throw new BadCredentialsException("missing.device");
            } else {
                // otherwise, get the authorities
                auth = new DeviceUsernamePasswordAuthenticationToken(null, null,
                        device.getDeviceId(), device.getAuthorities());

                //also we need to set a new token into the database

                String newToken = Hashing.sha256()
                        .hashString("your input", Charsets.UTF_8)
                        .toString();


                deviceDetailsService.setToken(device.getDeviceId(),newToken);

                // and put it into the response headers
                auth.setDeviceTokenForHeaders(newToken);

            }
        } else if(deviceInformation[0].equals("hasToken")) {
            LOG.debug("hasToken");
            if (deviceInformation.length < 3) {
                LOG.error("malformed.device.token");
                throw new BadCredentialsException("malformed.device.token");
            }

            // check that there is a token and that the token has not expired
            String token = deviceDetailsService.getToken(deviceInformation[1]);

            if (token == null) {
                // we got a token in the request but the token we have no stored token
                LOG.error("mismatched.device.token");
                throw new BadCredentialsException("mismatched.device.token");
            } else if(!token.equals(deviceInformation[2])) {
                // we got a token in the request and its not the same as the token we have stored
                LOG.error("mismatched.device.token");
                throw new BadCredentialsException("mismatched.device.token");
            } else if ( deviceDetailsService.hasTokenExpired(deviceInformation[1])) {
                // we got a token in the request and its not the same as the token we have stored
                LOG.error("expired.device.token");
                throw new BadCredentialsException("expired.device.token");
            } else {
                // token was in the request, correctly formed, and matches out records
                device = deviceDetailsService.loadDeviceByDeviceId(deviceInformation[1]);
                auth = new DeviceUsernamePasswordAuthenticationToken(null, null,
                        device.getDeviceId(), device.getAuthorities());
            }


        } else {
            LOG.error("malformed.device.token");
            throw new BadCredentialsException("malformed.device.token");
        }

        if (!isDeviceRequest) {

            UserDetails user = customUserDetailsService.loadUserByUsername(name);
            auth = new DeviceUsernamePasswordAuthenticationToken(name, password, device.getDeviceId(), device.getAuthorities());
        }

        return auth;
    }

}

жетон:

public class DeviceUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
    private String deviceAuthorisation;
    private String deviceTokenForHeaders;

    public DeviceUsernamePasswordAuthenticationToken(Object principal, Object credentials, String deviceAuthorisation) {
        super(principal, credentials);
        this.deviceAuthorisation = deviceAuthorisation;
    }

    public DeviceUsernamePasswordAuthenticationToken(Object principal, Object credentials, String deviceAuthorisation, List<GrantedAuthority> authorities) {
        super(principal, credentials, authorities);
        this.deviceAuthorisation = deviceAuthorisation;
    }

    public String getDeviceAuthorisation() {
        return deviceAuthorisation;
    }


    public void setDeviceAuthorisation(String deviceAuthorisation) {
        this.deviceAuthorisation = deviceAuthorisation;
    }

    public String getDeviceTokenForHeaders() {
        return deviceTokenForHeaders;
    }

    public void setDeviceTokenForHeaders(String deviceTokenForHeaders) {
        this.deviceTokenForHeaders = deviceTokenForHeaders;
    }

    @Override
    public String toString() {
        return "DeviceUsernamePasswordAuthenticationToken{" +
                "deviceAuthorisation='" + deviceAuthorisation + '\'' +
                '}';
    }
}

и фильтр:

public class DeviceUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    public static final String SPRING_SECURITY_FORM_DEVICE_KEY = "device";
    private String deviceParameter = SPRING_SECURITY_FORM_DEVICE_KEY;
    private boolean postOnly = true;

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

        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }

        String username = obtainUsername(request);
        String password = obtainPassword(request);
        String device = obtainDevice(request);

        if(username != null) {
            username = username.trim();
        }

        DeviceUsernamePasswordAuthenticationToken authRequest = new DeviceUsernamePasswordAuthenticationToken(username, password, device);


        // TODO: check an see if I need to do any additional work here.
        setDetails(request, authRequest);

        response.addHeader("X-AUTH-TOKEN", authRequest.getDeviceTokenForHeaders());

        return this.getAuthenticationManager().authenticate(authRequest);
    }

    protected String obtainDevice(HttpServletRequest request) {

        String token = "hasToken|" + request.getHeader("X-AUTH-TOKEN");

        if(token == null) {
            String deviceInformation = request.getParameter(deviceParameter);
            if(deviceInformation != null) {
                token = "getToken|" + StringUtils.newStringUtf8(
                        Base64.decodeBase64(deviceInformation));
            }
        }
        return token;
    }
}

Теперь у меня есть конфигурация безопасности, которая выглядит так:

@Configuration
@EnableWebMvcSecurity
@ComponentScan({
        "com.xxxxxcorp.xxxxxpoint.security",
        "com.xxxxxcorp.xxxxxpoint.service",
        "com.xxxxxcorp.xxxxxpoint.model.dao"})
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Autowired
    DeviceUsernamePasswordAuthenticationProvider customAuthenticationProvider;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        System.out.println( "we are getting the custom config right?" );

        auth
                .authenticationProvider(customAuthenticationProvider);
    }

    @Configuration
    @Order(1)
    public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
        protected void configure(HttpSecurity http) throws Exception {
            http
                .antMatcher("/api/**")
                    .authorizeRequests()
                .anyRequest().hasRole("ADMIN")
                    .and()
                    .httpBasic();
        }
    }

    @Order(2)
    @Configuration
    public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .csrf().disable()
                .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                .loginPage("/login")
                    .failureUrl("/login?error=1")
                    .permitAll()
                    .and()
                .logout()
                    .logoutUrl("/logout")
                    .logoutSuccessUrl("/");
        }
    } 
}

и, наконец, тестовый контекст (обратите внимание на автопроводку springSecurityFilterChain)

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {TestApplicationConfig.class,TestPersistenceConfig.class,MvcConfig.class,SecurityConfig.class},loader=AnnotationConfigWebContextLoader.class)
@WebAppConfiguration
@Transactional
public class ApplicationIntegrationTest {

    MockMvc mockMvc;

    @Autowired
    private WebApplicationContext wac;

    @Autowired
    private FilterChainProxy springSecurityFilterChain;

    @Autowired
    private UserDao userDao;

    @Autowired
    private ClientDao clientDao;

    @Autowired
    private RoleDao roleDao;


    UUID key = UUID.fromString("f3512d26-72f6-4290-9265-63ad69eccc13");


    @Before
    public void setup() {

        mockMvc = MockMvcBuilders.webAppContextSetup(wac).addFilter(springSecurityFilterChain).build();


        List<Client> clients = new ArrayList<Client>();

        List<Role> roles = new ArrayList<Role>();
        Role roleUser = new Role();
        roleUser.setRole("user");
        Role roleUserDomain = roleDao.save(roleUser);
        roles.add(roleUserDomain);

        Role roleAdmin = new Role();
        roleAdmin.setRole("admin");
        Role roleAdminDomain = roleDao.save(roleAdmin);
        roles.add(roleAdminDomain);

        Client clientEN = new Client();
        clientEN.setDeviceId("444444444");
        clientEN.setLanguage("en-EN");
        clientEN.setAgentId("444444444|68:5b:35:8a:7c:d0");
        clientEN.setRoles(roles);
        Client clientENDomain = clientDao.save(clientEN);
        clients.add(clientENDomain);

        User user = new User();
        user.setLogin("user");
        user.setPassword("password");
        user.setClients(clients);
        user.setRoles(roles);

        userDao.save(user);

    }

    @Test
    public void thatViewBootstrapUsesHttpNotFound() throws Exception {

        MvcResult result = mockMvc.perform(post("/login")
                .param("username", "user").param("password", "password")
                .header("X-AUTH-TOKEN","NDQ0NDQ0NDQ0fDY4OjViOjM1OjhhOjdjOmQw")).andReturn();
        Cookie c = result.getResponse().getCookie("my-cookie");

        Cookie[] cookies = result.getResponse().getCookies();
        for (int i = 0; i < cookies.length; i++) {
            System.out.println("cookie " + i + " name: " + cookies[i].getName());
            System.out.println("cookie " + i + " value: " + cookies[i].getValue());
        }
        //assertThat(c.getValue().length(), greaterThan(10));

        // No cookie; 401 Unauthorized
        mockMvc.perform(get("/")).andExpect(status().isUnauthorized());

        // With cookie; 200 OK
        mockMvc.perform(get("/").cookie(c)).andExpect(status().isOk());

        // Logout, and ensure we're told to wipe the cookie
        result = mockMvc.perform(delete("/session")).andReturn();
        c = result.getResponse().getCookie("my-cookie");
        assertThat(c.getValue().length(), is(0));
    }

}

В основном происходит то, что запрос на вход перехватывается обычным фильтром UsernamePasswordAuthenticationFilter, а не моей пользовательской аутентификацией. Я бы подумал, что SecurityConfig обеспечит правильные замены, но кажется, что использование:

@Autowired
private FilterChainProxy springSecurityFilterChain;

переопределяет это? Кто-нибудь знает, почему?


person Michael Coxon    schedule 11.06.2014    source источник
comment
Нужно ли мне повторно реализовать что-то вроде: @Bean public FilterChainProxy springSecurityFilterChain() throws Exception { // все, что он теперь делает, минус пароль пользователя и плюс мой новый провайдер }   -  person Michael Coxon    schedule 11.06.2014


Ответы (2)


У вас есть 3 цепочки фильтров (насколько мы можем судить). Один по умолчанию (внешний WebConfigurerAdapter) и 2 пользовательских (у одного явный httpBasic(), а у другого formLogin()). У стандартного порядка=0 я думаю, и защищает все, так что это тот, который вы встретите, если вы отправите запрос на весь фильтр. Так что, наверное, проблема. И ни один из них (насколько я вижу) не устанавливает фильтр сведений о вашем устройстве, поэтому добавленный вами поставщик аутентификации никогда не будет использоваться. Это еще одна проблема.

person Dave Syer    schedule 11.06.2014
comment
О, ОК, так вы говорите, что, перейдя: открытый класс SecurityConfig расширяет WebSecurityConfigurerAdapter, я устанавливаю цепочку фильтров по умолчанию (порядок (0))? Значит, не расширение WebSecurityConfigurerAdapter может помочь? Также вы сказали, что ни один из них не установил фильтр сведений об устройстве. Я думал, что auth.authenticationProvider(customAuthenticationProvider); устанавливал его, или, по крайней мере, это то, о чем мне говорили различные фрагменты документации. Если нет, то как мне это сделать? - person Michael Coxon; 11.06.2014
comment
Да, отсутствие расширения WebSecurityConfigurerAdapter может помочь, если вам не нужна цепочка по умолчанию. И нет, добавление поставщика аутентификации не устанавливает фильтр (два разных объекта, работающих вместе, но оба должны быть установлены). Посмотрите на методы addFilter() в HttpSecurity. Возможно, документация Spring Security поможет вам понять основные шаги. - person Dave Syer; 11.06.2014
comment
Да, я смотрел на это, к сожалению, весенний документ по безопасности немного не показывает, как это сделать. Если я просто добавлю фильтр, то в итоге у меня будет два фильтра аутентификации, потому что исходный все еще там. У меня была мысль добавить все фильтры в текущую цепочку за вычетом usernamepasswordauthfilter, но это показалось трудоемким и, вероятно, неправильным. Это может быть случай возврата к конфигурации xml, потому что в этом режиме это было относительно легко. Кстати, спасибо за помощь. - person Michael Coxon; 11.06.2014

В конечном итоге получается, что если вы переопределяете UsernamePasswordAuthenticationFilter, Provider и Token, вам нужно вернуться к конфигурации XML. Похоже, что Spring Security неправильно публикует переопределенную ванильную пружину SecurityFilterChain в виде bean-компонента, поэтому вы получаете ванильную версию обратно независимо от того, что вы пытаетесь настроить.

Возможно, когда Spring Security перейдет на версию 4.0.0, мы сможем сделать это с помощью Java Configuration.

person Michael Coxon    schedule 15.06.2014