## 綜合實例

## 簡單數據字典
用戶 (sys\_user)
| 名稱 | 類型 | 長度 | 描述 |
| --- | --- | --- | --- |
| id | bigint | | 編號 主鍵 |
| username | varchar | 100 | 用戶名 |
| password | varchar | 100 | 密碼 |
| salt | varchar | 50 | 鹽 |
| role_ids | varchar | 100 | 角色列表 |
| locked | bool | | 賬戶是否鎖定 |
組織機構 (sys\_organization)
| 名稱 | 類型 | 長度 | 描述 |
| --- | --- | --- | --- |
| id | bigint | | 編號 主鍵 |
| name | varchar | 100 | 組織機構名 |
| priority | int | 100 | 顯示順序 |
| parent_id | bigint | 50 | 父編號 |
| parent_ids | varchar | 100 | 父編號列表 |
| available | bool | | 是否可用 |
資源 (sys\_resource)
| 名稱 | 類型 | 長度 | 描述 |
| --- | --- | --- | --- |
| id | bigint | | 編號 主鍵 |
| name | varchar | 100 | 資源名稱 |
| type | varchar | 50 | 資源類型 |
| priority | int | 100 | 顯示順序 |
| parent_id | bigint | 50 | 父編號 |
| parent_ids | varchar | 100 | 父編號列表 |
| permission | varchar | 100 | 權限字符串 |
| available | bool | | 是否可用 |
角色 (sys\_role)
| 名稱 | 類型 | 長度 | 描述 |
| --- | --- | --- | --- |
| id | bigint | | 編號 主鍵 |
| role | varchar | 100 | 角色名稱 |
| description | varchar | 100 | 角色描述 |
| resource_ids | varchar | 100 | 授權的資源 |
| available | bool | | 是否可用 |
**資源**:表示菜單元素、頁面按鈕元素等;菜單元素用來顯示界面菜單的,頁面按鈕是每個頁面可進行的操作,如新增、修改、刪除按鈕;使用 type 來區分元素類型(如 menu 表示菜單,button 代表按鈕),priority 是元素的排序,如菜單顯示順序;permission 表示權限;如用戶菜單使用 `user:*`;也就是把菜單授權給用戶后,用戶就擁有了 `user:*` 權限;如用戶新增按鈕使用 user:create,也就是把用戶新增按鈕授權給用戶后,用戶就擁有了 user:create 權限了;available 表示資源是否可用,如菜單顯示 / 不顯示。
**角色**:role 表示角色標識符,如 admin,用于后臺判斷使用;description 表示角色描述,如超級管理員,用于前端顯示給用戶使用;resource\_ids 表示該角色擁有的資源列表,即該角色擁有的權限列表(顯示角色),即角色是權限字符串集合;available 表示角色是否可用。
**組織機構**:name 表示組織機構名稱,priority 是組織機構的排序,即顯示順序;available 表示組織機構是否可用。
**用戶**:username 表示用戶名;password 表示密碼;salt 表示加密密碼的鹽;role\_ids 表示用戶擁有的角色列表,可以通過角色再獲取其權限字符串列表;locked 表示用戶是否鎖定。
此處如資源、組織機構都是樹型結構:
| id | name | parent_id | parent_ids |
| --- | --- | --- | --- |
| 1 | 總公司 | 0 | 0/ |
| 2 | 山東分公司 | 1 | 0/1/ |
| 3 | 河北分公司 | 1 | 0/1/ |
| 4 | 濟南分公司 | 2 | 0/1/2/ |
parent\_id 表示父編號,parent\_ids 表示所有祖先編號;如 0/1/2/ 表示其祖先是 2、1、0;其中根節點父編號為 0。
為了簡單性,如用戶 - 角色,角色 - 資源關系直接在實體(用戶表中的 role\_ids,角色表中的 resource\_ids)里完成的,沒有建立多余的關系表,如要查詢擁有 admin 角色的用戶時,建議建立關聯表,否則就沒必要建立了。在存儲關系時如 role\_ids=1,2,3,;多個之間使用逗號分隔。
用戶組、組織機構組本實例沒有實現,即可以把一組權限授權給這些組,組中的用戶 / 組織機構就自動擁有這些角色 / 權限了;另外對于用戶組可以實現一個默認用戶組,如論壇,不管匿名 / 登錄用戶都有查看帖子的權限。
更復雜的權限請參考我的《JavaEE 項目開發腳手架》:[](http://github.com/zhangkaitao/es)<http://github.com/zhangkaitao/es>。
## 表 / 數據 SQL
具體請參考
* sql/ shiro-schema.sql (表結構)
* sql/ shiro-data.sql (初始數據)
默認用戶名 / 密碼是 admin/123456。
## 實體
具體請參考 com.github.zhangkaitao.shiro.chapter16.entity 包下的實體,此處就不列舉了。
## DAO
具體請參考 com.github.zhangkaitao.shiro.chapter16.dao 包下的 DAO 接口及實現。
## Service
具體請參考 com.github.zhangkaitao.shiro.chapter16.service 包下的 Service 接口及實現。以下是出了基本 CRUD 之外的關鍵接口:
``` java
public interface ResourceService {
Set<String> findPermissions(Set<Long> resourceIds); //得到資源對應的權限字符串
List<Resource> findMenus(Set<String> permissions); //根據用戶權限得到菜單
}
```
``` java
public interface RoleService {
Set<String> findRoles(Long... roleIds); //根據角色編號得到角色標識符列表
Set<String> findPermissions(Long[] roleIds); //根據角色編號得到權限字符串列表
}
```
``` java
public interface UserService {
public void changePassword(Long userId, String newPassword); //修改密碼
public User findByUsername(String username); //根據用戶名查找用戶
public Set<String> findRoles(String username);// 根據用戶名查找其角色
public Set<String> findPermissions(String username);// 根據用戶名查找其權限
}
```
Service 實現請參考源代碼,此處就不列舉了。
## UserRealm 實現
``` java
public class UserRealm extends AuthorizingRealm {
@Autowired private UserService userService;
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String)principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setRoles(userService.findRoles(username));
authorizationInfo.setStringPermissions(userService.findPermissions(username));
System.out.println(userService.findPermissions(username));
return authorizationInfo;
}
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String)token.getPrincipal();
User user = userService.findByUsername(username);
if(user == null) {
throw new UnknownAccountException();//沒找到帳號
}
if(Boolean.TRUE.equals(user.getLocked())) {
throw new LockedAccountException(); //帳號鎖定
}
return new SimpleAuthenticationInfo(
user.getUsername(), //用戶名
user.getPassword(), //密碼
ByteSource.Util.bytes(user.getCredentialsSalt()),//salt=username+salt
getName() //realm name
);
}
}
```
此處的 UserRealm 和《第六章 Realm 及相關對象》中的 UserRealm 類似,通過 UserService 獲取帳號及角色 / 權限信息。
## Web 層控制器
``` java
@Controller
public class IndexController {
@Autowired
private ResourceService resourceService;
@Autowired
private UserService userService;
@RequestMapping("/")
public String index(@CurrentUser User loginUser, Model model) {
Set<String> permissions = userService.findPermissions(loginUser.getUsername());
List<Resource> menus = resourceService.findMenus(permissions);
model.addAttribute("menus", menus);
return "index";
}
}
```
IndexController 中查詢菜單在前臺界面顯示,請參考相應的 jsp 頁面;
``` java
@Controller
public class LoginController {
@RequestMapping(value = "/login")
public String showLoginForm(HttpServletRequest req, Model model) {
String exceptionClassName = (String)req.getAttribute("shiroLoginFailure");
String error = null;
if(UnknownAccountException.class.getName().equals(exceptionClassName)) {
error = "用戶名/密碼錯誤";
} else if(IncorrectCredentialsException.class.getName().equals(exceptionClassName)) {
error = "用戶名/密碼錯誤";
} else if(exceptionClassName != null) {
error = "其他錯誤:" + exceptionClassName;
}
model.addAttribute("error", error);
return "login";
}
}
```
LoginController 用于顯示登錄表單頁面,其中 shiro authc 攔截器進行登錄,登錄失敗的話會把錯誤存到 shiroLoginFailure 屬性中,在該控制器中獲取后來顯示相應的錯誤信息。
``` java
@RequiresPermissions("resource:view")
@RequestMapping(method = RequestMethod.GET)
public String list(Model model) {
model.addAttribute("resourceList", resourceService.findAll());
return "resource/list";
}
```
在控制器方法上使用 @RequiresPermissions 指定需要的權限信息,其他的都是類似的,請參考源碼。
## Web 層標簽庫
com.github.zhangkaitao.shiro.chapter16.web.taglib.Functions 提供了函數標簽實現,有根據編號顯示資源 / 角色 / 組織機構名稱,其定義放在 src/main/webapp/tld/zhang-functions.tld。
## Web 層異常處理器
``` java
@ControllerAdvice
public class DefaultExceptionHandler {
@ExceptionHandler({UnauthorizedException.class})
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ModelAndView processUnauthenticatedException(NativeWebRequest request, UnauthorizedException e) {
ModelAndView mv = new ModelAndView();
mv.addObject("exception", e);
mv.setViewName("unauthorized");
return mv;
}
}
```
如果拋出 UnauthorizedException,將被該異常處理器截獲來顯示沒有權限信息。
## Spring 配置——spring-config.xml
定義了 context:component-scan 來掃描除 web 層的組件、dataSource(數據源)、事務管理器及事務切面等;具體請參考配置源碼。
## Spring 配置——spring-config-cache.xml
定義了 spring 通用 cache,使用 ehcache 實現;具體請參考配置源碼。
## Spring 配置——spring-config-shiro.xml
定義了 shiro 相關組件。
``` xml
<bean id="userRealm" class="com.github.zhangkaitao.shiro.chapter16.realm.UserRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"/>
<property name="cachingEnabled" value="false"/>
</bean>
```
userRealm 組件禁用掉了 cache,可以參考 [](https://github.com/zhangkaitao/es/tree/master/web/src/main/java/com/sishuok/es/extra/aop)<https://github.com/zhangkaitao/es/tree/master/web/src/main/java/com/sishuok/es/extra/aop> 實現自己的 cache 切面;否則需要在修改如資源 / 角色等信息時清理掉緩存。
``` xml
<bean id="sysUserFilter"
class="com.github.zhangkaitao.shiro.chapter16.web.shiro.filter.SysUserFilter"/>
```
sysUserFilter 用于根據當前登錄用戶身份獲取 User 信息放入 request;然后就可以通過 request 獲取 User。
``` xml
<property name="filterChainDefinitions">
<value>
/login = authc
/logout = logout
/authenticated = authc
/** = user,sysUser
</value>
</property>
```
如上是 shiroFilter 的 filterChainDefinitions 定義。
## Spring MVC 配置——spring-mvc.xml
定義了 spring mvc 相關組件。
``` xml
<mvc:annotation-driven>
<mvc:argument-resolvers>
<bean class="com.github.zhangkaitao.shiro.chapter16
.web.bind.method.CurrentUserMethodArgumentResolver"/>
</mvc:argument-resolvers>
</mvc:annotation-driven>
```
此處注冊了一個 @CurrentUser 參數解析器。如之前的 IndexController,從 request 獲取 shiro sysUser 攔截器放入的當前登錄 User 對象。
## Spring MVC 配置——spring-mvc-shiro.xml
定義了 spring mvc 相關組件。
``` xml
<aop:config proxy-target-class="true"></aop:config>
<bean class="org.apache.shiro.spring.security
.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
```
定義 aop 切面,用于代理如 @RequiresPermissions 注解的控制器,進行權限控制。
## web.xml 配置文件
定義 Spring ROOT 上下文加載器、ShiroFilter、及 SpringMVC 攔截器。具體請參考源碼。
## JSP 頁面
``` xml
<shiro:hasPermission name="user:create">
<a href="${pageContext.request.contextPath}/user/create">用戶新增</a><br/>
</shiro:hasPermission>
```
使用 shiro 標簽進行權限控制。具體請參考源碼。
## 系統截圖
訪問 [](http://localhost:8080/chapter16/)<http://localhost:8080/chapter16/>;
首先進入登錄頁面,輸入用戶名 / 密碼(默認 admin/123456)登錄:

登錄成功后到達整個頁面主頁,并根據當前用戶權限顯示相應的菜單,此處菜單比較簡單,沒有樹型結構顯示

然后就可以進行一些操作,如組織機構維護、用戶修改、資源維護、角色授權




## 相關資料
《跟我學 spring3》
[](http://www.iteye.com/blogs/subjects/spring3)<http://www.iteye.com/blogs/subjects/spring3>
《跟開濤學 SpringMVC》
[](http://www.iteye.com/blogs/subjects/kaitao-springmvc)<http://www.iteye.com/blogs/subjects/kaitao-springmvc>
《簡單 shiro 擴展實現 NOT、AND、OR 權限驗證》
[](http://jinnianshilongnian.iteye.com/blog/1864800)<http://jinnianshilongnian.iteye.com/blog/1864800>
《Shiro+Struts2+Spring3 加上 @RequiresPermissions 后 @Autowired 失效》
[](http://jinnianshilongnian.iteye.com/blog/1850425)<http://jinnianshilongnian.iteye.com/blog/1850425>
更復雜的權限請參考我的《JavaEE 項目開發腳手架》:[](http://github.com/zhangkaitao/es)<http://github.com/zhangkaitao/es>,提供了更加復雜的實現。
- 第1章 Shiro簡介
- 1.1 簡介
- 第2章 身份驗證
- 第3章 授權
- 第4章 INI配置
- 第5章 編碼 / 加密
- 第6章 Realm及相關對象
- 第7章 與Web集成
- 第8章 攔截器機制
- 第9章 JSP標簽
- 第10章 會話管理
- 第11章 緩存機制
- 第12章 與Spring集成
- 第13章 RememberMe
- 第14章 SSL
- 第15章 單點登錄
- 第16章 綜合實例
- 第17章 OAuth2集成
- 第18章 并發登錄人數控制
- 第19章 動態URL權限控制
- 第20章 無狀態Web應用集成
- 第21章 授予身份及切換身份
- 第22章 集成驗證碼
- 第23章 多項目集中權限管理及分布式會話
- 第24章 在線會話管理