https://github.com/Dreampie/jfinal-shiro 的jfinal-shiro插件:
<dependency> <groupId>cn.dreampie</groupId> <artifactId>jfinal-shiro</artifactId> <version>${jfinal-shiro.version}</version> </dependency>
目前刚刚发布第一个版本0.1:
<jfinal-shiro.version>0.1</jfinal-shiro.version>
首先感谢jfinal-ext中原作者,该插件主要是针对ext插件的部分改进。
下面主要介绍两种使用方式:
在web.xml里添加
<!--权限过滤器 start--> <listener> <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class> </listener> <filter> <filter-name>shiroFilter</filter-name> <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>FORWARD</dispatcher> <dispatcher>INCLUDE</dispatcher> <dispatcher>ERROR</dispatcher> </filter-mapping> <!--权限过滤器 end-->
启用shiro 在jfinal config里configPlugin方法添加
//shiro权限框架 在jfinal plugins里添加shiro plugin plugins.add(new ShiroPlugin(routes, new MyJdbcAuthzService()));
添加shiro的过滤器 在jfinal config里configInterceptor方法添加
interceptors.add(new ShiroInterceptor()); public class User extends cn.dreampie.shiro.model.User<User> //user的model需要继承User
在方法上使用注解
Shiro共有5个注解,分别如下:
RequiresAuthentication:使用该注解标注的类,实例,方法在访问或调用时,当前Subject必须在当前session中已经过认证。
RequiresGuest:使用该注解标注的类,实例,方法在访问或调用时,当前Subject可以是“gust”身份,不需要经过认证或者在原先的session中存在记录。
RequiresPermissions:当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前Subject不具有这样的权限,则方法不会被执行。
RequiresRoles:当前Subject必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天Subject不同时拥有所有指定角色,则方法不会执行还会抛出AuthorizationException异常。
RequiresUser:当前Subject必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。
Shiro的认证注解处理是有内定的处理顺序的,如果有个多个注解的话,前面的通过了会继续检查后面的,若不通过则直接返回,处理顺序依次为(与实际声明顺序无关):
RequiresRoles
RequiresPermissions
RequiresAuthentication
RequiresUser
RequiresGuest
例如:你同时生命了RequiresRoles和RequiresPermissions,那就要求拥有此角色的同时还得拥有相应的权限。
RequiresRoles可以用在Controller或者方法上。可以多个roles,默认逻辑为 AND也就是所有具备所有role才能访问。
示例:
//属于user角色 @RequiresRoles("user") //必须同时属于user和admin角色 @RequiresRoles({"user","admin"}) //属于user或者admin之一。 @RequiresRoles(value={"user","admin"},logical=Logical.OR)
其他使用方法类似。
@RequiresPermissions @RequiresAuthentication @RequiresUser @RequiresGusst
详细可以参考玛雅牛的shiro注解使用,http://my.oschina.net/myaniu/blog/137205
基于数据库的权限设计与维护
数据库的基本权限结构主要:用户->角色->权限
表结构设计如下(h2数据库,使用其他数据修改部分sql语句之后使用):
DROP TABLE IF EXISTS sec_user; DROP SEQUENCE IF EXISTS sec_user_id_seq; CREATE SEQUENCE sec_user_id_seq START WITH 1; CREATE TABLE sec_user ( --用户表 id BIGINT NOT NULL DEFAULT NEXTVAL('sec_user_id_seq') PRIMARY KEY, username VARCHAR(50) NOT NULL COMMENT '登录名', providername VARCHAR(50) NOT NULL COMMENT '提供者', email VARCHAR(200) COMMENT '邮箱', mobile VARCHAR(50) COMMENT '手机', password VARCHAR(200) NOT NULL COMMENT '密码', hasher VARCHAR(200) NOT NULL COMMENT '加密类型', salt VARCHAR(200) NOT NULL COMMENT '加密盐', avatar_url VARCHAR(255) COMMENT '头像', first_name VARCHAR(10) COMMENT '名字', last_name VARCHAR(10) COMMENT '姓氏', full_name VARCHAR(20) COMMENT '全名', department_id BIGINT NOT NULL COMMENT '部门id', created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP, deleted_at TIMESTAMP ); DROP TABLE IF EXISTS sec_user_info; DROP SEQUENCE IF EXISTS sec_user_info_id_seq; CREATE SEQUENCE sec_user_info_id_seq START WITH 1; CREATE TABLE sec_user_info (-- 用户详细信息表 id BIGINT NOT NULL DEFAULT NEXTVAL('sec_user_info_id_seq') PRIMARY KEY, user_id BIGINT NOT NULL COMMENT '用户id', creator_id BIGINT COMMENT '创建者id', gender INT DEFAULT 0 COMMENT '性别0男,1女', province_id BIGINT COMMENT '省id', city_id BIGINT COMMENT '市id', county_id BIGINT COMMENT '县id', street VARCHAR(500) COMMENT '街道', zip_code VARCHAR(50) COMMENT '邮编', created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP, deleted_at TIMESTAMP ); DROP TABLE IF EXISTS sec_role; DROP SEQUENCE IF EXISTS sec_role_id_seq; CREATE SEQUENCE sec_role_id_seq START WITH 1; CREATE TABLE sec_role (--角色表 id BIGINT NOT NULL DEFAULT NEXTVAL('sec_role_id_seq') PRIMARY KEY, name VARCHAR(50) NOT NULL COMMENT '名称', value VARCHAR(50) NOT NULL COMMENT '值', intro VARCHAR(255) COMMENT '简介', pid BIGINT DEFAULT 0 COMMENT '父级id', left_code BIGINT DEFAULT 0 COMMENT '数据左边码', right_code BIGINT DEFAULT 0 COMMENT '数据右边码', created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP, deleted_at TIMESTAMP ); DROP TABLE IF EXISTS sec_user_role; DROP SEQUENCE IF EXISTS sec_user_role_id_seq; CREATE SEQUENCE sec_user_role_id_seq START WITH 1; CREATE TABLE sec_user_role (--用户角色关系表 id BIGINT NOT NULL DEFAULT NEXTVAL('sec_user_role_id_seq') PRIMARY KEY, user_id BIGINT NOT NULL, role_id BIGINT NOT NULL ); DROP TABLE IF EXISTS sec_permission; DROP SEQUENCE IF EXISTS sec_permission_id_seq; CREATE SEQUENCE sec_permission_id_seq START WITH 1; CREATE TABLE sec_permission (--权限表 id BIGINT NOT NULL DEFAULT NEXTVAL('sec_permission_id_seq') PRIMARY KEY, name VARCHAR(50) NOT NULL COMMENT '名称', value VARCHAR(50) NOT NULL COMMENT '值', url VARCHAR(255) COMMENT 'url地址', intro VARCHAR(255) COMMENT '简介', pid BIGINT DEFAULT 0 COMMENT '父级id', left_code BIGINT DEFAULT 0 COMMENT '数据左边码', right_code BIGINT DEFAULT 0 COMMENT '数据右边码', created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP, deleted_at TIMESTAMP ); DROP TABLE IF EXISTS sec_role_permission; DROP SEQUENCE IF EXISTS sec_role_permission_id_seq; CREATE SEQUENCE sec_role_permission_id_seq START WITH 1; CREATE TABLE sec_role_permission (--角色权限关系表 id BIGINT NOT NULL DEFAULT NEXTVAL('sec_role_permission_id_seq') PRIMARY KEY, role_id BIGINT NOT NULL, permission_id BIGINT NOT NULL );
数据结构基本完成,提示:pid,left_code,right_code是数据的树形结构设计和权限无关
测试数据:
--create role-- INSERT INTO sec_role(id,name, value, intro, pid,left_code,right_code,created_at) VALUES (sec_role_id_seq.nextval,'超级管理员','R_ADMIN','',0,1,8, current_timestamp), (sec_role_id_seq.nextval,'系统管理员','R_MANAGER','',1,2,7,current_timestamp), (sec_role_id_seq.nextval,'会员','R_MEMBER','',2,3,4,current_timestamp), (sec_role_id_seq.nextval,'普通用户','R_USER','',2,5,6,current_timestamp); --create permission-- INSERT INTO sec_permission(id, name, value, url, intro,pid,left_code,right_code, created_at) VALUES (sec_permission_id_seq.nextval,'管理员目录','P_D_ADMIN','/admin/**','',0,1,6,current_timestamp), (sec_permission_id_seq.nextval,'角色权限管理','P_ROLE','/admin/role/**','',1,2,3,current_timestamp), (sec_permission_id_seq.nextval,'用户管理','P_USER','/admin/user/**','',1,4,5,current_timestamp), (sec_permission_id_seq.nextval,'会员目录','P_D_MEMBER','/member/**','',0,9,10,current_timestamp), (sec_permission_id_seq.nextval,'普通用户目录','P_D_USER','/user/**','',0,11,12,current_timestamp); INSERT INTO sec_role_permission(id,role_id, permission_id) VALUES (sec_role_permission_id_seq.nextval,1,1),(sec_role_permission_id_seq.nextval,1,2), (sec_role_permission_id_seq.nextval,1,3),(sec_role_permission_id_seq.nextval,1,4), (sec_role_permission_id_seq.nextval,1,5), (sec_role_permission_id_seq.nextval,2,1),(sec_role_permission_id_seq.nextval,2,3), (sec_role_permission_id_seq.nextval,2,4),(sec_role_permission_id_seq.nextval,2,5), (sec_role_permission_id_seq.nextval,3,4),(sec_role_permission_id_seq.nextval,3,5), (sec_role_permission_id_seq.nextval,4,5); --user data-- --create admin-- INSERT INTO sec_user(id, username, providername, email, mobile, password, hasher, salt, avatar_url, first_name, last_name, full_name,department_id, created_at) VALUES (sec_user_id_seq.nextval,'admin','dreampie','[email protected]','18611434500','$shiro1$SHA-256$500000$ZMhNGAcL3HbpTbNXzxxT1Q==$wRi5AF6BK/1FsQdvISIY1lJ9Rm/aekBoChjunVsqkUU=','default_hasher','','','仁辉','王','仁辉·王',1,current_timestamp), (sec_user_id_seq.nextval,'aaaaa','dreampie','[email protected]','18511400000','$shiro1$SHA-256$500000$ZMhNGAcL3HbpTbNXzxxT1Q==$wRi5AF6BK/1FsQdvISIY1lJ9Rm/aekBoChjunVsqkUU=','default_hasher','','','金彤','刘','金彤·刘',2,current_timestamp); --create user_info-- INSERT INTO sec_user_info(id, user_id, creator_id, gender,province_id,city_id,county_id,street,created_at) VALUES (sec_user_info_id_seq.nextval,1,0,0,1,2,3,'人民大学',current_timestamp), (sec_user_info_id_seq.nextval,2,0,0,1,2,3,'人民大学',current_timestamp); --create user_role-- INSERT INTO sec_user_role(id, user_id, role_id) VALUES (sec_user_role_id_seq.nextval,1,1), (sec_user_role_id_seq.nextval,2,2);
接下来实现两个关键接口,一个是shiro的JdbcRealm:
public class MyJdbcRealm extends AuthorizingRealm { /** * 登录认证 * * @param token * @return * @throws org.apache.shiro.authc.AuthenticationException */ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken userToken = (UsernamePasswordToken) token; User user = null; String username = userToken.getUsername(); if (ValidateKit.isEmail(username)) { user = User.dao.findFirstBy(" `user`.email =? AND `user`.deleted_at is null", username); } else if (ValidateKit.isMobile(username)) { user = User.dao.findFirstBy(" `user`.mobile =? AND `user`.deleted_at is null", username); } else { user = User.dao.findFirstBy(" `user`.username =? AND `user`.deleted_at is null", username); } if (user != null) { SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getStr("password"), getName()); return info; } else { return null; } } /** * 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用. * * @param principals * @return */ protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String loginName = ((User) principals.fromRealm(getName()).iterator().next()).get("username"); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); Set<String> roleSet = new LinkedHashSet<String>(); // 角色集合 Set<String> permissionSet = new LinkedHashSet<String>(); // 权限集合 List<Role> roles = null; User user = User.dao.findFirstBy(" `user`.username =? AND `user`.deleted_at is null", loginName); if (user != null) { //遍历角色 roles = Role.dao.findUserBy("", user.getLong("id")); } else { SubjectKit.getSubject().logout(); } loadRole(roleSet, permissionSet, roles); info.setRoles(roleSet); // 设置角色 info.setStringPermissions(permissionSet); // 设置权限 return info; } /** * @param roleSet * @param permissionSet * @param roles */ private void loadRole(Set<String> roleSet, Set<String> permissionSet, List<Role> roles) { List<Permission> permissions; for (Role role : roles) { //角色可用 if (role.getDate("deleted_at") == null) { roleSet.add(role.getStr("value")); permissions = Permission.dao.findByRole("", role.getLong("id")); loadAuth(permissionSet, permissions); } } } /** * @param permissionSet * @param permissions */ private void loadAuth(Set<String> permissionSet, List<Permission> permissions) { //遍历权限 for (Permission permission : permissions) { //权限可用 if (permission.getDate("deleted_at") == null) { permissionSet.add(permission.getStr("value")); } } } /** * 更新用户授权信息缓存. */ public void clearCachedAuthorizationInfo(Object principal) { SimplePrincipalCollection principals = new SimplePrincipalCollection(principal, getName()); clearCachedAuthorizationInfo(principals); } /** * 清除所有用户授权信息缓存. */ public void clearAllCachedAuthorizationInfo() { Cache<Object, AuthorizationInfo> cache = getAuthorizationCache(); if (cache != null) { for (Object key : cache.keys()) { cache.remove(key); } } } }
实现数据库权限的初始化加载:
public class MyJdbcAuthzService implements JdbcAuthzService { @Override public Map<String, AuthzHandler> getJdbcAuthz() { //加载数据库的url配置 Map<String, AuthzHandler> authzJdbcMaps = new HashMap<String, AuthzHandler>(); //遍历角色 List<Role> roles = Role.dao.findAll(); List<Permission> permissions = null; for (Role role : roles) { //角色可用 if (role.getDate("daleted_at") == null) { permissions = Permission.dao.findByRole("", role.get("id")); //遍历权限 for (Permission permission : permissions) { //权限可用 if (permission.getDate("daleted_at") == null) { if (permission.getStr("url") != null && !permission.getStr("url").isEmpty()) { authzJdbcMaps.put(permission.getStr("url"), new JdbcPermissionAuthzHandler(permission.getStr("value"))); } } } } } return authzJdbcMaps; } }
前台使用验证码时传入username,password,captcha 三个参数,第三个是验证码参数名,提前把验证码内容存入session,shiro会自动进行验证,注意名称为captcha
主要结构是权限表里的url-value,如果需要访问
url: /admin/** 需要value:P_D_ADMIN
把这些权限绑定到角色之后,角色绑定给用户就相当于,用户下面有很多这些 url-value
1.系统启动的时候把这个对应关系加载到内存或者缓存 //cn.dreampie.shiro.core.ShiroKit
2. 用户登录的时候把用户对应的角色所有的权限加载到缓存,这一步是shiro自己实现
3.当用户访问某个url的时候 如访问/admin/index,过滤器会匹配到/admin/**,这个url需要拥有P_D_ADMIN的权限
4.然后使用shiro的接口hasPremission(value),判断用户是否拥有这个权限//cn.dreampie.shiro.core.ShiroInterceptor
5.放行或者拒绝访问返回403状态
jfinal-shiro支持Ajax登陆/退出,使用json数据
shiro.ini 配置文件:
[users] guest = guest,guest [main] authc = cn.dreampie.shiro.ShiroFormAuthenticationFilter #登陆请求路径 authc.loginUrl = /signin #分角色登录配置 #authc.loginUrlMap = user:/login,admin:/admin/login #登陆成功跳转路径 authc.successUrl = / #登陆失败跳转路径 authc.failureUrl = /signin #登陆成功跳转路径 #authc.failureUrlMap = user:/login.ftl,admin:/admin/login.ftl signout = cn.dreampie.shiro.ShiroLogoutFilter #退出跳转路径 signout.redirectUrl = / #分角色退出跳转 #logout.redirectUrlMap = user:/index,admin:/index #realm 上面实现的获取登陆用户和用户权限的借口类 jdbcRealm = org.icedog.common.shiro.MyJdbcRealm securityManager.realm = $jdbcRealm passwordService = org.apache.shiro.authc.credential.DefaultPasswordService passwordMatcher = cn.dreampie.shiro.ShiroPasswordMatcher passwordMatcher.passwordService = $passwordService jdbcRealm.credentialsMatcher = $passwordMatcher #cache shiroCacheManager = org.apache.shiro.cache.ehcache.EhCacheManager shiroCacheManager.cacheManagerConfigFile = classpath:ehcache.xml securityManager.cacheManager = $shiroCacheManager #session sessionDAO = org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager sessionDAO.activeSessionsCacheName = shiro-activeSessionCache sessionManager.sessionDAO = $sessionDAO securityManager.sessionManager = $sessionManager sessionListener = cn.dreampie.shiro.listeners.ShiroSessionListener securityManager.sessionManager.sessionListeners = $sessionListener # cookie for single sign on #cookie = org.apache.shiro.web.servlet.SimpleCookie #cookie.name = www.dreampie.cn.session #cookie.path = / #cookie.maxAge = -1 #sessionManager.sessionIdCookie = $cookie # 1,800,000 milliseconds = 30 mins securityManager.sessionManager.globalSessionTimeout = 1200000 ;sessionValidationScheduler = org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler ;sessionValidationScheduler.interval = 1200000 ;securityManager.sessionManager.sessionValidationScheduler = $sessionValidationScheduler securityManager.sessionManager.sessionValidationSchedulerEnabled = false securityManager.sessionManager.deleteInvalidSessions = false ;securityManager.subjectDAO.sessionStorageEvaluator.sessionStorageEnabled = false [urls] /signin = authc /signout = signout /** = anon
如果你使用freemarker作为模板,推荐使用jfinal-shiro-freemarker标签库 http://my.oschina.net/wangrenhui1990/blog/312741
https://github.com/Dreampie?tab=repositories 目录下有多款插件:
cn.dreampie.jfinal-shiro https://github.com/Dreampie/jfinal-shiro shiro插件
cn.dreampie.jfinal-shiro-freemarker https://github.com/Dreampie/jfinal-shiro-freemarker shiro插件实现的freemarker标签库
cn.dreampie.jfinal-web https://github.com/Dreampie/jfinal-web 相关web插件,简洁model实现
cn.dreampie.jfinal-utils https://github.com/Dreampie/jfinal-utils 部分jfinal工具
cn.dreampie.jfinal-tablebind https://github.com/Dreampie/jfinal-tablebind jfinal的table自动绑定插件,支持多数据源
cn.dreampie.jfinal-flyway https://github.com/Dreampie/jfinal-flyway 数据库脚本升级插件,开发中升级应用时,使用脚本同步升级数据库或者回滚
cn.dreampie.jfinal-captcha https://github.com/Dreampie/jfinal-captcha 基于jfinal render的超简单验证吗插件
cn.dreampie.jfinal-quartz https://github.com/Dreampie/jfinal-quartz 基于jfinal 的quartz管理器
cn.dreampie.jfinal-sqlinxml https://github.com/Dreampie/jfinal-sqlinxml 基于jfinal 的类似ibatis的sql语句管理方案
cn.dreampie.jfinal-lesscss https://github.com/Dreampie/jfinal-lesscss java实现的lesscsss实时编译插件,可以由于jfinal
cn.dreampie.jfinal-coffeescript https://github.com/Dreampie/jfinal-coffeescript java实现的coffeescript实时编译插件,可以由于jfinal
cn.dreampie.jfinal-akka https://github.com/Dreampie/jfinal-akka java使用akka执行异步任务
cn.dreampie.jfinal-mailer https://github.com/Dreampie/jfinal-mailer 使用akka发布邮件的jfinal插件
cn.dreampie.jfinal-slf4j https://github.com/Dreampie/jfinal-slf4j 让jfinal使用slf4j的日志api
部分内容借鉴了网络资料