管理后台的权限管理模块搭建中使用了shiro框架。
shiro的优点是:相对Spring Security较为轻巧,使用起来自由度大,和Spring框架结合的方式也很成熟。缺点是:shiro本身没实现缓存,需要自己定义缓存实现,更新比较慢,有的功能需要自己拓展。
shiro文档:http://shiro.apache.org/static/1.2.3/apidocs/
十分钟入门:http://shiro.apache.org/10-minute-tutorial.html
以下总结在项目中使用shiro的方法和管理后台项目中对shiro的拓展。
一、使用shiro管理权限
1. 引入shiro需要的包。使用maven的项目中,在pom.xml增加以下依赖:
- <!-- shiro权限管理 -->
- <dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-core</artifactId>
- <version>1.1.0</version>
- </dependency>
- <dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-web</artifactId>
- <version>1.1.0</version>
- </dependency>
- <dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-spring</artifactId>
- <version>1.1.0</version>
- </dependency>
- <!-- shiro权限管理 end -->
2. 在项目中增加shiro配置。
在spring配置文件目录下新建spring-shiro.xml。内容如下:
- <!-- Shiro权限管理配置 -->
- <bean id="shiroFilter"
- class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!-- shiro通过一个filter控制权限-->
- <property name="securityManager" ref="securityManager" />
- <property name="loginUrl" value="/login/execute.do" /> <!-- 登陆页 -->
- <property name="successUrl" value="/index.jsp" /> <!-- 登陆成功之后跳转的页面 -->
- <property name="unauthorizedUrl" value="/login/execute.do" /> <!-- 用户在请求无权限的资源时,跳转到这个url -->
- <property name="filters">
- <util:map>
- <entry key="authc">
- <bean class="org.apache.shiro.web.filter.authc.PassThruAuthenticationFilter" />
- </entry>
- </util:map>
- </property>
- <property name="filterChainDefinitions"> <!-- 配置访问url资源需要用户拥有什么权限 配置的优先级由上至下-->
- <value>
- /=anon
- /template/main.jsp=user
- <!-- api用户信息 -->
- /api/createApiUser**=perms[api:user:create] <!-- 权限可以用":"分级,如拥有api权限相当于拥有api*权限(父权限涵盖子权限) -->
- /api/updateApiUser**=perms[api:user:update]
- /api/*User*=perms[api:user:view]
- /template/apiUserManage/**=perms[api:user:view]
- <!-- api接口管理 -->
- /api/*Interface*=perms[api:user:interface]
- <!-- api统计数据 -->
- /api/querySummaryData**=perms[api:data]
- /template/apiSumData/**=perms[api:data]
- /api/**=perms[api:*]
- ...
- /**=anon
- </value>
- </property>
- </bean>
- <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
- <property name="realm" ref="permissionsRealm" /> <!-- 自定义登陆及获取权限的源 -->
- </bean>
- <!-- shiro权限管理配置 end -->
- <context-param>
- <param-name>contextConfigLocation</param-name>
- <param-value>
- classpath*:applicationContext.xml,
- classpath*:spring-*.xml //此处引入了spring-shiro
- </param-value>
- </context-param>
- ...
- <filter>
- <filter-name>shiroFilter</filter-name>
- <filter-class>
- org.springframework.web.filter.DelegatingFilterProxy
- </filter-class>
- </filter>
- <filter-mapping>
- <filter-name>shiroFilter</filter-name>
- <url-pattern>/*</url-pattern>
- </filter-mapping>
3. 定义登陆及获取权限的源。
- /**
- * 认证实现类
- *
- * @author kexm
- *
- */
- @Service("permissionsRealm")
- public class PermissionsRealm extends AuthorizingRealm {
- @Autowired
- private AccountDao accountDao;
- @Autowired
- private GroupDao groupDao;
- private Account acc;
- private static LogUtil log = LogUtil.getLogger(PermissionsRealm.class);
- /**
- * 用户权限源(shiro调用此方法获取用户权限,至于从何处获取权限项,由我们定义。)
- */
- @Override
- protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
- SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
- log.info("method[doGetAuthorizationInfo] begin.");
- if (acc != null) {
- if(acc.getAdminType() == 2){//超级管理员 始终拥有所有权限
- info.addStringPermission("*");
- return info;
- }
- try {
- List<UserGroup> gList = accountDao.getUserGroups(acc.getLoginName());
- for (UserGroup g: gList) { //获取用户的组
- log.info("method[doGetAuthorizationInfo] group<" + g.getName() + ">");
- List<Permission> pList = groupDao.getGroupPerms(g.getId());
- for (Permission p: pList) { //获取组内权限
- log.info("method[doGetAuthorizationInfo] perm<" + p.getName() + "," + p.getPermList() + ">");
- String permList = p.getPermList();
- if (permList != null && !"".equals(permList)) {
- String[] perms = p.getPermList().split(",");
- for (String perm: perms) {//分别放入容器 (权限以字符串形式呈现,如"api:data"等,和spring-shiro.xml中的配置相对应)
- log.info("method[doGetAuthorizationInfo] add perm<" + perm + ">");
- info.addStringPermission(perm);
- }
- }
- }
- }
- return info;//将用户权限返回给shiro
- } catch (Exception e) {
- log.error("method[doGetAuthorizationInfo] e.message<" + e.getMessage() + "> e<" + e + ">", e);
- }
- }
- return null;
- }
- /**
- * 用户登录验证源(shiro调用此方法执行认证)
- */
- @Override
- protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authtoken) throws AuthenticationException {
- log.info("method[doGetAuthenticationInfo] begin.");
- UsernamePasswordToken token = (UsernamePasswordToken) authtoken;
- SimpleAuthenticationInfo authenticationInfo = null;
- String userName = token.getUsername();
- String password = new String(token.getPassword());
- Login conf = DefaultConfigure.config.getLogin();
- String MD5pwd = MD5Util.generateSignature(conf.getSalt(), password);
- try {
- if (userName != null && !"".equals(userName)) {
- acc = accountDao.login(userName, MD5pwd);
- }
- if (acc != null) {
- doGetAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
- authenticationInfo = new SimpleAuthenticationInfo(token.getUsername(), token.getPassword(), getName());
- return authenticationInfo;
- }
- } catch (Exception e) {
- log.error("method[doGetAuthenticationInfo] acc<" + acc + "> message<" + e.getMessage() + "> e<" + e + ">",
- e);
- }
- return null;
- }
- }
3. shiro中,使用subject管理用户。可以把subject理解为shiro存储用户信息的容器和操纵用户的工具。有了前几步的配置,便可以使用以下代码登入登出,并享受shiro的url权限控制了。
- //登入
- UsernamePasswordToken token = new UsernamePasswordToken(loginName, password);
- Subject user = SecurityUtils.getSubject();
- user.login(token);
- //使用shiro自带的session存储用户信息 独立于httpSession
- Session ss = user.getSession().setAttribute("userInfo", acc);
- //登出
- SecurityUtils.getSubject().logout();
4. 在页面中使用shiro标签。假如我们要让有权限的用户看到某些菜单或按钮,可以用以下方式。
- <%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
- <shiro:hasPermission name="api:data">
- who has permission can see
- </shiro:hasPermission>
以上只使用了shiro的permission管理,shiro还支持对role的管理,如有进一步抽象的需求可以使用。
假如我们的web项目是分布式部署的,则需要让shiro把session和用户权限到放到集中缓存上去。Shiro本身不实现Cache,但是提供了接口,方便更换不同的底层Cache实现。
shiro提供的cache接口:
- public interface Cache<K, V> {
- //根据Key获取缓存中的值
- public V get(K key) throws CacheException;
- //往缓存中放入key-value,返回缓存中之前的值
- public V put(K key, V value) throws CacheException;
- //移除缓存中key对应的值,返回该值
- public V remove(K key) throws CacheException;
- //清空整个缓存
- public void clear() throws CacheException;
- //返回缓存大小
- public int size();
- //获取缓存中所有的key
- public Set<K> keys();
- //获取缓存中所有的value
- public Collection<V> values();
- }
观察接口可以发现,我们需要实现一个keys方法。这个方法限制了shiro不能使用缓存集群(SharedRedis不提供这个方法,只有单台redis可用keys方法,望找到解决方案)。
我们的项目使用redis作为集中缓存,shiro和redis结合的方式可以使用一个现成的工具——shiro-redis。
shiro-redis的github:https://github.com/alexxiyang/shiro-redis
目前管理后台项目正在使用此工具。这个工具有一处问题:读取缓存时没对读取的对象延长有效期,修复这个BUG之后还挺好用。
三、对shiro页面标签拓展,增加and or not 逻辑符
参考:http://jinnianshilongnian.iteye.com/blog/1864800
使用过shiro的朋友应该都知道在要想实现any permission的验证是比较麻烦。
很多朋友刚开始接触时以为如<shiro:hasPermission name="showcase:tree:*"> 代表验证用户是否拥有tree下的任何权限,但这是错误的。如果我们把showcase:tree:*授权给用户,那么此时表示用户具有showcase:tree资源的任意权限,如<shiro:hasPermission name="showcase:tree:*">或shiro:hasPermission name="showcase:tree:create">都能验证成功。
还有朋友认为<shiro:hasPermission name="showcase:tree:create,update"> 是或的关系,也不是,默认是且的关系。
下载了最新的shiro1.3.0-SNAPSHOT 发现并没有增加新的标签或其他支持。
因此我们需要简单的扩展下shiro来支持像spring security 3那样的@Secured支持表达式的强大注解。我们扩展AuthorizingRealm,并修改:
- private static final String OR_OPERATOR = " or ";
- private static final String AND_OPERATOR = " and ";
- private static final String NOT_OPERATOR = "not ";
- /**
- * 支持or and not 关键词 不支持and or混用
- * @param principals
- * @param permission
- * @return
- */
- public boolean isPermitted(PrincipalCollection principals, String permission) {
- if(permission.contains(OR_OPERATOR)) {
- String[] permissions = permission.split(OR_OPERATOR);
- for(String orPermission : permissions) {
- if(isPermittedWithNotOperator(principals, orPermission)) {
- return true;
- }
- }
- return false;
- } else if(permission.contains(AND_OPERATOR)) {
- String[] permissions = permission.split(AND_OPERATOR);
- for(String orPermission : permissions) {
- if(!isPermittedWithNotOperator(principals, orPermission)) {
- return false;
- }
- }
- return true;
- } else {
- return isPermittedWithNotOperator(principals, permission);
- }
- }
- private boolean isPermittedWithNotOperator(PrincipalCollection principals, String permission) {
- if(permission.startsWith(NOT_OPERATOR)) {
- return !super.isPermitted(principals, permission.substring(NOT_OPERATOR.length()));
- } else {
- return super.isPermitted(principals, permission);
- }
- }
如上代码即可以实现简单的NOT、AND、OR支持,不过缺点是不支持复杂的如AND、OR组合。
如下标签在拓展后可以生效:
- <shiro:hasPermission name="api:data or api:user"></shiro:hasPermission>
- <pre name="code" class="html"><shiro:hasPermission name="api:data and api:user"></shiro:hasPermission>
- <shiro:hasPermission name="not api:data"></shiro:hasPermission>