Spring Security原理學習--許可權校驗(四)
在上一篇部落格Spring Security原理學習--使用者名稱和密碼認證(三)中我們已經瞭解到Spring Security關於使用者名稱和密碼在UsernamePasswordAuthenticationFilter中的認證處理邏輯,接下來我們看看許可權的校驗處理。
Spring Security許可權角色的校驗處理是在FilterSecurityInterceptor過濾器中進行處理的,在invoke方法中,呼叫父類的beforeInvocation方法對請求進行許可權角色校驗。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); }
在invoke方法中會呼叫父類AbstractSecurityInterceptor的beforeInvocation對Authentication物件進行身份校驗。
public void invoke(FilterInvocation fi) throws IOException, ServletException { if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) { // filter already applied to this request and user wants us to observe // once-per-request handling, so don't re-do security checking fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else { // first time this request being called, so perform security checking if (fi.getRequest() != null) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } //呼叫父類的beforeInvocation進行身份校驗 InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } }
在beforeInvocation中叫呼叫介面AccessDecisionManager的實現類,校驗角色。
protected InterceptorStatusToken beforeInvocation(Object object) { //省略部分程式碼 //獲取當前執行緒的Authentication物件 Authentication authenticated = authenticateIfRequired(); // Attempt authorization try { //許可權角色校驗 this.accessDecisionManager.decide(authenticated, object, attributes); } //校驗失敗丟擲異常 catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); throw accessDeniedException; } //省略部分程式碼 }
最終呼叫子類AffirmativeBased的decide方法,在decide方法中會獲取AccessDecisionVoter對許可權進行投票處理,獲取投票結果,當投票結果是1時則表示有許可權,否則等於-1表示沒有許可權,拒絕。
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
//投票獲取結果
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
//當有許可權時直接返回
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
//否則拒絕
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
//
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
在實現類WebExpressionVoter會根據authentication結果進行校驗判斷,根據Authentication物件建立介面SecurityExpressionOperations的實現類SecurityExpressionRoot
(1)當請求url被配置為permitAll,則直接返回true,校驗通過
(2)其他需要登入請求url會呼叫isAuthenticated方法,最終呼叫AuthenticationTrustResolver的AuthenticationTrustResolverImpl的方法進行判斷
public abstract class SecurityExpressionRoot implements SecurityExpressionOperations {
protected final Authentication authentication;
private AuthenticationTrustResolver trustResolver;
private RoleHierarchy roleHierarchy;
private Set<String> roles;
private String defaultRolePrefix = "ROLE_";
/** Allows "permitAll" expression */
public final boolean permitAll = true;
/** Allows "denyAll" expression */
public final boolean denyAll = false;
private PermissionEvaluator permissionEvaluator;
public final String read = "read";
public final String write = "write";
public final String create = "create";
public final String delete = "delete";
public final String admin = "administration";
/**
* Creates a new instance
* @param authentication the {@link Authentication} to use. Cannot be null.
*/
public SecurityExpressionRoot(Authentication authentication) {
if (authentication == null) {
throw new IllegalArgumentException("Authentication object cannot be null");
}
this.authentication = authentication;
}
public final boolean hasAuthority(String authority) {
return hasAnyAuthority(authority);
}
public final boolean hasAnyAuthority(String... authorities) {
return hasAnyAuthorityName(null, authorities);
}
public final boolean hasRole(String role) {
return hasAnyRole(role);
}
public final boolean hasAnyRole(String... roles) {
return hasAnyAuthorityName(defaultRolePrefix, roles);
}
private boolean hasAnyAuthorityName(String prefix, String... roles) {
Set<String> roleSet = getAuthoritySet();
for (String role : roles) {
String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
if (roleSet.contains(defaultedRole)) {
return true;
}
}
return false;
}
public final Authentication getAuthentication() {
return authentication;
}
public final boolean permitAll() {
return true;
}
public final boolean denyAll() {
return false;
}
public final boolean isAnonymous() {
return trustResolver.isAnonymous(authentication);
}
public final boolean isAuthenticated() {
return !isAnonymous();
}
public final boolean isRememberMe() {
return trustResolver.isRememberMe(authentication);
}
public final boolean isFullyAuthenticated() {
return !trustResolver.isAnonymous(authentication)
&& !trustResolver.isRememberMe(authentication);
}
/**
* Convenience method to access {@link Authentication#getPrincipal()} from
* {@link #getAuthentication()}
* @return
*/
public Object getPrincipal() {
return authentication.getPrincipal();
}
public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
this.trustResolver = trustResolver;
}
public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
this.roleHierarchy = roleHierarchy;
}
/**
* <p>
* Sets the default prefix to be added to {@link #hasAnyRole(String...)} or
* {@link #hasRole(String)}. For example, if hasRole("ADMIN") or hasRole("ROLE_ADMIN")
* is passed in, then the role ROLE_ADMIN will be used when the defaultRolePrefix is
* "ROLE_" (default).
* </p>
*
* <p>
* If null or empty, then no default role prefix is used.
* </p>
*
* @param defaultRolePrefix the default prefix to add to roles. Default "ROLE_".
*/
public void setDefaultRolePrefix(String defaultRolePrefix) {
this.defaultRolePrefix = defaultRolePrefix;
}
private Set<String> getAuthoritySet() {
if (roles == null) {
roles = new HashSet<String>();
Collection<? extends GrantedAuthority> userAuthorities = authentication
.getAuthorities();
if (roleHierarchy != null) {
userAuthorities = roleHierarchy
.getReachableGrantedAuthorities(userAuthorities);
}
roles = AuthorityUtils.authorityListToSet(userAuthorities);
}
return roles;
}
public boolean hasPermission(Object target, Object permission) {
return permissionEvaluator.hasPermission(authentication, target, permission);
}
public boolean hasPermission(Object targetId, String targetType, Object permission) {
return permissionEvaluator.hasPermission(authentication, (Serializable) targetId,
targetType, permission);
}
public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) {
this.permissionEvaluator = permissionEvaluator;
}
/**
* Prefixes role with defaultRolePrefix if defaultRolePrefix is non-null and if role
* does not already start with defaultRolePrefix.
*
* @param defaultRolePrefix
* @param role
* @return
*/
private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
if (role == null) {
return role;
}
if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) {
return role;
}
if (role.startsWith(defaultRolePrefix)) {
return role;
}
return defaultRolePrefix + role;
}
}
在AuthenticationTrustResolverImpl方法isAnonymous中就是判斷傳遞過來的Authentication物件是不是AnonymousAuthenticationToken,如果是AnonymousAuthenticationToken則表示沒有登入,因為登入之後生成的物件是UsernamePasswordAuthenticationToken或其他Authentication物件,這也是Spring Security設計的最精華也是最難以理解也是最簡單的方式。
private Class<? extends Authentication> anonymousClass = AnonymousAuthenticationToken.class;
public boolean isAnonymous(Authentication authentication) {
if ((anonymousClass == null) || (authentication == null)) {
return false;
}
return anonymousClass.isAssignableFrom(authentication.getClass());
}
總結:Spring Security對url的許可權判斷有兩種方式,一種是請求是permitAll的則直接返回校驗通過,另外一個是判斷Authentication是不是AnonymousAuthenticationToken,因為正常登入等產生的不是這個物件,如果不是這個型別的物件則表示登入成功了。