Spring Boot测试:springsecurityfilterchain上没有显示自定义身份验证提供程序的Spring Security

时间:2014-06-11 01:52:33

标签: spring-security spring-boot spring-java-config

我对身份验证有相当特殊的要求(作为用户名,密码和设备或只是要输入的设备)。这让我得出结论,通常的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;

覆盖了吗?有谁知道为什么?

2 个答案:

答案 0 :(得分:1)

你有3个过滤链(据我们所知)。一个默认值(外部WebConfigurerAdapter)和2个自定义值(一个具有显式httpBasic(),另一个具有formLogin())。我认为默认值为order = 0,并保护所有内容,因此如果您将请求发送到整个过滤器,那么您将遇到的那个。所以这可能是一个问题。并且没有一个(据我所知)安装您的设备详细信息过滤器,因此您添加的身份验证提供程序将永远不会被使用。那是另一个问题。

答案 1 :(得分:0)

最终,事实证明,如果要覆盖UsernamePasswordAuthenticationFilterProviderToken,则需要返回XML配置。似乎 Spring Security 没有正确地将重写的vanilla spring SecurityFilterChain作为bean发布,所以无论你尝试配置什么,这都是你得到的vanilla版本。

也许当 Spring Security 转到版本4.0.0时,我们可以通过 Java 配置来完成此任务。