1.前言
1.1 shiro介紹
Authentication:身份認證/登入,驗證使用者是不是擁有相應的身份;
Authorization:授權,即許可權驗證,驗證某個已認證的使用者是否擁有某個許可權;即判斷使用者是否能做事情,常見的如:驗證某個使用者是否擁有某個角色。或者細粒度的驗證某個使用者對某個資源是否具有某個許可權;
Session Manager:會話管理,即使用者登入後就是一次會話,在沒有退出之前,它的所有資訊都在會話中;會話可以是普通JavaSE環境的,也可以是如Web環境的;
Cryptography:加密,保護資料的安全性,如密碼加密儲存到資料庫,而不是明文儲存;
Web Support:Web支援,可以非常容易的整合到Web環境;
Caching:緩衝,比如使用者登入後,其使用者資訊、擁有的角色/許可權不必每次去查,這樣可以提高效率;
Concurrency:shiro支援多線程應用的並發驗證,即如在一個線程中開啟另一個線程,能把許可權自動傳播過去;
Testing:提供測試支援;
Run As:允許一個使用者假裝為另一個使用者(如果他們允許)的身份進行訪問;
Remember Me:記住我,這個是非常常見的功能,即一次登入後,下次再來的話不用登入了。
1.2使用api
Subject:主體,代表了當前“使用者”,這個使用者不一定是一個具體的人,與當前應用互動的任何東西都是Subject,如網路爬蟲,機器人等;即一個抽象概念;所有Subject都綁定到SecurityManager,與Subject的所有互動都會委託給SecurityManager;可以把Subject認為是一個門面;SecurityManager才是實際的執行者;
SecurityManager:安全管理器;即所有與安全有關的操作都會與SecurityManager互動;且它管理著所有Subject;可以看出它是Shiro的核心,它負責與後邊介紹的其他組件進行互動,如果學習過SpringMVC,你可以把它看成DispatcherServlet前端控制器;
Realm:域,Shiro從Realm擷取安全資料(如使用者、角色、許可權),就是說SecurityManager要驗證使用者身份,那麼它需要從Realm擷取相應的使用者進行比較以確定使用者身份是否合法;也需要從Realm得到使用者相應的角色/許可權進行驗證使用者是否能進行操作;可以把Realm看成DataSource,即安全資料來源。
也就是說對於我們而言,最簡單的一個Shiro應用:
1、應用代碼通過Subject來進行認證和授權,而Subject又委託給SecurityManager;
2、我們需要給Shiro的SecurityManager注入Realm,從而讓SecurityManager能得到合法的使用者及其許可權進行判斷。
2.整合項目 2.1 依賴
<!-- shiro --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.2.3</version> </dependency>
2.2 shiro-context.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-3.0.xsd"> <!--自訂許可權認證--> <bean id="authRealm" class="com.tl.skyLine.shiro.AuthRealm"> <!--自訂密碼密碼編譯演算法 --> <!--<property name="credentialsMatcher" ref="passwordMatcher"/>--> <!--<property name="roleDao" ref="roleDao"/>--> <!--<property name="permissionDao" ref="permissionDao"/>--> </bean> <!-- 設定密碼加密策略 md5hash --> <!--<bean id="passwordMatcher" class="com.fyh.www.shiro.CustomCredentialsMatcher"/>--> <!-- 配置許可權管理器(核心) --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- 我們自訂的realm --> <property name="realm" ref="authRealm"/> <!-- 緩衝管理器 --> <property name="cacheManager" ref="cacheManager"/> </bean> <!-- 此bean要被web.xml引用,和web.shiroFilter --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!-- 許可權管理器 --> <property name="securityManager" ref="securityManager"/> <!-- 登入地址 --> <property name="loginUrl" value="/pages/login.jsp"/> <!-- 登入後跳轉到業務頁面 --> <property name="successUrl" value="/pages/home.jsp"/> <!-- 錯誤頁面 --> <property name="unauthorizedUrl" value="/pages/unauthorized.jsp"/> <!-- 許可權配置 --> <property name="filterChainDefinitions"> <value> <!-- 匿名登入請求 --> /public/** = anon <!-- **代表任意子目錄 --> /static/**=anon <!-- 需要許可權為edit的使用者才能訪問此請求--> /user/edit=perms[user:edit] <!-- 需要管理員角色才能訪問此頁面 --> <!--/user/edit=roles[admin]--> <!--攔截非靜態資源的所有請求--> /** = authc <!--authc:確保已認證的使用者發送的請求才能通過(若未認證,則跳轉到登入頁面)--> </value> </property> </bean> <!-- 使用者授權/認證資訊Cache, 採用EhCache 緩衝 --> <!--<bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">--> <!--<property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml"/>--> <!--</bean>--> <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/> <!-- 保證實現了Shiro內部lifecycle函數的bean執行 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <!-- 安全管理器 --> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean> <!--啟用Shiro相關的註解--> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean></beans>
2.3 匯入spring-context.xml和springmvc-context.xml中
<import resource="shiro-context.xml"/>
這邊對之前的配置要做一些修改,涉及到spring父子容器的查看範圍的問題,參考下一篇文章
點擊開啟連結
修改的spring-context.xml:
<context:component-scan base-package="com.tl.skyLine"> <!-- 排除不掃描的,controller放在子容器springmvc的設定檔中 --> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan>
修改的springmvc-context.xml:
<!--只在springmvc容器中配置controller的掃描--> <context:component-scan base-package="com.tl.skyLine" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan>
2.4 web.xml
<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>
這個shiroFilter與shiro-context.xml中的<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">對應。
2.5 自訂AuthRealm
package com.tl.skyLine.shiro;import com.tl.skyLine.model.User;import com.tl.skyLine.repository.PermissionDao;import com.tl.skyLine.repository.RoleDao;import com.tl.skyLine.repository.UserDao;import org.apache.shiro.authc.*;import org.apache.shiro.authz.AuthorizationInfo;import org.apache.shiro.authz.SimpleAuthorizationInfo;import org.apache.shiro.realm.AuthorizingRealm;import org.apache.shiro.subject.PrincipalCollection;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.util.stream.Collectors;/** * Created by tl on 17/2/20. */@Componentpublic class AuthRealm extends AuthorizingRealm { @Autowired private RoleDao roleDao; @Autowired private PermissionDao permissionDao; @Autowired private UserDao userDao; /** * 用來為當前登陸成功的使用者授予許可權和角色(已經登陸成功了) */ @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { //擷取使用者名稱 //String username = (String) principals.getPrimaryPrincipal(); //擷取目前使用者 User user = (User) principals.fromRealm(getName()).iterator().next(); //得到許可權字串 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addRoles(roleDao.getRoles(user.getId()) .stream().map(role -> role.getName()).collect(Collectors.toList())); info.addStringPermissions(permissionDao.getPermissionByUser(user.getId()) .stream().map(permission -> permission.getName()).collect(Collectors.toList())); return info; } /** * 用來驗證當前登入的使用者,擷取認證資訊 */ @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken authcToken) throws AuthenticationException { UsernamePasswordToken upToken = (UsernamePasswordToken) authcToken; User user = userDao.findOneByUsername(upToken.getUsername()); if (user == null) { return null; } else { AuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), getName()); return info; } }}
在認證、授權內部實現機制中都有提到,最終處理都將交給Realm進行處理。 因為在Shiro中,最終是通過Realm來擷取應用程式中的使用者、角色及許可權資訊的。通常情況下,在Realm中會直接從我們的資料來源中擷取Shiro需要的驗證資訊。 可以說,Realm是專用於安全架構的DAO.
shiro登陸成功,再次訪問url的時候會請求doGetAuthorizationInfo時候會將登陸使用者對應的角色,許可權全部查到,然後set到SimpleAuthorizationInfo執行個體對象info中,儲存的分別是role和permission對象的name欄位,
這個name子段跟shiro-context.xml中的shiroFilterbean對象的filterChainDefinitions屬性對應:
2.6 角色權限類別的結構
我這邊是用mongodb資料庫,沒有外部索引鍵關聯約束,就是基本的使用者,角色,許可權,然後外加兩個中間表,我就不一一貼出了。 2.7 repository
由於非關聯式資料庫的約束性,這邊做表關聯查詢沒有關係型資料庫那麼簡單,這個對整體架構沒有影響,我把這個代碼貼一下:
RoleDaoImpl:
package com.tl.skyLine.repository.impl;import com.tl.skyLine.model.Role;import com.tl.skyLine.model.UserRole;import com.tl.skyLine.repository.RoleDao;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.mongodb.core.MongoTemplate;import org.springframework.data.mongodb.core.query.Criteria;import org.springframework.data.mongodb.core.query.Query;import org.springframework.stereotype.Component;import java.util.ArrayList;import java.util.List;/** * RoleDaoImpl * Created by tl on 17/2/13. */@Component("roleDao")public class RoleDaoImpl implements RoleDao { @Autowired private MongoTemplate mongoTemplate; @Override public Role findOne(String roleId) { return this.mongoTemplate.findOne(new Query().addCriteria(Criteria.where("id").is(roleId)), Role.class); } @Override public List<Role> getRoles(String userId) { Query query = new Query(); query.addCriteria(Criteria.where("userId").is(userId)); List<UserRole> userRoles = this.mongoTemplate.find(query, UserRole.class); List<Role> roles = new ArrayList<Role>(); userRoles.stream().forEach(userRole -> { roles.add(this.findOne(userRole.getRoleId())); }); return roles; } @Override public void store(Role role) { this.mongoTemplate.save(role); } @Override public void store(UserRole userRole) { this.mongoTemplate.save(userRole); } @Override public Role findOneByName(String name) { return this.mongoTemplate.findOne( new Query().addCriteria(Criteria.where("name").is(name)) , Role.class); }}
PermissionImpl:
package com.tl.skyLine.repository.impl;import com.tl.skyLine.model.Permission;import com.tl.skyLine.model.RolePermission;import com.tl.skyLine.repository.PermissionDao;import com.tl.skyLine.repository.RoleDao;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.mongodb.core.MongoTemplate;import org.springframework.data.mongodb.core.query.Criteria;import org.springframework.data.mongodb.core.query.Query;import org.springframework.stereotype.Component;import java.util.ArrayList;import java.util.List;/** * PermissionDaoImpl * Created by tl on 17/2/13. */@Component("permissionDao")public class PermissionDaoImpl implements PermissionDao { @Autowired private MongoTemplate mongoTemplate; @Autowired private RoleDao roleDao; @Override public Permission findOne(String permissionId) { return this.mongoTemplate.findOne(new Query().addCriteria(Criteria.where("id").is(permissionId)), Permission.class); } @Override public List<Permission> getPermissionByRole(String roleId) { Query query = new Query(); query.addCriteria(Criteria.where("roleId").is(roleId)); List<RolePermission> rolePermissions = this.mongoTemplate.find(query, RolePermission.class); List<Permission> permissions = new ArrayList<Permission>(); rolePermissions.stream().forEach(rolePermission -> { permissions.add(this.findOne(rolePermission.getPermissionId())); }); return permissions; } @Override public List<Permission> getPermissionByUser(String userId) { List<Permission> permissions = new ArrayList<Permission>(); roleDao.getRoles(userId).stream().forEach(role -> { permissions.addAll(this.getPermissionByRole(role.getId())); }); return permissions; } @Override public void store(Permission permission) { this.mongoTemplate.save(permission); } @Override public void store(RolePermission rolePermission) { this.mongoTemplate.save(rolePermission); } @Override public Permission findOneByName(String name) { return this.mongoTemplate.findOne( new Query().addCriteria(Criteria.where("name").is(name)) , Permission.class); }}
2.8 單元測試增加測試資料
/** * 關聯使用者角色許可權 */ @Test public void testShiro() { Role role = new Role(); role.setName("admin"); role.setDescription("管理員權限"); roleDao.store(role); UserRole userRole = new UserRole(); userRole.setRoleId(role.getId()); userRole.setUserId(userDao.findOneByUsername("admin").getId()); roleDao.store(userRole); Permission permission = new Permission(); permission.setName("edit"); permission.setDescription("編輯許可權"); permissionDao.store(permission); RolePermission rolePermission = new RolePermission(); rolePermission.setRoleId(role.getId()); rolePermission.setPermissionId(permission.getId()); permissionDao.store(rolePermission); }
2.9 controller類和jsp
PublicController:
/** * PublicController * Created by tl on 17/2/13. */@Controller@RequestMapping("/public")public class PublicController { @Autowired private UserDao userDao; //使用者登入 @RequestMapping("/login") public String login(User user, HttpServletRequest request) { Subject subject = SecurityUtils.getSubject(); userDao.findAll(); UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword()); try { subject.login(token);//會跳到我們自訂的realm中 request.getSession().setAttribute("user", user); return "success"; } catch (Exception e) { e.printStackTrace(); request.getSession().setAttribute("user", user); request.setAttribute("error", "使用者名稱或密碼錯誤。"); return "login"; } } @RequestMapping("/logout") public String logout(HttpServletRequest request) { request.getSession().invalidate(); return "index"; }}
login.jsp
<!DOCTYPE html><%@ page language="java" contentType="text/html; charset=GB2312" %><html lang="en"><head> <title>登入頁面</title></head><body><% String path = request.getContextPath();// 獲得項目完全路徑(假設你的項目叫skyLine,那麼獲得到的地址就是http://localhost:8080/skyLine/): String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";%><form action="${basePath}/public/login" method="post"> <span>使用者名稱:</span><input type="text" name="username"/><br> <span>密碼:</span><input type="password" name="password"/><br> <input type="submit" value="登陸">${error}</form></body></html>
success.jsp
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %><!DOCTYPE html><%@ page language="java" contentType="text/html; charset=GB2312" %><html lang="en"><head> <title>success</title></head><body><p>登陸成功!歡迎你${user.username}</p><a href="${pageContext.request.contextPath}/user/list">進入使用者list頁面</a><shiro:hasPermission name="user:edit"> <p>你有許可權看到此處!</p></shiro:hasPermission></body></html>
上面的<shiro:hasPermission name="user:edit">註解,下節再講
點擊開啟連結 3.開始測試shiro許可權
首先項目啟動之後,不再運行web.xml這行代碼<welcome-file-list>,而是直接被shiroFilter過濾,執行shiro-context.xml設定檔中的<property name="loginUrl" value="/pages/login.jsp"/>,進入登入頁面,然後輸入登入資訊,進入publicController中的login方法,執行到subject.login(token);時會跳轉到我們自訂的realm中進行使用者名稱和密碼校正,成功跳轉success.jsp,失敗重新返回login.jsp頁面,同時顯示報錯資訊。
下面針對shiro-contex.xml中的url設定,訪問http://localhost:8080/user/edit,提示訪問成功,如果將設定檔的/user/edit=perms[user:edit]中的use:edit改成use:edit,重新啟動,再次訪問則會報錯,提示沒有存取權限
但是,上面的配置有一個局限性,就是每次在設定檔裡面加url許可權,會很麻煩,每次都要重啟,而且shiro許可權是細粒化的這種加配置的方法會產生很多代碼,是有意下文我們將介紹通過註解在controller和jsp中如何?許可權細粒化。