在做一些企業內部項目時或一些互聯網后臺時;可能會涉及到集中權限管理,統一進行多項目的權限管理;另外也需要統一的會話管理,即實現單點身份認證和授權控制。
學習本章之前,請務必先學習《第十章 會話管理》和《第十六章 綜合實例》,本章代碼都是基于這兩章的代碼基礎上完成的。
本章示例是同域名的場景下完成的,如果跨域請參考《第十五章 單點登錄》和《第十七章 OAuth2 集成》了解使用 CAS 或 OAuth2 實現跨域的身份驗證和授權。另外比如客戶端 / 服務器端的安全校驗可參考《第二十章 無狀態 Web 應用集成》。
## 部署架構

1. 有三個應用:用于用戶 / 權限控制的 Server(端口:8080);兩個應用 App1(端口 9080)和 App2(端口 10080);
2. 使用 Nginx 反向代理這三個應用,nginx.conf 的 server 配置部分如下:
```
server {
listen 80;
server_name localhost;
charset utf-8;
location ~ ^/(chapter23-server)/ {
proxy_pass http://127.0.0.1:8080;
index /;
proxy_set_header Host $host;
}
location ~ ^/(chapter23-app1)/ {
proxy_pass http://127.0.0.1:9080;
index /;
proxy_set_header Host $host;
}
location ~ ^/(chapter23-app2)/ {
proxy_pass http://127.0.0.1:10080;
index /;
proxy_set_header Host $host;
}
}
```
如訪問 `http://localhost/chapter23-server` 會自動轉發到 `http://localhost:8080/chapter23-server`;
訪問 `http://localhost/chapter23-app1` 會自動轉發到 `http://localhost:9080/chapter23-app1`;
訪問 `http://localhost/chapter23-app3` 會自動轉發到 `http://localhost:10080/chapter23-app3`;
Nginx 的安裝及使用請自行搜索學習,本文不再闡述。
## 項目架構

1. 首先通過用戶 / 權限 Server 維護用戶、應用、權限信息;數據都持久化到 MySQL 數據庫中;
2. 應用 App1 / 應用 App2 使用客戶端 Client 遠程調用用戶 / 權限 Server 獲取會話及權限信息。
此處使用 Mysql 存儲會話,而不是使用如 Memcached/Redis 之類的,主要目的是降低學習成本;如果換成如 Redis 也不會很難;如:

使用如 Redis 還一個好處就是無需在用戶 / 權限 Server 中開會話過期調度器,可以借助 Redis 自身的過期策略來完成。
## 模塊關系依賴


1、shiro-example-chapter23-pom 模塊:提供了其他所有模塊的依賴;這樣其他模塊直接繼承它即可,簡化依賴配置,如 shiro-example-chapter23-server:
``` xml
<parent>
<artifactId>shiro-example-chapter23-pom</artifactId>
<groupId>com.github.zhangkaitao</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
```
2、shiro-example-chapter23-core 模塊:提供給 shiro-example-chapter23-server、shiro-example-chapter23-client、`shiro-example-chapter23-app *` 模塊的核心依賴,比如遠程調用接口等;
3、shiro-example-chapter23-server 模塊:提供了用戶、應用、權限管理功能;
4、shiro-example-chapter23-client 模塊:提供給應用模塊獲取會話及應用對應的權限信息;
5、`shiro-example-chapter23-app *` 模塊:各個子應用,如一些內部管理系統應用;其登錄都跳到 shiro-example-chapter23-server 登錄;另外權限都從 shiro-example-chapter23-server 獲取(如通過遠程調用)。
## shiro-example-chapter23-pom 模塊
其 pom.xml 的 packaging 類型為 pom,并且在該 pom 中加入其他模塊需要的依賴,然后其他模塊只需要把該模塊設置為 parent 即可自動繼承這些依賴,如 shiro-example-chapter23-server 模塊:
``` xml
<parent>
<artifactId>shiro-example-chapter23-pom</artifactId>
<groupId>com.github.zhangkaitao</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
```
簡化其他模塊的依賴配置等。
## shiro-example-chapter23-core 模塊
提供了其他模塊共有的依賴,如遠程調用接口:
``` java
public interface RemoteServiceInterface {
public Session getSession(String appKey, Serializable sessionId);
Serializable createSession(Session session);
public void updateSession(String appKey, Session session);
public void deleteSession(String appKey, Session session);
public PermissionContext getPermissions(String appKey, String username);
}
```
提供了會話的 CRUD,及根據應用 key 和用戶名獲取權限上下文(包括角色和權限字符串);shiro-example-chapter23-server 模塊服務端實現;shiro-example-chapter23-client 模塊客戶端調用。
另外提供了 com.github.zhangkaitao.shiro.chapter23.core.ClientSavedRequest,其擴展了 org.apache.shiro.web.util.SavedRequest;用于 shiro-example-chapter23-app \* 模塊當訪問一些需要登錄的請求時,自動把請求保存下來,然后重定向到 shiro-example-chapter23-server 模塊登錄;登錄成功后再重定向回來;因為 SavedRequest 不保存 URL 中的 `schema://domain:port` 部分;所以才需要擴展 SavedRequest;使得 ClientSavedRequest 能保存 `schema://domain:port`;這樣才能從一個應用重定向另一個(要不然只能在一個應用內重定向):
```
public String getRequestUrl() {
String requestURI = getRequestURI();
if(backUrl != null) {//1
if(backUrl.toLowerCase().startsWith("http://") || backUrl.toLowerCase().startsWith("https://")) {
return backUrl;
} else if(!backUrl.startsWith(contextPath)) {//2
requestURI = contextPath + backUrl;
} else {//3
requestURI = backUrl;
}
}
StringBuilder requestUrl = new StringBuilder(scheme);//4
requestUrl.append("://");
requestUrl.append(domain);//5
//6
if("http".equalsIgnoreCase(scheme) && port != 80) {
requestUrl.append(":").append(String.valueOf(port));
} else if("https".equalsIgnoreCase(scheme) && port != 443) {
requestUrl.append(":").append(String.valueOf(port));
}
//7
requestUrl.append(requestURI);
//8
if (backUrl == null && getQueryString() != null) {
requestUrl.append("?").append(getQueryString());
}
return requestUrl.toString();
}
```
1. 如果從外部傳入了 successUrl(登錄成功之后重定向的地址),且以 `http://` 或 `https://` 開頭那么直接返回(相應的攔截器直接重定向到它即可);
2. 如果 successUrl 有值但沒有上下文,拼上上下文;
3. 否則,如果 successUrl 有值,直接賦值給 requestUrl 即可;否則,如果 successUrl 沒值,那么 requestUrl 就是當前請求的地址;
4. 拼上 url 前邊的 schema,如 http 或 https;
5. 拼上域名;
6. 拼上重定向到的地址(帶上下文);
7. 如果 successUrl 沒值,且有查詢參數,拼上;
8. 返回該地址,相應的攔截器直接重定向到它即可。
## shiro-example-chapter23-server 模塊
**簡單的實體關系圖**

**簡單數據字典**
用戶 (sys\_user)
| 名稱 | 類型 | 長度 | 描述 |
| --- | --- | --- | --- |
| id | bigint | | 編號 主鍵 |
| role | varchar | 100 | 角色名稱 |
| description | varchar | 100 | 角色描述 |
| resource_ids | varchar | 100 | 授權的資源 |
| available | bool | | 是否可用 |
應用 (sys\_app)
| 名稱 | 類型 | 長度 | 描述 |
| --- | --- | --- | --- |
| id | bigint | | 編號 主鍵 |
| role | varchar | 100 | 角色名稱 |
| description | varchar | 100 | 角色描述 |
| resource_ids | varchar | 100 | 授權的資源 |
| available | bool | | 是否可用 |
授權 (sys\_authorization)
| 名稱 | 類型 | 長度 | 描述 |
| --- | --- | --- | --- |
| id | bigint | | 編號 主鍵 |
| role | varchar | 100 | 角色名稱 |
| description | varchar | 100 | 角色描述 |
| resource_ids | varchar | 100 | 授權的資源 |
| available | bool | | 是否可用 |
**用戶**:比《第十六章 綜合實例》少了 role\_ids,因為本章是多項目集中權限管理;所以授權時需要指定相應的應用;而不是直接給用戶授權;所以不能在用戶中出現 role\_ids 了;
**應用**:所有集中權限的應用;在此處需要指定應用 key(app\_key) 和應用安全碼(app\_secret),app 在訪問 server 時需要指定自己的 app\_key 和用戶名來獲取該 app 對應用戶權限信息;另外 app\_secret 可以認為 app 的密碼,比如需要安全訪問時可以考慮使用它,可參考《第二十章 無狀態 Web 應用集成》。另外 available 屬性表示該應用當前是否開啟;如果 false 表示該應用當前不可用,即不能獲取到相應的權限信息。
**授權**:給指定的用戶在指定的 app 下授權,即角色是與用戶和 app 存在關聯關系。
因為本章使用了《第十六章 綜合實例》代碼,所以還有其他相應的表結構(本章未使用到)。
**表 / 數據 SQL**
具體請參考
* sql/shiro-schema.sql (表結構)
* sql/shiro-data.sql (初始數據)
**實體**
具體請參考 com.github.zhangkaitao.shiro.chapter23.entity 包下的實體,此處就不列舉了。
**DAO**
具體請參考 com.github.zhangkaitao.shiro.chapter23.dao 包下的 DAO 接口及實現。
**Service**
具體請參考 com.github.zhangkaitao.shiro.chapter23.service 包下的 Service 接口及實現。以下是出了基本 CRUD 之外的關鍵接口:
``` java
public interface AppService {
public Long findAppIdByAppKey(String appKey);// 根據appKey查找AppId
}
```
``` java
public interface AuthorizationService {
//根據AppKey和用戶名查找其角色
public Set<String> findRoles(String appKey, String username);
//根據AppKey和用戶名查找權限字符串
public Set<String> findPermissions(String appKey, String username);
}
```
根據 AppKey 和用戶名查找用戶在指定應用中對于的角色和權限字符串。
**UserRealm**
``` java
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String)principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setRoles(
authorizationService.findRoles(Constants.SERVER_APP_KEY, username));
authorizationInfo.setStringPermissions(
authorizationService.findPermissions(Constants.SERVER_APP_KEY, username));
return authorizationInfo;
}
```
此處需要調用 AuthorizationService 的 findRoles/findPermissions 方法傳入 AppKey 和用戶名來獲取用戶的角色和權限字符串集合。其他的和《第十六章 綜合實例》代碼一樣。
**ServerFormAuthenticationFilter**
``` java
public class ServerFormAuthenticationFilter extends FormAuthenticationFilter {
protected void issueSuccessRedirect(ServletRequest request, ServletResponse response) throws Exception {
String fallbackUrl = (String) getSubject(request, response)
.getSession().getAttribute("authc.fallbackUrl");
if(StringUtils.isEmpty(fallbackUrl)) {
fallbackUrl = getSuccessUrl();
}
WebUtils.redirectToSavedRequest(request, response, fallbackUrl);
}
}
```
因為是多項目登錄,比如如果是從其他應用中重定向過來的,首先檢查 Session 中是否有 “authc.fallbackUrl” 屬性,如果有就認為它是默認的重定向地址;否則使用 Server 自己的 successUrl 作為登錄成功后重定向到的地址。
**MySqlSessionDAO**
將會話持久化到 Mysql 數據庫;此處大家可以將其實現為如存儲到 Redis/Memcached 等,實現策略請參考《第十章 會話管理》中的會話存儲 / 持久化章節的 MySessionDAO,完全一樣。
**MySqlSessionValidationScheduler**
和《第十章 會話管理》中的會話驗證章節部分中的 MySessionValidationScheduler 完全一樣。如果使用如 Redis 之類的有自動過期策略的 DB,完全可以不用實現 SessionValidationScheduler,直接借助于這些 DB 的過期策略即可。
**RemoteService**
``` java
public class RemoteService implements RemoteServiceInterface {
@Autowired private AuthorizationService authorizationService;
@Autowired private SessionDAO sessionDAO;
public Session getSession(String appKey, Serializable sessionId) {
return sessionDAO.readSession(sessionId);
}
public Serializable createSession(Session session) {
return sessionDAO.create(session);
}
public void updateSession(String appKey, Session session) {
sessionDAO.update(session);
}
public void deleteSession(String appKey, Session session) {
sessionDAO.delete(session);
}
public PermissionContext getPermissions(String appKey, String username) {
PermissionContext permissionContext = new PermissionContext();
permissionContext.setRoles(authorizationService.findRoles(appKey, username));
permissionContext.setPermissions(authorizationService.findPermissions(appKey, username));
return permissionContext;
}
}
```
將會使用 HTTP 調用器暴露為遠程服務,這樣其他應用就可以使用相應的客戶端調用這些接口進行 Session 的集中維護及根據 AppKey 和用戶名獲取角色 / 權限字符串集合。此處沒有實現安全校驗功能,如果是局域網內使用可以通過限定 IP 完成;否則需要使用如《第二十章 無狀態 Web 應用集成》中的技術完成安全校驗。
然后在 spring-mvc-remote-service.xml 配置文件把服務暴露出去:
``` xml
<bean id="remoteService"
class="com.github.zhangkaitao.shiro.chapter23.remote.RemoteService"/>
<bean name="/remoteService"
class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter">
<property name="service" ref="remoteService"/>
<property name="serviceInterface"
value="com.github.zhangkaitao.shiro.chapter23.remote.RemoteServiceInterface">
</bean>
```
**Shiro 配置文件 spring-config-shiro.xml**
和《第十六章 綜合實例》配置類似,但是需要在 shiroFilter 中的 filterChainDefinitions 中添加如下配置,即遠程調用不需要身份認證:
`/remoteService = anon`
對于 userRealm 的緩存配置直接禁用;因為如果開啟,修改了用戶權限不會自動同步到緩存;另外請參考《第十一章 緩存機制》進行緩存的正確配置。
**服務器端數據維護**
1、首先開啟 ngnix 反向代理;然后就可以直接訪問 [](http://localhost/chapter23-server/)<http://localhost/chapter23-server/>; 2、輸入默認的用戶名密碼:admin/123456 登錄 3、應用管理,進行應用的 CRUD,主要維護應用 KEY(必須唯一)及應用安全碼;客戶端就可以使用應用 KEY 獲取用戶對應應用的權限了。

4、授權管理,維護在哪個應用中用戶的角色列表。這樣客戶端就可以根據應用 KEY 及用戶名獲取到對應的角色 / 權限字符串列表了。


## shiro-example-chapter23-client 模塊
Client 模塊提供給其他應用模塊依賴,這樣其他應用模塊只需要依賴 Client 模塊,然后再在相應的配置文件中配置如登錄地址、遠程接口地址、攔截器鏈等等即可,簡化其他應用模塊的配置。
**配置遠程服務 spring-client-remote-service.xml**
``` xml
<bean id="remoteService"
class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
<property name="serviceUrl" value="${client.remote.service.url}"/>
<property name="serviceInterface"
value="com.github.zhangkaitao.shiro.chapter23.remote.RemoteServiceInterface"/>
</bean>
```
client.remote.service.url 是遠程服務暴露的地址;通過相應的 properties 配置文件配置,后續介紹。然后就可以通過 remoteService 獲取會話及角色 / 權限字符串集合了。
**ClientRealm**
``` java
public class ClientRealm extends AuthorizingRealm {
private RemoteServiceInterface remoteService;
private String appKey;
public void setRemoteService(RemoteServiceInterface remoteService) {
this.remoteService = remoteService;
}
public void setAppKey(String appKey) {
this.appKey = appKey;
}
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
PermissionContext context = remoteService.getPermissions(appKey, username);
authorizationInfo.setRoles(context.getRoles());
authorizationInfo.setStringPermissions(context.getPermissions());
return authorizationInfo;
}
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//永遠不會被調用
throw new UnsupportedOperationException("永遠不會被調用");
}
}
```
ClientRealm 提供身份認證信息和授權信息,此處因為是其他應用依賴客戶端,而這些應用不會實現身份認證,所以 doGetAuthenticationInfo 獲取身份認證信息直接無須實現。另外獲取授權信息,是通過遠程暴露的服務 RemoteServiceInterface 獲取,提供 appKey 和用戶名獲取即可。
**ClientSessionDAO**
``` java
public class ClientSessionDAO extends CachingSessionDAO {
private RemoteServiceInterface remoteService;
private String appKey;
public void setRemoteService(RemoteServiceInterface remoteService) {
this.remoteService = remoteService;
}
public void setAppKey(String appKey) {
this.appKey = appKey;
}
protected void doDelete(Session session) {
remoteService.deleteSession(appKey, session);
}
protected void doUpdate(Session session) {
remoteService.updateSession(appKey, session);
}
protected Serializable doCreate(Session session) {
Serializable sessionId = remoteService.createSession(session);
assignSessionId(session, sessionId);
return sessionId;
}
protected Session doReadSession(Serializable sessionId) {
return remoteService.getSession(appKey, sessionId);
}
}
```
Session 的維護通過遠程暴露接口實現,即本地不維護會話。
**ClientAuthenticationFilter**
``` java
public class ClientAuthenticationFilter extends AuthenticationFilter {
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
Subject subject = getSubject(request, response);
return subject.isAuthenticated();
}
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
String backUrl = request.getParameter("backUrl");
saveRequest(request, backUrl, getDefaultBackUrl(WebUtils.toHttp(request)));
return false;
}
protected void saveRequest(ServletRequest request, String backUrl, String fallbackUrl) {
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
HttpServletRequest httpRequest = WebUtils.toHttp(request);
session.setAttribute("authc.fallbackUrl", fallbackUrl);
SavedRequest savedRequest = new ClientSavedRequest(httpRequest, backUrl);
session.setAttribute(WebUtils.SAVED_REQUEST_KEY, savedRequest);
}
private String getDefaultBackUrl(HttpServletRequest request) {
String scheme = request.getScheme();
String domain = request.getServerName();
int port = request.getServerPort();
String contextPath = request.getContextPath();
StringBuilder backUrl = new StringBuilder(scheme);
backUrl.append("://");
backUrl.append(domain);
if("http".equalsIgnoreCase(scheme) && port != 80) {
backUrl.append(":").append(String.valueOf(port));
} else if("https".equalsIgnoreCase(scheme) && port != 443) {
backUrl.append(":").append(String.valueOf(port));
}
backUrl.append(contextPath);
backUrl.append(getSuccessUrl());
return backUrl.toString();
}
}
```
ClientAuthenticationFilter 是用于實現身份認證的攔截器(authc),當用戶沒有身份認證時;
1. 首先得到請求參數 backUrl,即登錄成功重定向到的地址;
2. 然后保存保存請求到會話,并重定向到登錄地址(server 模塊);
3. 登錄成功后,返回地址按照如下順序獲取:backUrl、保存的當前請求地址、defaultBackUrl(即設置的 successUrl);
**ClientShiroFilterFactoryBean**
``` java
public class ClientShiroFilterFactoryBean extends ShiroFilterFactoryBean implements ApplicationContextAware {
private ApplicationContext applicationContext;
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
public void setFiltersStr(String filters) {
if(StringUtils.isEmpty(filters)) {
return;
}
String[] filterArray = filters.split(";");
for(String filter : filterArray) {
String[] o = filter.split("=");
getFilters().put(o[0], (Filter)applicationContext.getBean(o[1]));
}
}
public void setFilterChainDefinitionsStr(String filterChainDefinitions) {
if(StringUtils.isEmpty(filterChainDefinitions)) {
return;
}
String[] chainDefinitionsArray = filterChainDefinitions.split(";");
for(String filter : chainDefinitionsArray) {
String[] o = filter.split("=");
getFilterChainDefinitionMap().put(o[0], o[1]);
}
}
}
```
1. setFiltersStr:設置攔截器,設置格式如 “filterName=filterBeanName; filterName=filterBeanName”;多個之間分號分隔;然后通過 applicationContext 獲取 filterBeanName 對應的 Bean 注冊到攔截器 Map 中;
2. setFilterChainDefinitionsStr:設置攔截器鏈,設置格式如 “url=filterName1[config],filterName2; url=filterName1[config],filterName2”;多個之間分號分隔;
**Shiro 客戶端配置 spring-client.xml**
提供了各應用通用的 Shiro 客戶端配置;這樣應用只需要導入相應該配置即可完成 Shiro 的配置,簡化了整個配置過程。
``` xml
<context:property-placeholder location=
"classpath:client/shiro-client-default.properties,classpath:client/shiro-client.properties"/>
```
提供給客戶端配置的 properties 屬性文件,client/shiro-client-default.properties 是客戶端提供的默認的配置;classpath:client/shiro-client.properties 是用于覆蓋客戶端默認配置,各應用應該提供該配置文件,然后提供各應用個性配置。
``` xml
<bean id="remoteRealm" class="com.github.zhangkaitao.shiro.chapter23.client.ClientRealm">
<property name="cachingEnabled" value="false"/>
<property name="appKey" value="${client.app.key}"/>
<property name="remoteService" ref="remoteService"/>
</bean>
```
appKey:使用 ${client.app.key} 占位符替換,即需要在之前的 properties 文件中配置。
``` xml
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="${client.session.id}"/>
<property name="httpOnly" value="true"/>
<property name="maxAge" value="-1"/>
<property name="domain" value="${client.cookie.domain}"/>
<property name="path" value="${client.cookie.path}"/>
</bean>
```
Session Id Cookie,cookie 名字、域名、路徑等都是通過配置文件配置。
``` xml
<bean id="sessionDAO"
class="com.github.zhangkaitao.shiro.chapter23.client.ClientSessionDAO">
<property name="sessionIdGenerator" ref="sessionIdGenerator"/>
<property name="appKey" value="${client.app.key}"/>
<property name="remoteService" ref="remoteService"/>
</bean>
```
SessionDAO 的 appKey,也是通過 ${client.app.key} 占位符替換,需要在配置文件配置。
``` xml
<bean id="sessionManager"
class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="sessionValidationSchedulerEnabled" value="false"/>//省略其他
</bean>
```
其他應用無須進行會話過期調度,所以 sessionValidationSchedulerEnabled=false。
``` xml
<bean id="clientAuthenticationFilter"
class="com.github.zhangkaitao.shiro.chapter23.client.ClientAuthenticationFilter"/>
```
應用的身份認證使用 ClientAuthenticationFilter,即如果沒有身份認證,則會重定向到 Server 模塊完成身份認證,身份認證成功后再重定向回來。
``` xml
<bean id="shiroFilter"
class="com.github.zhangkaitao.shiro.chapter23.client.ClientShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="${client.login.url}"/>
<property name="successUrl" value="${client.success.url}"/>
<property name="unauthorizedUrl" value="${client.unauthorized.url}"/>
<property name="filters">
<util:map>
<entry key="authc" value-ref="clientAuthenticationFilter"/>
</util:map>
</property>
<property name="filtersStr" value="${client.filters}"/>
<property name="filterChainDefinitionsStr" value="${client.filter.chain.definitions}"/>
</bean>
```
ShiroFilter 使用我們自定義的 ClientShiroFilterFactoryBean,然后 loginUrl(登錄地址)、successUrl(登錄成功后默認的重定向地址)、unauthorizedUrl(未授權重定向到的地址)通過占位符替換方式配置;另外 filtersStr 和 filterChainDefinitionsStr 也是使用占位符替換方式配置;這樣就可以在各應用進行自定義了。
**默認配置 client/shiro-client-default.properties**
``` properties
#各應用的appKey
client.app.key=
#遠程服務URL地址
client.remote.service.url=http://localhost/chapter23-server/remoteService
#登錄地址
client.login.url=http://localhost/chapter23-server/login
#登錄成功后,默認重定向到的地址
client.success.url=/
#未授權重定向到的地址
client.unauthorized.url=http://localhost/chapter23-server/unauthorized
#session id 域名
client.cookie.domain=
#session id 路徑
client.cookie.path=/
#cookie中的session id名稱
client.session.id=sid
#cookie中的remember me名稱
client.rememberMe.id=rememberMe
#過濾器 name=filter-ref;name=filter-ref
client.filters=
#過濾器鏈 格式 url=filters;url=filters
client.filter.chain.definitions=/**=anon
```
在各應用中主要配置 client.app.key、client.filters、client.filter.chain.definitions。
## shiro-example-chapter23-app \* 模塊
繼承 shiro-example-chapter23-pom 模塊
``` xml
<parent>
<artifactId>shiro-example-chapter23-pom</artifactId>
<groupId>com.github.zhangkaitao</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
```
**依賴 shiro-example-chapter23-client 模塊**
``` xml
<dependency>
<groupId>com.github.zhangkaitao</groupId>
<artifactId>shiro-example-chapter23-client</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
```
**客戶端配置 client/shiro-client.properties**
**配置 shiro-example-chapter23-app1**
``` properties
client.app.key=645ba612-370a-43a8-a8e0-993e7a590cf0
client.success.url=/hello
client.filter.chain.definitions=/hello=anon;/login=authc;/**=authc
```
client.app.key 是 server 模塊維護的,直接拷貝過來即可;client.filter.chain.definitions 定義了攔截器鏈;比如訪問 / hello,匿名即可。
**配置 shiro-example-chapter23-app2**
``` properties
client.app.key=645ba613-370a-43a8-a8e0-993e7a590cf0
client.success.url=/hello
client.filter.chain.definitions=/hello=anon;/login=authc;/**=authc
```
和 app1 類似,client.app.key 是 server 模塊維護的,直接拷貝過來即可;client.filter.chain.definitions 定義了攔截器鏈;比如訪問 / hello,匿名即可。
**web.xml**
``` xml
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:client/spring-client.xml
</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
```
指定加載客戶端 Shiro 配置,client/spring-client.xml。
``` xml
<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>
```
配置 ShiroFilter 攔截器。
**控制器**
shiro-example-chapter23-app1
``` java
@Controller
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "success";
}
@RequestMapping(value = "/attr", method = RequestMethod.POST)
public String setAttr(
@RequestParam("key") String key, @RequestParam("value") String value) {
SecurityUtils.getSubject().getSession().setAttribute(key, value);
return "success";
}
@RequestMapping(value = "/attr", method = RequestMethod.GET)
public String getAttr(
@RequestParam("key") String key, Model model) {
model.addAttribute("value",
SecurityUtils.getSubject().getSession().getAttribute(key));
return "success";
}
@RequestMapping("/role1")
@RequiresRoles("role1")
public String role1() {
return "success";
}
}
```
shiro-example-chapter23-app2 的控制器類似,role2 方法使用 @RequiresRoles("role2") 注解,即需要角色 2。
其他配置請參考源碼。
## 測試
**1、安裝配置啟動 nginx**
1、首先到 `http://nginx.org/en/download.html` 下載,比如我下載的是 windows 版本的;
2、然后編輯 conf/nginx.conf 配置文件,在 server 部分添加如下部分:
```
location ~ ^/(chapter23-server)/ {
proxy_pass http://127.0.0.1:8080;
index /;
proxy_set_header Host $host;
}
location ~ ^/(chapter23-app1)/ {
proxy_pass http://127.0.0.1:9080;
index /;
proxy_set_header Host $host;
}
location ~ ^/(chapter23-app2)/ {
proxy_pass http://127.0.0.1:10080;
index /;
proxy_set_header Host $host;
}
```
3、最后雙擊 nginx.exe 啟動 Nginx 即可。
已經配置好的 nginx 請到 shiro-example-chapter23-nginx 模塊下下周 nginx-1.5.11.rar 即可。
**2、安裝依賴**
1、首先安裝 shiro-example-chapter23-core 依賴,到 shiro-example-chapter23-core 模塊下運行 mvn install 安裝 core 模塊。
2、接著到 shiro-example-chapter23-client 模塊下運行 mvn install 安裝客戶端模塊。
**3、啟動 Server 模塊**
到 shiro-example-chapter23-server 模塊下運行 mvn jetty:run 啟動該模塊;使用 `http://localhost:8080/chapter23-server/` 即可訪問,因為啟動了 nginx,那么可以直接訪問 `http://localhost/chapter23-server/`。
**4、啟動 App\* 模塊**
到 shiro-example-chapter23-app1 和 shiro-example-chapter23-app2 模塊下分別運行 mvn jetty:run 啟動該模塊;使用 `http://localhost:9080/chapter23-app1/` 和 `http://localhost:10080/chapter23-app2/` 即可訪問,因為啟動了 nginx,那么可以直接訪問 `http://localhost/chapter23-app1/` 和 `http://localhost/chapter23-app2/`。
**5、服務器端維護**
1、訪問 `http://localhost/chapter23-server/`;
2、輸入默認的用戶名密碼:admin/123456 登錄
3、應用管理,進行應用的 CRUD,主要維護應用 KEY(必須唯一)及應用安全碼;客戶端就可以使用應用 KEY 獲取用戶對應應用的權限了。

4、授權管理,維護在哪個應用中用戶的角色列表。這樣客戶端就可以根據應用 KEY 及用戶名獲取到對應的角色 / 權限字符串列表了。


\**6、App* 模塊身份認證及授權\*\*
1、在未登錄情況下訪問 `http://localhost/chapter23-app1/hello`,看到下圖:

2、登錄地址是 `http://localhost/chapter23-app1/login?backUrl=/chapter23-app1`,即登錄成功后重定向回 `http://localhost/chapter23-app1`(這是個錯誤地址,為了測試登錄成功后重定向地址),點擊登錄按鈕后重定向到 Server 模塊的登錄界面:

3、登錄成功后,會重定向到相應的登錄成功地址;接著訪問 `http://localhost/chapter23-app1/hello`,看到如下圖:

4、可以看到 admin 登錄,及其是否擁有 role1/role2 角色;可以在 server 模塊移除 role1 角色或添加 role2 角色看看頁面變化;
5、可以在 `http://localhost/chapter23-app1/hello` 頁面設置屬性,如 key=123;接著訪問 `http://localhost/chapter23-app2/attr?key=key` 就可以看到剛才設置的屬性,如下圖:

另外在 app2,用戶默認擁有 role2 角色,而沒有 role1 角色。
到此整個測試就完成了,可以看出本示例實現了:會話的分布式及權限的集中管理。
## 本示例缺點
1. 沒有加緩存;
2. 客戶端每次獲取會話 / 權限都需要通過客戶端訪問服務端;造成服務端單點和請求壓力大;單點可以考慮使用集群來解決;請求壓力大需要考慮配合緩存服務器(如 Redis)來解決;即每次會話 / 權限獲取時首先查詢緩存中是否存在,如果有直接獲取即可;否則再查服務端;降低請求壓力;
3. 會話的每次更新(比如設置屬性 / 更新最后訪問時間戳)都需要同步到服務端;也造成了請求壓力過大;可以考慮在請求的最后只同步一次會話(需要對 Shiro 會話進行改造,通過如攔截器在執行完請求后完成同步,這樣每次請求只同步一次);
4. 只能同域名才能使用,即會話 ID 是從同一個域名下獲取,如果跨域請考慮使用 CAS/OAuth2 之實現。
所以實際應用時可能還是需要改造的,但大體思路是差不多的。
- 第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章 在線會話管理