領先一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多Spring Security 以其高度可定製性而聞名,因此在我第一次嘗試使用 Google App Engine 時,我決定建立一個簡單的應用程式,透過實現一些核心 Spring Security 介面來探索 GAE 功能的使用。本文我們將瞭解如何:
您應該已經熟悉將應用程式部署到 GAE。啟動和執行一個基本應用程式不需要很長時間,您將在 GAE 網站上找到大量相關指導。
註冊使用者儲存為GAE資料儲存實體。首次認證時,新使用者會被重定向到註冊頁面,在那裡他們可以輸入自己的姓名。一旦註冊,使用者賬戶可以在資料儲存中被標記為“停用”,使用者將不允許使用該應用程式,即使他們已經透過GAE認證。
過濾器將認證決策委託給AuthenticationManager,它配置了一個AuthenticationProviderbean 列表,其中任何一個都可以認證使用者,或者在認證失敗時丟擲異常。
在基於表單的登入情況下,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 賬戶登入螢幕。

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. EnumSets 非常節省資源,使用者的角色可以儲存為單個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 的第一個里程碑版本也應該在本月晚些時候釋出。