1.概述
本文重点介绍如何针对提供安全服务的安全REST API进行身份验证 -主要是RESTful用户帐户和身份验证服务。
2.目标
首先,让我们看一下参与者-典型的启用了Spring Security的应用程序需要针对某些事物进行身份验证-该事物可以是数据库,LDAP或可以是REST服务。 数据库是最常见的情况。 但是,RESTful UAA(用户帐户和身份验证)服务也可以正常工作。
就本文而言,REST UAA服务将在/ authentication上公开一个GET操作,该操作将返回 Spring Security执行完整身份验证过程所需的Principal信息 。
3.客户
通常,启用了Spring Security的简单应用程序将使用简单的用户服务作为身份验证源:
<authentication-manager alias="authenticationManager"><authentication-provider user-service-ref="customUserDetailsService" />
</authentication-manager>
这将实现org.springframework.security.core.userdetails.UserDetailsService并将基于提供的用户名 返回Principal :
@Component
public class CustomUserDetailsService implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String username) { ...}
}
当客户端通过RESTful UAA服务进行身份验证时,仅使用用户名 已不再足够 -客户端现在将身份验证请求发送到服务时,需要完整的凭据(包括用户名和密码) 。 由于服务本身是安全的,因此这很有意义,因此请求本身必须包含身份验证凭据才能正确处理。
从Spring Security的角度来看,不能在loadUserByUsername内完成此操作,因为此时密码不再可用-我们需要尽快控制身份验证过程。
我们可以通过向Spring Security提供完整的身份验证提供程序来做到这一点:
<authentication-manager alias="authenticationManager"><authentication-provider ref="restAuthenticationProvider" />
</authentication-manager>
覆盖整个身份验证提供程序使我们有更多的自由来执行从服务中自定义的委托人检索,但是确实带来了相当大的复杂性。 标准身份验证提供程序– DaoAuthenticationProvider –满足了我们的大部分需求,因此一种好的方法是简单地扩展它并仅修改必要的内容。
不幸的是,这是不可能的,因为retrieveUser (我们希望扩展的方法)是final 。 这有点不直观(有一个JIRA讨论了这个问题 )–看起来这里的设计意图仅仅是提供一个不理想的替代实现,但也不是主要问题–我们的RestAuthenticationProvider复制并粘贴了大多数实现DaoAuthenticationProvider并重写所需的内容–从服务中检索主体:
@Override
protected UserDetails retrieveUser(String name, UsernamePasswordAuthenticationToken auth){String password = auth.getCredentials().toString();UserDetails loadedUser = null;try {ResponseEntity<Principal> authenticationResponse = authenticationApi.authenticate(name, password);if (authenticationResponse.getStatusCode().value() == 401) {return new User("wrongUsername", "wrongPass", Lists.<GrantedAuthority> newArrayList());}Principal principalFromRest = authenticationResponse.getBody();Set<String> privilegesFromRest = Sets.newHashSet(); // fill in the privilegesFromRest from the PrincipalString[] authoritiesAsArray = privilegesFromRest.toArray(new String[privilegesFromRest.size()]);List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(authoritiesAsArray);loadedUser = new User(name, password, true, authorities);} catch (Exception ex) {throw new AuthenticationServiceException(repositoryProblem.getMessage(), ex);}return loadedUser;
}
让我们从头开始 -与REST服务的HTTP通信-这由authenticationApi处理, authenticationApi是提供实际服务的身份验证操作的简单API。 可以使用任何支持HTTP的库来实现操作本身–在这种情况下,实现是使用RestTemplate进行的 :
public ResponseEntity<Principal> authenticate(String username, String pass) {HttpEntity<Principal> entity = new HttpEntity<Principal>(createHeaders(username, pass))return restTemplate.exchange(authenticationUri, HttpMethod.GET, entity, Principal.class);
}HttpHeaders createHeaders(String email, String password) {HttpHeaders acceptHeaders = new HttpHeaders() {{set(com.google.common.net.HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON.toString());}};String authorization = username + ":" + password;String basic = new String(Base64.encodeBase64(authorization.getBytes(Charset.forName("US-ASCII"))));acceptHeaders.set("Authorization", "Basic " + basic);return acceptHeaders;
}
FactoryBean可用于在上下文中设置RestTemplate 。
接下来 ,如果身份验证请求导致HTTP 401 Unauthorized ,很可能是由于来自客户端的不正确凭据,则返回具有错误凭据的主体,以便Spring Security身份验证过程可以拒绝它们:
return new User("wrongUsername", "wrongPass", Lists.<GrantedAuthority> newArrayList());
最后,Spring Security Principal需要一些权限-特定主体在身份验证过程后将拥有并在本地使用的特权。 / authenticate操作已经检索了包括特权的完整主体,因此需要按照Spring Security的要求从请求的结果中提取这些特权并将其转换为GrantedAuthority对象。
这些特权的存储方式在这里无关紧要-它们可以存储为简单的String或复杂的Role-Privilege结构-但无论细节如何,我们只需要使用它们的名称来构造GrantedAuthoritiy对象。 创建最终的Spring Security主体后,将其返回到标准身份验证过程:
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(authoritiesAsArray);
loadedUser = new User(name, password, true, authorities);
4.测试身份验证服务
编写一个在幸福路径上使用身份验证REST服务的集成测试非常简单:
@Test
public void whenAuthenticating_then200IsReceived() {// WhenResponseEntity<Principal> response = authenticationRestTemplate.authenticate("admin", "adminPass");// ThenassertThat(response.getStatusCode().value(), is(200));
}
完成此简单测试之后,也可以实施更复杂的集成测试-但这不在本文讨论范围之内。
5.结论
本文介绍了如何针对REST服务进行身份验证,而不是针对本地系统(如数据库)进行身份验证。 有关可以用作身份验证提供程序的安全RESTful服务的完整实现,请查看github项目 。
翻译自: https://www.javacodegeeks.com/2012/12/authentication-against-a-restful-service-with-spring-security.html