給JFinal添加Shiro插件功能,支持Shiro全部註解-實現篇

2015年8月3日更新:java

支持JFinal 2.0 版本,同時給出了一些實際代碼,想見gitgit

@JFinal給出了一些好的建議,已重構部分代碼。apache

代碼放在oschina的git上,訪問地址:app

http://git.oschina.net/myaniu/jfinalshiroplugin函數

最近用JFinal作個東西,須要進行較爲精細的權限控制,研究後決定用Shiro來實現。因而給JFinal作了一個插件。性能

作了兩版,初版採用Shiro自己的aop來作,使用攔截器實現,每次請求都要檢查註解,根據註解構建5個訪問控制攔截器進行處理。仔細研究下,以爲每一個請求須要處理的訪問控制註解在系統啓動時應該能所有得到,何不在啓動時構建好。這樣性能會好一些。Shiro原有的一套處理不適合在啓動時構建好,因而從新設計了,可是具體處理邏輯仍是來自Shiro,代碼直接搬過來。放代碼。ui

1)改造JFinal類,增長一個得到Routes的方法。this

 

2)定義訪問控制檢查接口.net

package com.jfinal.ext.plugin.shiro;

import org.apache.shiro.authz.AuthorizationException;

/**
 * 訪問控制處理器接口
 * @author dafei
 *
 */
interface AuthzHandler {
	/**
	 * 訪問控制檢查
	 * @throws AuthorizationException 受權異常
	 */
	public void assertAuthorized()throws AuthorizationException;
}

3)定義訪問控制抽象基類。插件

abstract class AbstractAuthzHandler implements AuthzHandler {

	/**
	 * 得到Shiro的Subject對象。
	 * @return
	 */
	 protected Subject getSubject() {
	     return SecurityUtils.getSubject();
	 }
}

4)定義五種權限檢查處理器。

/**
 * 基於角色的訪問控制處理器,非單例模式運行。
 * @author dafei
 *
 */
class RoleAuthzHandler extends AbstractAuthzHandler {

	private final Annotation annotation;

	public RoleAuthzHandler(Annotation annotation){
		this.annotation = annotation;
	}

	public void assertAuthorized() throws AuthorizationException {
		//if (!(annotation instanceof RequiresRoles)) return;
        RequiresRoles rrAnnotation = (RequiresRoles) annotation;
        String[] roles = rrAnnotation.value();

        if (roles.length == 1) {
            getSubject().checkRole(roles[0]);
            return;
        }
        if (Logical.AND.equals(rrAnnotation.logical())) {
            getSubject().checkRoles(Arrays.asList(roles));
            return;
        }
        if (Logical.OR.equals(rrAnnotation.logical())) {
            // Avoid processing exceptions unnecessarily - "delay" throwing the exception by calling hasRole first
            boolean hasAtLeastOneRole = false;
            for (String role : roles) if (getSubject().hasRole(role)) hasAtLeastOneRole = true;
            // Cause the exception if none of the role match, note that the exception message will be a bit misleading
            if (!hasAtLeastOneRole) getSubject().checkRole(roles[0]);
        }
	}
}
/**
 * 基於權限的訪問控制處理器,非單例模式運行。
 * @author dafei
 *
 */
class PermissionAuthzHandler extends AbstractAuthzHandler {
	private final Annotation annotation;

	public PermissionAuthzHandler(Annotation annotation) {
		this.annotation = annotation;
	}

	public void assertAuthorized() throws AuthorizationException {
		if (!(annotation instanceof RequiresPermissions))
			return;

		RequiresPermissions rpAnnotation = (RequiresPermissions) annotation;
		String[] perms = rpAnnotation.value();
		Subject subject = getSubject();

		if (perms.length == 1) {
			subject.checkPermission(perms[0]);
			return;
		}
		if (Logical.AND.equals(rpAnnotation.logical())) {
			getSubject().checkPermissions(perms);
			return;
		}
		if (Logical.OR.equals(rpAnnotation.logical())) {
			// Avoid processing exceptions unnecessarily - "delay" throwing the
			// exception by calling hasRole first
			boolean hasAtLeastOnePermission = false;
			for (String permission : perms)
				if (getSubject().isPermitted(permission))
					hasAtLeastOnePermission = true;
			// Cause the exception if none of the role match, note that the
			// exception message will be a bit misleading
			if (!hasAtLeastOnePermission)
				getSubject().checkPermission(perms[0]);

		}

	}

}
/**
 * 已認證經過訪問控制處理器
 * 單例模式運行。
 *
 * @author dafei
 *
 */
class AuthenticatedAuthzHandler extends AbstractAuthzHandler {

	private static AuthenticatedAuthzHandler aah = new AuthenticatedAuthzHandler();

	private AuthenticatedAuthzHandler(){}

	public static  AuthenticatedAuthzHandler me(){
		return aah;
	}

	public void assertAuthorized() throws AuthorizationException {
		if (!getSubject().isAuthenticated() ) {
            throw new UnauthenticatedException( "The current Subject is not authenticated.  Access denied." );
        }
	}
}
/**
 * 認證經過或已記住的用戶訪問控制處理器
 * 單例模式運行。
 * @author dafei
 *
 */
class UserAuthzHandler extends AbstractAuthzHandler {
	private static UserAuthzHandler uah = new UserAuthzHandler();

	private UserAuthzHandler(){}

	public static  UserAuthzHandler me(){
		return uah;
	}

	public void assertAuthorized() throws AuthorizationException {
		if (getSubject().getPrincipal() == null) {
            throw new UnauthenticatedException("Attempting to perform a user-only operation.  The current Subject is " +
                    "not a user (they haven't been authenticated or remembered from a previous login).  " +
                    "Access denied.");
        }
	}
}
/**
 * 訪客訪問控制處理器
 * @author dafei
 *
 */
class GuestAuthzHandler extends AbstractAuthzHandler {
	private static GuestAuthzHandler gah = new GuestAuthzHandler();

	private GuestAuthzHandler(){}

	public static  GuestAuthzHandler me(){
		return gah;
	}

	public void assertAuthorized() throws AuthorizationException {
		 if (getSubject().getPrincipal() != null) {
	            throw new UnauthenticatedException("Attempting to perform a guest-only operation.  The current Subject is " +
	                    "not a guest (they have been authenticated or remembered from a previous login).  Access " +
	                    "denied.");
	        }
	}

}

5)定義一個組合訪問處理器,用來統一幾個處理器。

class CompositeAuthzHandler implements AuthzHandler {

	private final List<AuthzHandler> authzHandlers;

	public CompositeAuthzHandler(List<AuthzHandler> authzHandlers){
		this.authzHandlers = authzHandlers;
	}

	public void assertAuthorized() throws AuthorizationException {
		for(AuthzHandler authzHandler : authzHandlers){
			authzHandler.assertAuthorized();
		}
	}
}

6)定義了一個註解,用來清除權限註解(主要用來清除Controller上的訪問控制註解)

/**
 * 用來清除全部的Shiro訪問控制註解,適合於Controller絕大部分方法都須要作訪問控制,個別不須要作訪問控制的場合。
 * 僅能用在方法上。
 * @author dafei
 */
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface ClearShiro {
}

7)實現ShiroPlugin方法

package com.jfinal.ext.plugin.shiro;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresGuest;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.authz.annotation.RequiresUser;

import com.jfinal.config.Routes;
import com.jfinal.core.ActionKey;
import com.jfinal.core.Controller;
import com.jfinal.core.JFinal;
import com.jfinal.plugin.IPlugin;

/**
 * Shiro插件,啓動時加載全部Shiro訪問控制註解。
 * @author dafei
 *
 */
@SuppressWarnings("unchecked")
public class ShiroPlugin implements IPlugin {

	private static final String SLASH = "/";

	/**
	 * Shiro的幾種訪問控制註解
	 */
	private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES = new Class[] {
			RequiresPermissions.class, RequiresRoles.class, RequiresUser.class,
			RequiresGuest.class, RequiresAuthentication.class };

/**
 * 路由設定
 */
 private final Routes routes;


 /**
 * 構造函數
 * @param routes 路由設定
 */
 public ShiroPlugin(Routes routes){
 this.routes = routes;
 }

	/**
	 * 中止插件
	 */
	public boolean stop() {
		return true;
	}

	/**
	 * 啓動插件
	 */
	public boolean start() {
		//獲取全部路由設定。這裏修改了JFinal類,增長了一個getRoutes方法。
		//Routes routes = JFinal.me().getRoutes();
		Set<String> excludedMethodName = buildExcludedMethodName();
		ConcurrentMap<String, AuthzHandler> authzMaps = new ConcurrentHashMap<String, AuthzHandler>();
		//逐個訪問全部註冊的Controller,解析Controller及action上的全部Shiro註解。
		//並依據這些註解,actionKey提早構建好權限檢查處理器。
		for (Entry<String, Class<? extends Controller>> entry : routes
				.getEntrySet()) {
			Class<? extends Controller> controllerClass = entry.getValue();

			// 獲取Controller的全部Shiro註解。
			List<Annotation> controllerAnnotations = getAuthzAnnotations(controllerClass);
                        String controllerKey = entry.getKey();
			// 逐個遍歷方法。
			Method[] methods = controllerClass.getMethods();
			for (Method method : methods) {
				//排除掉Controller基類的全部方法,而且只關注沒有參數的Action方法。
				if (!excludedMethodName.contains(method.getName())
						&& method.getParameterTypes().length == 0) {
					//若該方法上存在ClearShiro註解,則對該action不進行訪問控制檢查。
					if(isClearShiroAnnotationPresent(method)){
						continue;
					}
					//獲取方法的全部Shiro註解。
					List<Annotation> methodAnnotations = getAuthzAnnotations(method);
					//依據Controller的註解和方法的註解來生成訪問控制處理器。
					AuthzHandler authzHandler = createAuthzHandler(
							controllerAnnotations, methodAnnotations);
					//生成訪問控制處理器成功。
					if (authzHandler != null) {
						//構建ActionKey,參考ActionMapping中實現
						String actionKey = createActionKey(controllerClass,
								method,controllerKey);
						//添加映射
						authzMaps.put(actionKey, authzHandler);
					}
				}
			}
		}
		//注入到ShiroKit類中。ShiroKit類以單例模式運行。
		ShiroKit.init(authzMaps);
		return true;
	}

	/**
	 * 從Controller方法中構建出須要排除的方法列表
	 * @return
	 */
	private Set<String> buildExcludedMethodName() {
		Set<String> excludedMethodName = new HashSet<String>();
		Method[] methods = Controller.class.getMethods();
		for (Method m : methods) {
			if (m.getParameterTypes().length == 0)
				excludedMethodName.add(m.getName());
		}
		return excludedMethodName;
	}

	/**
	 * 依據Controller的註解和方法的註解來生成訪問控制處理器。
	 * @param controllerAnnotations  Controller的註解
	 * @param methodAnnotations 方法的註解
	 * @return 訪問控制處理器
	 */
	private AuthzHandler createAuthzHandler(
			List<Annotation> controllerAnnotations,
			List<Annotation> methodAnnotations) {

		//沒有註解
		if (controllerAnnotations.size() == 0 && methodAnnotations.size() == 0) {
			return null;
		}
		//至少有一個註解
		List<AuthzHandler> authzHandlers = new ArrayList<AuthzHandler>(5);
		for (int index = 0; index < 5; index++) {
			authzHandlers.add(null);
		}

		// 逐個掃描註解,如果相應的註解則在相應的位置賦值。
		scanAnnotation(authzHandlers, controllerAnnotations);
		// 逐個掃描註解,如果相應的註解則在相應的位置賦值。函數的註解優先級高於Controller
		scanAnnotation(authzHandlers, methodAnnotations);

		// 去除空值
		List<AuthzHandler> finalAuthzHandlers = new ArrayList<AuthzHandler>();
		for (AuthzHandler a : authzHandlers) {
			if (a != null) {
				finalAuthzHandlers.add(a);
			}
		}
		authzHandlers = null;
		// 存在多個,則構建組合AuthzHandler
		if (finalAuthzHandlers.size() > 1) {
			return new CompositeAuthzHandler(finalAuthzHandlers);
		}
		// 一個的話直接返回
		return finalAuthzHandlers.get(0);
	}

	/**
	 * 逐個掃描註解,如果相應的註解則在相應的位置賦值。
	 * 註解的處理是有順序的,依次爲RequiresRoles,RequiresPermissions,
	 * RequiresAuthentication,RequiresUser,RequiresGuest
	 *
	 * @param authzArray
	 * @param annotations
	 */
	private void scanAnnotation(List<AuthzHandler> authzArray,
			List<Annotation> annotations) {
		if (null == annotations || 0 == annotations.size()) {
			return;
		}
		for (Annotation a : annotations) {
			if (a instanceof RequiresRoles) {
				authzArray.set(0, new RoleAuthzHandler(a));
			} else if (a instanceof RequiresPermissions) {
				authzArray.set(1, new PermissionAuthzHandler(a));
			} else if (a instanceof RequiresAuthentication) {
				authzArray.set(2, AuthenticatedAuthzHandler.me());
			} else if (a instanceof RequiresUser) {
				authzArray.set(3, UserAuthzHandler.me());
			} else if (a instanceof RequiresGuest) {
				authzArray.set(4, GuestAuthzHandler.me());
			}
		}
	}

	/**
	 * 構建actionkey,參考ActionMapping中的實現。
	 *
	 * @param controllerClass
	 * @param method
	 * @param controllerKey
	 * @return
	 */
	private String createActionKey(Class<? extends Controller> controllerClass,
			Method method, String controllerKey) {
String methodName = method.getName();
 String actionKey = "";


 ActionKey ak = method.getAnnotation(ActionKey.class);
 if (ak != null) {
 actionKey = ak.value().trim();
 if ("".equals(actionKey))
 throw new IllegalArgumentException(controllerClass.getName() + "." + methodName + "(): The argument of ActionKey can not be blank.");
 if (!actionKey.startsWith(SLASH))
 actionKey = SLASH + actionKey;
 }
 else if (methodName.equals("index")) {
 actionKey = controllerKey;
 }
 else {
 actionKey = controllerKey.equals(SLASH) ? SLASH + methodName : controllerKey + SLASH + methodName;
 }
 return actionKey;

	}

	/**
	 * 返回該方法的全部訪問控制註解
	 *
	 * @param method
	 * @return
	 */
	private List<Annotation> getAuthzAnnotations(Method method) {
		List<Annotation> annotations = new ArrayList<Annotation>();
		for (Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES) {
			Annotation a = method.getAnnotation(annClass);
			if (a != null) {
				annotations.add(a);
			}
		}
		return annotations;
	}

	/**
	 * 返回該Controller的全部訪問控制註解
	 *
	 * @param method
	 * @return
	 */
	private List<Annotation> getAuthzAnnotations(
			Class<? extends Controller> targetClass) {
		List<Annotation> annotations = new ArrayList<Annotation>();
		for (Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES) {
			Annotation a = targetClass.getAnnotation(annClass);
			if (a != null) {
				annotations.add(a);
			}
		}
		return annotations;
	}
	/**
	 * 該方法上是否有ClearShiro註解
	 * @param method
	 * @return
	 */
	private boolean isClearShiroAnnotationPresent(Method method) {
		Annotation a = method.getAnnotation(ClearShiro.class);
		if (a != null) {
			return true;
		}
		return false;
	}
}

8)實現Shiro攔截器。

package com.jfinal.ext.plugin.shiro;

import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthenticatedException;

import com.jfinal.aop.Interceptor;
import com.jfinal.core.ActionInvocation;

public class ShiroInterceptor implements Interceptor {

	public void intercept(Invocation ai){                 AuthzHandler  ah = ShiroKit.getAuthzHandler(ai.getActionKey());

		//存在訪問控制處理器。
		if(ah != null){
	        try {
	        	//執行權限檢查。
	        	ah.assertAuthorized();
			} catch (UnauthenticatedException lae) {
				//RequiresGuest,RequiresAuthentication,RequiresUser,未知足時,拋出未經受權的異常。
				//若是沒有進行身份驗證,返回HTTP401狀態碼
				ai.getController().renderError(401);
				return;
			} catch (AuthorizationException ae) {
				//RequiresRoles,RequiresPermissions受權異常
				//若是沒有權限訪問對應的資源,返回HTTP狀態碼403。
				ai.getController().renderError(403);
				return;
			} catch (Exception e) {
				ai.getController().renderError(401); return;
			}
        }
        //執行正常邏輯
        ai.invoke();
	}
}

9)構建一個ShiroKit輔助類

package com.jfinal.ext.plugin.shiro;

import java.util.concurrent.ConcurrentMap;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;


/**
 * 將全部Shiro指令封裝成HTTL的函數。
 *
 * @author dafei
 */
public class ShiroKit {

	/**
	 * 用來記錄那個action或者actionpath中是否有shiro認證註解。
	 */
	private static ConcurrentMap<String, AuthzHandler> authzMaps = null;


	private static final String NAMES_DELIMETER = ",";

	/**
	 * 禁止初始化
	 */
	private ShiroKit() {}

	static void init(ConcurrentMap<String, AuthzHandler> maps) {
		authzMaps = maps;
	}

	static AuthzHandler getAuthzHandler(String actionKey){
		return authzMaps.get(actionKey);
	}

	/**
	 * 獲取 Subject
	 *
	 * @return Subject
	 */
	protected static Subject getSubject() {
		return SecurityUtils.getSubject();
	}

	/**
	 * 驗證當前用戶是否屬於該角色?,使用時與lacksRole 搭配使用
	 *
	 * @param roleName
	 *            角色名
	 * @return 屬於該角色:true,不然false
	 */
	public static boolean hasRole(String roleName) {
		return getSubject() != null && roleName != null
				&& roleName.length() > 0 && getSubject().hasRole(roleName);
	}

	/**
	 * 與hasRole標籤邏輯相反,當用戶不屬於該角色時驗證經過。
	 *
	 * @param roleName
	 *            角色名
	 * @return 不屬於該角色:true,不然false
	 */
	public static boolean lacksRole(String roleName) {
		return !hasRole(roleName);
	}

	/**
	 * 驗證當前用戶是否屬於如下任意一個角色。
	 *
	 * @param roleNames
	 *            角色列表
	 * @return 屬於:true,不然false
	 */
	public static boolean hasAnyRoles(String roleNames) {
		boolean hasAnyRole = false;
		Subject subject = getSubject();
		if (subject != null && roleNames != null && roleNames.length() > 0) {
			// Iterate through roles and check to see if the user has one of the
			// roles
			for (String role : roleNames.split(NAMES_DELIMETER)) {
				if (subject.hasRole(role.trim())) {
					hasAnyRole = true;
					break;
				}
			}
		}
		return hasAnyRole;
	}

	/**
	 * 驗證當前用戶是否屬於如下全部角色。
	 *
	 * @param roleNames
	 *            角色列表
	 * @return 屬於:true,不然false
	 */
	public static boolean hasAllRoles(String roleNames) {
		boolean hasAllRole = true;
		Subject subject = getSubject();
		if (subject != null && roleNames != null && roleNames.length() > 0) {
			// Iterate through roles and check to see if the user has one of the
			// roles
			for (String role : roleNames.split(NAMES_DELIMETER)) {
				if (!subject.hasRole(role.trim())) {
					hasAllRole = false;
					break;
				}
			}
		}
		return hasAllRole;
	}

	/**
	 * 驗證當前用戶是否擁有指定權限,使用時與lacksPermission 搭配使用
	 *
	 * @param permission
	 *            權限名
	 * @return 擁有權限:true,不然false
	 */
	public static boolean hasPermission(String permission) {
		return getSubject() != null && permission != null
				&& permission.length() > 0
				&& getSubject().isPermitted(permission);
	}

	/**
	 * 與hasPermission標籤邏輯相反,當前用戶沒有制定權限時,驗證經過。
	 *
	 * @param permission
	 *            權限名
	 * @return 擁有權限:true,不然false
	 */
	public static boolean lacksPermission(String permission) {
		return !hasPermission(permission);
	}

	/**
	 * 已認證經過的用戶。不包含已記住的用戶,這是與user標籤的區別所在。與notAuthenticated搭配使用
	 *
	 * @return 經過身份驗證:true,不然false
	 */
	public static boolean authenticated() {
		return getSubject() != null && getSubject().isAuthenticated();
	}

	/**
	 * 未認證經過用戶,與authenticated標籤相對應。與guest標籤的區別是,該標籤包含已記住用戶。。
	 *
	 * @return 沒有經過身份驗證:true,不然false
	 */
	public static boolean notAuthenticated() {
		return !authenticated();
	}

	/**
	 * 認證經過或已記住的用戶。與guset搭配使用。
	 *
	 * @return 用戶:true,不然 false
	 */
	public static boolean user() {
		return getSubject() != null && getSubject().getPrincipal() != null;
	}

	/**
	 * 驗證當前用戶是否爲「訪客」,即未認證(包含未記住)的用戶。用user搭配使用
	 *
	 * @return 訪客:true,不然false
	 */
	public static boolean guest() {
		return !user();
	}

	/**
	 * 輸出當前用戶信息,一般爲登陸賬號信息。
	 * @return 當前用戶信息
	 */
	public String principal(){
		if (getSubject() != null) {
            // Get the principal to print out
            Object principal = getSubject().getPrincipal();
            return principal.toString();
        }
		return "";
	}
}

實現完畢。

----------------------------------------------------------

瑪雅牛

相關文章
相關標籤/搜索