给JFinal添加Shiro插件功能,支持Shiro所有注解-实现篇
给JFinal添加Shiro插件功能,支持Shiro所有注解-实现篇
玛雅牛 发表于4年前
给JFinal添加Shiro插件功能,支持Shiro所有注解-实现篇
  • 发表于 4年前
  • 阅读 14046
  • 收藏 75
  • 点赞 12
  • 评论 8

腾讯云 十分钟定制你的第一个小程序>>>   

2015年8月3日更新:

支持JFinal 2.0 版本,同时给出了一些实际代码,想见git

@JFinal给出了一些好的建议,已重构部分代码。

代码放在oschina的git上,访问地址:

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

最近用JFinal做个东西,需要进行较为精细的权限控制,研究后决定用Shiro来实现。于是给JFinal做了一个插件。

做了两版,第一版采用Shiro本身的aop来做,使用拦截器实现,每次请求都要检查注解,根据注解构建5个访问控制拦截器进行处理。仔细研究下,觉得每个请求需要处理的访问控制注解在系统启动时应该能全部获得,何不在启动时构建好。这样性能会好一些。Shiro原有的一套处理不适合在启动时构建好,于是重新设计了,但是具体处理逻辑还是来自Shiro,代码直接搬过来。放代码。

1)改造JFinal类,增加一个获得Routes的方法。

 

2)定义访问控制检查接口

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 "";
	}
}

实现完毕。

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

玛雅牛

 

标签: JFinal Shiro
共有 人打赏支持
玛雅牛
粉丝 467
博文 104
码字总数 25014
作品 4
评论 (8)
JFinal
终于有牛人集成了Shiro,顶一个。作者自己没用过Shrio,给不到太多建议。有几个地方提一下,Routes可以在YourJFinalConfig.configRoute(Routes me)方法中将 me 存放在一个static变量中,这样就可以得到了,不必修改JFinal源码。另外,拦截器里面为啥不用ai.getActionKey()来得actionKey,可能是别的原因吧
玛雅牛

引用来自“JFinal”的评论

终于有牛人集成了Shiro,顶一个。作者自己没用过Shrio,给不到太多建议。有几个地方提一下,Routes可以在YourJFinalConfig.configRoute(Routes me)方法中将 me 存放在一个static变量中,这样就可以得到了,不必修改JFinal源码。另外,拦截器里面为啥不用ai.getActionKey()来得actionKey,可能是别的原因吧

对JFinal的源码还不是非常熟悉,Routes获取的方式,你说的这种对JFinal侵入少一些,我该下,拦截器里刚开始是用ai.getActionKey()来做的,还是由于对JFinal代码不熟,才改成现在的方式,多谢你的指点,采用ai.getActionKey()话,性能应该会好一些。
铂金小虫
shiro本身不带aop吧,是依赖其他的aop框架吧。比如和spring整合。
玛雅牛

引用来自“铂金小虫”的评论

shiro本身不带aop吧,是依赖其他的aop框架吧。比如和spring整合。

shiro本身独立,但是又针对这种框架的插件。
罗盛力
楼主可以考虑ShiroInterceptor里面的catch中加入判断请求类型以适应Ajax请求的情况,或者直接在拦截器里面抛出异常,这样方便做异常的管理
懒的去懒
楼主你好,有没有一个完整的整合jfinal应用例子?
Solr_
为什么不放出一个例子?
程俊87
jfinal 3 以上 routes.getEntrySet() 不存在 ,可有提供修改的解決方案
×
玛雅牛
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: