不少朋友都問過我一樣一個問題:「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