领先一步
VMware 提供培训和认证,以助您快速进步。
了解更多Spring Security 以其高度可定制性而闻名,因此,在我第一次尝试使用 Google App Engine 时,我决定创建一个简单的应用程序,该应用程序将通过实现一些核心 Spring Security 接口来探索 GAE 功能的使用。在本文中,我们将看到如何
您应该已经熟悉将应用程序部署到 GAE。启动和运行基本应用程序不需要很长时间,您可以在 GAE 网站 上找到许多关于此方面的指南。
注册的用户存储为 GAE 数据存储区实体。在首次身份验证时,新用户将重定向到注册页面,他们可以在其中输入自己的姓名。注册后,可以将用户帐户标记为数据存储区中的“已禁用”,即使用户已通过 GAE 进行身份验证,也不允许他们使用该应用程序。
过滤器将身份验证决策委托给AuthenticationManager,后者配置了一个AuthenticationProviderbean 列表,这些 bean 中的任何一个都可以对用户进行身份验证,或者如果身份验证失败则引发异常。
在基于表单的登录的情况下,该AuthenticationEntryPoint只是将用户重定向到登录页面。身份验证过滤器(在本例中为UsernamePasswordAuthenticationFilter)从提交的 POST 请求中提取用户名和密码。它们存储在Authentication对象中并传递给AuthenticationProvider,后者通常会将用户的密码与存储在数据库或 LDAP 服务器中的密码进行比较。
这是组件之间基本交互。这如何应用于 GAE 应用程序?
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
public class GoogleAccountsAuthenticationEntryPoint implements AuthenticationEntryPoint {
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
UserService userService = UserServiceFactory.getUserService();
response.sendRedirect(userService.createLoginURL(request.getRequestURI()));
}
}
中直接重定向到它。如果将此添加到我们的配置中,使用 Spring Security 命名空间为此目的提供的特定挂钩,我们将得到如下内容
<b:beans xmlns="http://www.springframework.org/schema/security"
xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd">
<http use-expressions="true" entry-point-ref="gaeEntryPoint">
<intercept-url pattern="/" access="permitAll" />
<intercept-url pattern="/**" access="hasRole('USER')" />
</http>
<b:bean id="gaeEntryPoint" class="samples.gae.security.GoogleAccountsAuthenticationEntryPoint" />
...
</b:beans>
在这里,我们已将所有 URL 配置为需要“USER”角色,除了 Web 应用程序根目录。当用户首次尝试访问任何其他页面时,他们将被重定向到 Google 帐户登录屏幕
现在我们需要添加过滤器 bean,当用户被 GAE 重定向回我们的站点并登录到 Google 帐户时,该过滤器将设置安全上下文。这是身份验证过滤器代码
public class GaeAuthenticationFilter extends GenericFilterBean {
private static final String REGISTRATION_URL = "/register.htm";
private AuthenticationDetailsSource ads = new WebAuthenticationDetailsSource();
private AuthenticationManager authenticationManager;
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
// User isn't authenticated. Check if there is a Google Accounts user
User googleUser = UserServiceFactory.getUserService().getCurrentUser();
if (googleUser != null) {
// User has returned after authenticating through GAE. Need to authenticate to Spring Security.
PreAuthenticatedAuthenticationToken token = new PreAuthenticatedAuthenticationToken(googleUser, null);
token.setDetails(ads.buildDetails(request));
try {
authentication = authenticationManager.authenticate(token);
// Setup the security context
SecurityContextHolder.getContext().setAuthentication(authentication);
// Send new users to the registration page.
if (authentication.getAuthorities().contains(AppRole.NEW_USER)) {
((HttpServletResponse) response).sendRedirect(REGISTRATION_URL);
return;
}
} catch (AuthenticationException e) {
// Authentication information was rejected by the authentication manager
failureHandler.onAuthenticationFailure((HttpServletRequest)request, (HttpServletResponse)response, e);
return;
}
}
}
chain.doFilter(request, response);
}
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
this.failureHandler = failureHandler;
}
}
我们从头开始实现了过滤器,使其更容易理解,并避免了继承现有类的复杂性。如果用户当前未经身份验证(从 Spring Security 的角度来看),则过滤器检查 GAE 用户是否存在(再次利用 GAE 的UserService)。如果找到用户,则将其打包到合适的身份验证令牌对象中(此处出于方便起见使用了 Spring Security 的PreAuthenticatedAuthenticationToken)并将其传递给AuthenticationManager以由 Spring Security 进行身份验证。此时,新用户将被重定向到注册页面。
该AuthenticationProvider实现与UserRegistry交互以存储和检索GaeUser对象(两者特定于此示例)
public interface UserRegistry {
GaeUser findUser(String userId);
void registerUser(GaeUser newUser);
void removeUser(String userId);
}
public class GaeUser implements Serializable {
private final String userId;
private final String email;
private final String nickname;
private final String forename;
private final String surname;
private final Set<AppRole> authorities;
private final boolean enabled;
// Constructors and accessors omitted
...
该userId是 Google 帐户分配的唯一 ID。电子邮件和昵称也从 GAE 用户那里获取。名字和姓氏在注册表单中输入。除非通过 GAE 数据存储区管理控制台直接修改,否则启用标志设置为“true”。AppRole是 Spring Security 的GrantedAuthority的实现,作为枚举
public enum AppRole implements GrantedAuthority {
ADMIN (0),
NEW_USER (1),
USER (2);
private int bit;
AppRole(int bit) {
this.bit = bit;
}
public String getAuthority() {
return toString();
}
}
角色如上所述分配。然后AuthenticationProvider如下所示
public class GoogleAccountsAuthenticationProvider implements AuthenticationProvider {
private UserRegistry userRegistry;
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
User googleUser = (User) authentication.getPrincipal();
GaeUser user = userRegistry.findUser(googleUser.getUserId());
if (user == null) {
// User not in registry. Needs to register
user = new GaeUser(googleUser.getUserId(), googleUser.getNickname(), googleUser.getEmail());
}
if (!user.isEnabled()) {
throw new DisabledException("Account is disabled");
}
return new GaeUserAuthentication(user, authentication.getDetails());
}
public final boolean supports(Class<?> authentication) {
return PreAuthenticatedAuthenticationToken.class.isAssignableFrom(authentication);
}
public void setUserRegistry(UserRegistry userRegistry) {
this.userRegistry = userRegistry;
}
}
该GaeUserAuthentication类是 Spring Security 的Authentication接口的一个非常简单的实现,它将GaeUser对象作为主体。如果您以前自定义过 Spring Security,您可能想知道为什么我们没有在任何地方实现UserDetailsService以及为什么主体不是UserDetails实例。简单的答案是您不必这样做——Spring Security 通常不关心对象类型的具体内容,并且在这里我们选择直接实现AuthenticationProvider接口作为最简单的选项。
import com.google.appengine.api.datastore.*;
import org.springframework.security.core.GrantedAuthority;
import samples.gae.security.AppRole;
import java.util.*;
public class GaeDatastoreUserRegistry implements UserRegistry {
private static final String USER_TYPE = "GaeUser";
private static final String USER_FORENAME = "forename";
private static final String USER_SURNAME = "surname";
private static final String USER_NICKNAME = "nickname";
private static final String USER_EMAIL = "email";
private static final String USER_ENABLED = "enabled";
private static final String USER_AUTHORITIES = "authorities";
public GaeUser findUser(String userId) {
Key key = KeyFactory.createKey(USER_TYPE, userId);
DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
try {
Entity user = datastore.get(key);
long binaryAuthorities = (Long)user.getProperty(USER_AUTHORITIES);
Set<AppRole> roles = EnumSet.noneOf(AppRole.class);
for (AppRole r : AppRole.values()) {
if ((binaryAuthorities & (1 << r.getBit())) != 0) {
roles.add(r);
}
}
GaeUser gaeUser = new GaeUser(
user.getKey().getName(),
(String)user.getProperty(USER_NICKNAME),
(String)user.getProperty(USER_EMAIL),
(String)user.getProperty(USER_FORENAME),
(String)user.getProperty(USER_SURNAME),
roles,
(Boolean)user.getProperty(USER_ENABLED));
return gaeUser;
} catch (EntityNotFoundException e) {
logger.debug(userId + " not found in datastore");
return null;
}
}
public void registerUser(GaeUser newUser) {
Key key = KeyFactory.createKey(USER_TYPE, newUser.getUserId());
Entity user = new Entity(key);
user.setProperty(USER_EMAIL, newUser.getEmail());
user.setProperty(USER_NICKNAME, newUser.getNickname());
user.setProperty(USER_FORENAME, newUser.getForename());
user.setProperty(USER_SURNAME, newUser.getSurname());
user.setUnindexedProperty(USER_ENABLED, newUser.isEnabled());
Collection<? extends GrantedAuthority> roles = newUser.getAuthorities();
long binaryAuthorities = 0;
for (GrantedAuthority r : roles) {
binaryAuthorities |= 1 << ((AppRole)r).getBit();
}
user.setUnindexedProperty(USER_AUTHORITIES, binaryAuthorities);
DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
datastore.put(user);
}
public void removeUser(String userId) {
DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Key key = KeyFactory.createKey(USER_TYPE, userId);
datastore.delete(key);
}
}
如前所述,该示例使用枚举表示应用程序角色。分配给用户的角色(权限)存储为EnumSet. EnumSet非常节省资源,用户的角色可以存储为单个long值,从而可以更简单地与数据存储区 API 交互。为此,我们为每个角色分配了一个单独的“位”属性。
用户注册控制器包含以下方法,该方法处理注册表单的提交。
@Autowired
private UserRegistry registry;
@RequestMapping(method = RequestMethod.POST)
public String register(@Valid RegistrationForm form, BindingResult result) {
if (result.hasErrors()) {
return null;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
GaeUser currentUser = (GaeUser)authentication.getPrincipal();
Set<AppRole> roles = EnumSet.of(AppRole.USER);
if (UserServiceFactory.getUserService().isUserAdmin()) {
roles.add(AppRole.ADMIN);
}
GaeUser user = new GaeUser(currentUser.getUserId(), currentUser.getNickname(), currentUser.getEmail(),
form.getForename(), form.getSurname(), roles, true);
registry.registerUser(user);
// Update the context with the full authentication
SecurityContextHolder.getContext().setAuthentication(new GaeUserAuthentication(user, authentication.getDetails()));
return "redirect:/home.htm";
}
使用提供的名字和姓氏创建用户,并创建一组新的角色。如果 GAE 指示当前用户是应用程序的管理员,这也可能包括“ADMIN”角色。然后将其存储在用户注册表中,并使用更新的Authentication对象填充安全上下文,以确保 Spring Security 了解新的角色信息并相应地应用其访问控制约束。
安全应用程序上下文现在如下所示
<http use-expressions="true" entry-point-ref="gaeEntryPoint">
<intercept-url pattern="/" access="permitAll" />
<intercept-url pattern="/register.htm*" access="hasRole('NEW_USER')" />
<intercept-url pattern="/**" access="hasRole('USER')" />
<custom-filter position="PRE_AUTH_FILTER" ref="gaeFilter" />
</http>
<b:bean id="gaeEntryPoint" class="samples.gae.security.GoogleAccountsAuthenticationEntryPoint" />
<b:bean id="gaeFilter" class="samples.gae.security.GaeAuthenticationFilter">
<b:property name="authenticationManager" ref="authenticationManager"/>
</b:bean>
<authentication-manager alias="authenticationManager">
<authentication-provider ref="gaeAuthenticationProvider"/>
</authentication-manager>
<b:bean id="gaeAuthenticationProvider" class="samples.gae.security.GoogleAccountsAuthenticationProvider">
<b:property name="userRegistry" ref="userRegistry" />
</b:bean>
<b:bean id="userRegistry" class="samples.gae.users.GaeDatastoreUserRegistry" />
您可以看到我们使用custom-filter命名空间元素插入了我们的过滤器,声明了提供程序和用户注册表,并将它们全部连接起来。我们还添加了注册控制器的 URL,新用户可以访问该 URL。
Spring Security 多年来一直证明它足够灵活,可以在许多不同的场景中增加价值,在 Google App Engine 中的部署也不例外。还值得记住的是,自己实现一些接口(就像我们在这里所做的那样)通常比尝试使用不完全适合的现有类要好。您最终可能会获得一个更简洁的解决方案,该解决方案更符合您的需求。
这里的重点是如何在启用了 Spring Security 的应用程序中使用 Google App Engine API。我们没有介绍应用程序工作方式的其他所有细节,但我鼓励您查看代码并亲自了解。如果您是 GAE 专家,那么我们随时欢迎您提出改进建议!
示例代码已在 3.1 代码库中,因此您可以从 我们的 Git 存储库 中检出它。Spring Security 3.1 的第一个里程碑也应该在本月晚些时候发布。