不少朋友都问过我一样一个问题:“Smart 目前有身份认证与权限管理等安全控制功能吗?”html
当听到这样的问题时,我真的很是很差意思,实在是没有这方面的特性。不过当我学习了 Shiro 之后,让我萌发了一个想法:前端
可否提供一个更加 Smart 的 Shiro 框架呢? java
你们知道,Smart 是一款轻量级 Java Web 开发框架,此外,Smart 还提供了一系列的模块,以前开发过一款 Smart SSO 模块,它是不依赖于 Smart 框架的,能够在任何的 Web 项目中使用。mysql
那么,可否再开发一款 Smart Security 模块呢?一样它也不依赖于 Smart 框架,仍然能够在其它 Web 项目中使用。也就是说,只须要拿到 smart-security.jar 这个包,并将其放入到 lib 目录下,最后只需少许的配置便可使用 Shiro 提供的安全控制功能。 git
为达到以上的目标,我首先建立了一个应用场景:web
用户必须登陆成功后,才能看到用户空间页面,不然跳转到登陆页面要求用户进行身份认证。 spring
这应该是一个很典型的案例,咱们如何在普通的 Web 应用中使用 Shiro 呢?sql
首先,须要在 web.xml 中作以下配置:apache
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <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> </filter-mapping> </web-app>
而后,须要在 classpath 下或 WEB-INF 目录下新增一个 shiro.ini 文件: 编程
[main] authc.loginUrl=/login ds = org.apache.commons.dbcp.BasicDataSource ds.driverClassName = com.mysql.jdbc.Driver ds.url = jdbc:mysql://localhost:3306/sample ds.username = root ds.password = root passwordMatcher = org.apache.shiro.authc.credential.PasswordMatcher jdbcRealm = org.apache.shiro.realm.jdbc.JdbcRealm jdbcRealm.dataSource = $ds jdbcRealm.authenticationQuery = select password from user where username = ? jdbcRealm.userRolesQuery = select r.role_name from user u, user_role ur, role r where u.id = ur.user_id and r.id = ur.role_id and u.username = ? jdbcRealm.permissionsQuery = select p.permission_name from role r, role_permission rp, permission p where r.id = rp.role_id and p.id = rp.permission_id and r.role_name = ? jdbcRealm.permissionsLookupEnabled = true jdbcRealm.credentialsMatcher = $passwordMatcher securityManager.realms = $jdbcRealm cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager securityManager.cacheManager = $cacheManager [urls] / = anon /space/** = authc
以上使用了 Shiro 的 JdbcRealm 来提供认证与受权服务,并提供了 Cache 功能,此外还有密码安全支持。
最后,就能够在代码里使用 Shiro 提供的 API 进行编程了。
Shiro 提供了几个经常使用 API,掌握了这些 API 的用法,基本上就会用 Shiro 了,这些 API 包括:
org.apache.shiro.SecurityUtils
org.apache.shiro.subject.Subject
org.apache.shiro.authc.UsernamePasswordToken
org.apache.shiro.authc.AuthenticationException
org.apache.shiro.authc.credential.PasswordService
org.apache.shiro.authc.credential.DefaultPasswordService
总结一下,咱们须要在 web.xml 中整合 Shiro(一大堆的配置),须要提供一个 Shiro 的配置文件(又是一大堆配置),还须要在代码里使用 Shiro API(须要学习 Shiro 文档及其 JavaDoc)。
这简直就是在自找麻烦!
你们应该知道,Spring 有较强的系统整合能力,将 Shiro 整合到 Spring 中是否会变得简单呢?
首先,须要在 web.xml 中作以下配置:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:spring.xml classpath:spring-shiro.xml </param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
须要经过 ContextLoaderListener 这个监听器来读取 spring.xml 与 spring-shiro.xml 这两个 Spring 配置文件,而 Spring MVC 框架须要读取 spring-mvc.xml 配置文件。
Spring 的 DelegatingFilterProxy 将拦截全部的请求,并将请求的委托给 ShiroFilter 来处理。
要实现请求委托,须要在 spring-shiro.xml 中作以下配置:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login"/> <property name="filterChainDefinitions"> <value> / = anon /space/** = authc </value> </property> </bean> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="jdbcRealm"/> <property name="cacheManager" ref="cacheManager"/> </bean> <bean id="jdbcRealm" class="org.apache.shiro.realm.jdbc.JdbcRealm"> <property name="dataSource" ref="dataSource"/> <property name="authenticationQuery" value="select password from user where username = ?"/> <property name="userRolesQuery" value="select r.role_name from user u, user_role ur, role r where u.id = ur.user_id and r.id = ur.role_id and u.username = ?"/> <property name="permissionsQuery" value="select p.permission_name from role r, role_permission rp, permission p where r.id = rp.role_id and p.id = rp.permission_id and r.role_name = ?"/> <property name="permissionsLookupEnabled" value="true"/> <property name="cacheManager" ref="cacheManager"/> <property name="credentialsMatcher" ref="passwordMatcher"/> </bean> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/> <bean id="passwordMatcher" class="org.apache.shiro.authc.credential.PasswordMatcher"/> <bean id="passwordService" class="org.apache.shiro.authc.credential.DefaultPasswordService"/> </beans>
首先,经过 Spring 的 ShiroFilterFactoryBean 来建立 ShiroFilter,并在其中注入了 SecurityManager,此外只需在这里配置 loginUrl 与相关的 Filter Chain 便可。
随后,提供了相关 Bean 的定义,包括:SecurityManager、JdbcRealm、LifecycleBeanPostProcessor、CacheManager、PasswordMatcher、PasswordService 等。
须要注意的是,这里同时将 cacheManager 注入到 securityManager 与 jdbcRealm 中才能让 Cache 生效(目前还没有阅读源码,缘由不详)。
必须提供 LifecycleBeanPostProcessor 才能让 Web 容器启动与关闭时调用 Shrio 的生命周期方法,同时须要确保 web.xml 中的 DelegatingFilterProxy 的 targetFilterLifecycle 参数要为 true 才行。
最后,其中 PasswordMatcher 与 PasswordService 是为密码加密与解密提供服务的。
看来 Spring 只是将 shiro.ini 中的配置转移到 spring-shiro.xml 中了,并无对 Shiro 提供一个很好的封装,只是简单的集成而已。
通常状况下,咱们不想作太多的配置,可以提供一个简单的安全控制功能便可,咱们不须要它应有尽有,而须要它可以简单而实用。
这就是个人想法:
可否对 Shiro 的经常使用功能作一个封装呢?也就是说,只需对外提供统一的 API,让底层的具体实现变得更加透明,若是未来打算使用其它安全框架,只需改造这个框架便可,这样就无需在每一个项目中进行重构了。
它就是 Smart Security 模块。
首先须要申明一下 Smart 模块的设计原则:
1. 提供最实用的功能,忽略不经常使用的功能,但须要提供扩展机制。
2. 隐藏具体实现细节,底层实现能够任意切换,从而不会影响到应用程序的改动。
3. 能够做为一个独立的模块,无需在 web.xml 里作任何配置,直接把 jar 包拿来就能用。
Smart Security 彻底遵循以上设计原则。
使用了 Smart Security 以后,不再须要配置 web.xml 了。
此外,shiro.ini 也简单了:
[main] authc.loginUrl = /login [urls] / = anon /space/** = authc
其它的配置将转移到 smart.properties 中:
app.package = demo.shiro app.home_page = /index.jsp jdbc.type = mysql jdbc.driver = com.mysql.jdbc.Driver jdbc.url = jdbc:mysql://localhost:3306/sample jdbc.username = root jdbc.password = root security.realms = jdbc security.jdbc.authc_query = select password from user where username = ? security.jdbc.roles_query = select r.role_name from user u, user_role ur, role r where u.id = ur.user_id and r.id = ur.role_id and u.username = ? security.jdbc.perms_query = select p.permission_name from role r, role_permission rp, permission p where r.id = rp.role_id and p.id = rp.permission_id and r.role_name = ? security.cache = true
注意以上 security 开头的配置项,这样的配置是否更加简单呢?
Smart Security 提供了一个 SmartSecurityHelper 类,包括如下静态方法:
public class SmartSecurityHelper { public static void login(String username, String password, boolean isRememberMe) throws LoginException { ... } public static void logout() { ... } public static String encrypt(String plaintext) { ... } }
固然还能够扩展其它实用 API,目前只提供了几个经常使用方法。
咱们能够在业务代码中这样使用:
@Service public class UserServiceImpl implements UserService { @Override public void login(String username, String password, boolean isRememberMe) throws LoginException { SmartSecurityHelper.login(username, password, isRememberMe); } @Override public void register(Map<String, Object> fieldMap) throws RegisterException { // 获取表单数据 String username = CastUtil.castString(fieldMap.get("username")); String password = CastUtil.castString(fieldMap.get("password")); // 在 user 表中根据 username 查询用户是否已存在 Long userCount = DataSet.selectCount(User.class, "username = ?", username); if (userCount > 0) { throw new RegisterException(); } // 加密密码 password = SmartSecurityHelper.encrypt(password); // 插入 user 表 User user = new User(); user.setUsername(username); user.setPassword(password); DataSet.insert(user); } }
这样咱们就经过 SmartSecurityHelper 屏蔽了 Shiro API 的细节了,代码量也精简了许多。
你们或许已经见识过 Shrio 的 JSP 标签了,不错,确实提供了不少有用的标签,但彷佛又缺乏了几个,致使咱们不得不自行扩展,这样就会引用咱们自定义的 Taglib,从而增长了前端开发人员的负担。
为了解决这个问题,不妨从新将 Shiro 的标签作一个整理,添加咱们自定义的标签,这样前端开发人员只需面对 Smart Security 的 Taglib 就能工做了,若是不够用,咱们能够随意扩展。
只需在 JSP 上使用如下 Taglib 定义便可:
<%@ taglib prefix="security" uri="/smart_security" %>
Smart Security 提供了一套比 Shiro 更为详尽的标签(少许扩展,大多数来自于 Shiro):
类型 | 标签 | 属性 | 功能 |
用户 |
<security:user> | 无 | 判断当前用户是否已登陆(已认证 或 已记住) |
<security:guest> | 无 | 判断当前用户是否未登陆(为游客身份) | |
<security:authenticated> | 无 | 判断当前用户是否已认证 | |
<security:notAuthenticated> | 无 | 判断当前用户是否未认证 | |
<security:principal> | type、property、defaultValue | 显示当前用户的相关属性 | |
角色 | <security:hasRole> | name | 判断当前用户是否拥有某种角色 |
<security:lacksRole> | name | 判断当前用户是否缺乏某种角色 | |
<security:hasAnyRoles> | name | 判断当前用户是否拥有其中某一种角色(逗号分隔,或的关系) | |
<security:hasAllRoles> | name | 判断当前用户是否拥有其中全部的角色(逗号分隔,与的关系) | |
权限 | <security:hasPermission> | name | 判断当前用户是否拥有某种权限 |
<security:lacksPermission> | name | 判断当前用户是否缺乏某种权限 | |
<security:hasAnyPermissions> | name | 判断当前用户是否拥有其中某一种权限(逗号分隔,或的关系) | |
<security:hasAllPermissions> | name | 判断当前用户是否拥有其中全部的权限(逗号分隔,与的关系) |
在 JSP 中能够这样使用:
<%@ page pageEncoding="UTF-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="security" uri="/smart_security" %> <html> <head> <title>首页</title> </head> <body> <h1>首页</h1> <security:guest> <p>身份:游客</p> <a href="<c:url value="/login"/>">登陆</a> <a href="<c:url value="/register"/>">注册</a> </security:guest> <security:user> <p>身份:<security:principal/></p> <a href="<c:url value="/space"/>">空间</a> <a href="<c:url value="/logout"/>">退出</a> </security:user> </body> </html>
那么 Smart Security 应该如何实现呢?我将这个想法作了一个尝试:
http://git.oschina.net/huangyong/smart/tree/master/smart-security
此外,还有一个 Shiro Demo:
http://git.oschina.net/huangyong/shiro_demo
期待您的点评!
补充(2014-04-01)
通过今天上午你们在 QQ 群里的一番激烈讨论后,获得以下反馈:
1. 大多数人不喜欢在 properties 文件里定义那些 SQL 语句
2. Smart Security 能够不用依赖于 DBCP 链接池
今天下午,我开发了一个新特性,在 Smart Security 中提供了一个 ISmartSecurity 接口:
/** * Smart Security 接口 */ public interface ISmartSecurity { /** * 根据用户名获取密码 * * @param username 用户名 * @return 密码 */ String getPassword(String username); /** * 根据用户名获取角色名集合 * * @param username 用户名 * @return 角色名集合 */ Set<String> getRoleNameSet(String username); /** * 根据角色名获取权限名集合 * * @param roleName 角色名 * @return 权限名集合 */ Set<String> getPermNameSet(String roleName); }
在实际的项目中须要实现该接口,并将以前写在 properties 文件中的 SQL 语句放入实现类中执行,下面用 Smart API 写了一个实现类:
public class SmartSecurity implements ISmartSecurity { @Override public String getPassword(String username) { String sql = "select password from user where username = ?"; return DatabaseHelper.queryColumn(sql, username); } @Override public Set<String> getRoleNameSet(String username) { String sql = "select r.role_name from user u, user_role ur, role r where u.id = ur.user_id and r.id = ur.role_id and u.username = ?"; return DatabaseHelper.queryColumnSet(sql, username); } @Override public Set<String> getPermNameSet(String roleName) { String sql = "select p.permission_name from role r, role_permission rp, permission p where r.id = rp.role_id and p.id = rp.permission_id and r.role_name = ?"; return DatabaseHelper.queryColumnSet(sql, roleName); } }
只须要简单经过 Smart 提供的 DatabaseHelper 类便可执行这些 SQL 语句并返回相关结果。
最后须要作的事情,就是配置 smart.properties 了:
security.realms = custom security.custom.class = demo.shiro.SmartSecurity security.cache = true
只需三行配置便可,实际上只有两行,最后一行是可选的。
目前 security.realms 包括三种:jdbc、ad、custom,下表是详细的配置方式:
配置项 security.realms 种类 |
相关配置项 |
jdbc | security.jdbc.authc_query security.jdbc.roles_query security.jdbc.perms_query |
ad | security.ad.url security.ad.system_username security.ad.system_password security.ad.search_base |
custom | security.custom.class |
此外,security.realms 可同时配置多个 Realm,用英文逗号分隔。
感谢你们提供的建议!尤为感谢 @哈库纳 的建议。
有兴趣的朋友欢迎加群讨论:120404320