在做一些企業(yè)內(nèi)部項目時或一些互聯(lián)網(wǎng)后臺時;可能會涉及到集中權(quán)限管理,統(tǒng)一進行多項目的權(quán)限管理;另外也需要統(tǒng)一的會話管理,即實現(xiàn)單點身份認證和授權(quán)控制。
學習本章之前,請務(wù)必先學習《第十章 會話管理》和《第十六章 綜合實例》,本章代碼都是基于這兩章的代碼基礎(chǔ)上完成的。
本章示例是同域名的場景下完成的,如果跨域請參考《第十五章 單點登錄》和《第十七章 OAuth2 集成》了解使用 CAS 或 OAuth2 實現(xiàn)跨域的身份驗證和授權(quán)。另外比如客戶端 / 服務(wù)器端的安全校驗可參考《第二十章 無狀態(tài) Web 應(yīng)用集成》。
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
會自動轉(zhuǎn)發(fā)到 http://localhost:8080/chapter23-server
;
訪問 http://localhost/chapter23-app1
會自動轉(zhuǎn)發(fā)到 http://localhost:9080/chapter23-app1
;
訪問 http://localhost/chapter23-app3
會自動轉(zhuǎn)發(fā)到 http://localhost:10080/chapter23-app3
;
Nginx 的安裝及使用請自行搜索學習,本文不再闡述。
此處使用 Mysql 存儲會話,而不是使用如 Memcached/Redis 之類的,主要目的是降低學習成本;如果換成如 Redis 也不會很難;如:
使用如 Redis 還一個好處就是無需在用戶 / 權(quán)限 Server 中開會話過期調(diào)度器,可以借助 Redis 自身的過期策略來完成。
1、shiro-example-chapter23-pom 模塊:提供了其他所有模塊的依賴;這樣其他模塊直接繼承它即可,簡化依賴配置,如 shiro-example-chapter23-server:
<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 *
模塊的核心依賴,比如遠程調(diào)用接口等;
3、shiro-example-chapter23-server 模塊:提供了用戶、應(yīng)用、權(quán)限管理功能;
4、shiro-example-chapter23-client 模塊:提供給應(yīng)用模塊獲取會話及應(yīng)用對應(yīng)的權(quán)限信息;
5、shiro-example-chapter23-app *
模塊:各個子應(yīng)用,如一些內(nèi)部管理系統(tǒng)應(yīng)用;其登錄都跳到 shiro-example-chapter23-server 登錄;另外權(quán)限都從 shiro-example-chapter23-server 獲?。ㄈ缤ㄟ^遠程調(diào)用)。
其 pom.xml 的 packaging 類型為 pom,并且在該 pom 中加入其他模塊需要的依賴,然后其他模塊只需要把該模塊設(shè)置為 parent 即可自動繼承這些依賴,如 shiro-example-chapter23-server 模塊:
<parent>
<artifactId>shiro-example-chapter23-pom</artifactId>
<groupId>com.github.zhangkaitao</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
簡化其他模塊的依賴配置等。
提供了其他模塊共有的依賴,如遠程調(diào)用接口:
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,及根據(jù)應(yīng)用 key 和用戶名獲取權(quán)限上下文(包括角色和權(quán)限字符串);shiro-example-chapter23-server 模塊服務(wù)端實現(xiàn);shiro-example-chapter23-client 模塊客戶端調(diào)用。
另外提供了 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
;這樣才能從一個應(yīng)用重定向另一個(要不然只能在一個應(yīng)用內(nèi)重定向):
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();
}
http://
或 https://
開頭那么直接返回(相應(yīng)的攔截器直接重定向到它即可);簡單的實體關(guān)系圖
簡單數(shù)據(jù)字典
用戶 (sys_user)
名稱 |
類型 |
長度 |
描述 |
id |
bigint |
|
編號 主鍵 |
username |
varchar |
100 |
用戶名 |
password |
varchar |
100 |
密碼 |
salt |
varchar |
50 |
鹽 |
locked |
bool |
賬戶是否鎖定 |
應(yīng)用 (sys_app)
名稱 |
類型 |
長度 |
描述 |
id |
bigint |
|
編號 主鍵 |
name |
varchar |
100 |
應(yīng)用名稱 |
app_key |
varchar |
100 |
應(yīng)用 key(唯一) |
app_secret |
varchar |
100 |
應(yīng)用安全碼 |
available |
bool |
|
是否鎖定 |
授權(quán) (sys_authorization)
名稱 |
類型 |
長度 |
描述 |
id |
bigint |
|
編號 主鍵 |
user_id |
bigint |
|
所屬用戶 |
app_id |
bigint |
|
所屬應(yīng)用 |
role_ids |
varchar |
100 |
角色列表 |
用戶:比《第十六章 綜合實例》少了 role_ids,因為本章是多項目集中權(quán)限管理;所以授權(quán)時需要指定相應(yīng)的應(yīng)用;而不是直接給用戶授權(quán);所以不能在用戶中出現(xiàn) role_ids 了;
應(yīng)用:所有集中權(quán)限的應(yīng)用;在此處需要指定應(yīng)用 key(app_key) 和應(yīng)用安全碼(app_secret),app 在訪問 server 時需要指定自己的 app_key 和用戶名來獲取該 app 對應(yīng)用戶權(quán)限信息;另外 app_secret 可以認為 app 的密碼,比如需要安全訪問時可以考慮使用它,可參考《第二十章 無狀態(tài) Web 應(yīng)用集成》。另外 available 屬性表示該應(yīng)用當前是否開啟;如果 false 表示該應(yīng)用當前不可用,即不能獲取到相應(yīng)的權(quán)限信息。
授權(quán):給指定的用戶在指定的 app 下授權(quán),即角色是與用戶和 app 存在關(guān)聯(lián)關(guān)系。
因為本章使用了《第十六章 綜合實例》代碼,所以還有其他相應(yīng)的表結(jié)構(gòu)(本章未使用到)。
表 / 數(shù)據(jù) SQL
具體請參考
實體
具體請參考 com.github.zhangkaitao.shiro.chapter23.entity 包下的實體,此處就不列舉了。
DAO
具體請參考 com.github.zhangkaitao.shiro.chapter23.dao 包下的 DAO 接口及實現(xiàn)。
Service
具體請參考 com.github.zhangkaitao.shiro.chapter23.service 包下的 Service 接口及實現(xiàn)。以下是出了基本 CRUD 之外的關(guān)鍵接口:
public interface AppService {
public Long findAppIdByAppKey(String appKey);// 根據(jù)appKey查找AppId
}
public interface AuthorizationService {
//根據(jù)AppKey和用戶名查找其角色
public Set<String> findRoles(String appKey, String username);
//根據(jù)AppKey和用戶名查找權(quán)限字符串
public Set<String> findPermissions(String appKey, String username);
}
根據(jù) AppKey 和用戶名查找用戶在指定應(yīng)用中對于的角色和權(quán)限字符串。
UserRealm
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;
}
此處需要調(diào)用 AuthorizationService 的 findRoles/findPermissions 方法傳入 AppKey 和用戶名來獲取用戶的角色和權(quán)限字符串集合。其他的和《第十六章 綜合實例》代碼一樣。
ServerFormAuthenticationFilter
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);
}
}
因為是多項目登錄,比如如果是從其他應(yīng)用中重定向過來的,首先檢查 Session 中是否有 “authc.fallbackUrl” 屬性,如果有就認為它是默認的重定向地址;否則使用 Server 自己的 successUrl 作為登錄成功后重定向到的地址。
MySqlSessionDAO
將會話持久化到 Mysql 數(shù)據(jù)庫;此處大家可以將其實現(xiàn)為如存儲到 Redis/Memcached 等,實現(xiàn)策略請參考《第十章 會話管理》中的會話存儲 / 持久化章節(jié)的 MySessionDAO,完全一樣。
MySqlSessionValidationScheduler
和《第十章 會話管理》中的會話驗證章節(jié)部分中的 MySessionValidationScheduler 完全一樣。如果使用如 Redis 之類的有自動過期策略的 DB,完全可以不用實現(xiàn) SessionValidationScheduler,直接借助于這些 DB 的過期策略即可。
RemoteService
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 調(diào)用器暴露為遠程服務(wù),這樣其他應(yīng)用就可以使用相應(yīng)的客戶端調(diào)用這些接口進行 Session 的集中維護及根據(jù) AppKey 和用戶名獲取角色 / 權(quán)限字符串集合。此處沒有實現(xiàn)安全校驗功能,如果是局域網(wǎng)內(nèi)使用可以通過限定 IP 完成;否則需要使用如《第二十章 無狀態(tài) Web 應(yīng)用集成》中的技術(shù)完成安全校驗。
然后在 spring-mvc-remote-service.xml 配置文件把服務(wù)暴露出去:
<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 中添加如下配置,即遠程調(diào)用不需要身份認證:
/remoteService = anon
對于 userRealm 的緩存配置直接禁用;因為如果開啟,修改了用戶權(quán)限不會自動同步到緩存;另外請參考《第十一章 緩存機制》進行緩存的正確配置。
服務(wù)器端數(shù)據(jù)維護
1、首先開啟 ngnix 反向代理;然后就可以直接訪問 http://localhost/chapter23-server/; 2、輸入默認的用戶名密碼:admin/123456 登錄 3、應(yīng)用管理,進行應(yīng)用的 CRUD,主要維護應(yīng)用 KEY(必須唯一)及應(yīng)用安全碼;客戶端就可以使用應(yīng)用 KEY 獲取用戶對應(yīng)應(yīng)用的權(quán)限了。
4、授權(quán)管理,維護在哪個應(yīng)用中用戶的角色列表。這樣客戶端就可以根據(jù)應(yīng)用 KEY 及用戶名獲取到對應(yīng)的角色 / 權(quán)限字符串列表了。
Client 模塊提供給其他應(yīng)用模塊依賴,這樣其他應(yīng)用模塊只需要依賴 Client 模塊,然后再在相應(yīng)的配置文件中配置如登錄地址、遠程接口地址、攔截器鏈等等即可,簡化其他應(yīng)用模塊的配置。
配置遠程服務(wù) spring-client-remote-service.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 是遠程服務(wù)暴露的地址;通過相應(yīng)的 properties 配置文件配置,后續(xù)介紹。然后就可以通過 remoteService 獲取會話及角色 / 權(quán)限字符串集合了。
ClientRealm
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 {
//永遠不會被調(diào)用
throw new UnsupportedOperationException("永遠不會被調(diào)用");
}
}
ClientRealm 提供身份認證信息和授權(quán)信息,此處因為是其他應(yīng)用依賴客戶端,而這些應(yīng)用不會實現(xiàn)身份認證,所以 doGetAuthenticationInfo 獲取身份認證信息直接無須實現(xiàn)。另外獲取授權(quán)信息,是通過遠程暴露的服務(wù) RemoteServiceInterface 獲取,提供 appKey 和用戶名獲取即可。
ClientSessionDAO
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 的維護通過遠程暴露接口實現(xiàn),即本地不維護會話。
ClientAuthenticationFilter
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 是用于實現(xiàn)身份認證的攔截器(authc),當用戶沒有身份認證時;
ClientShiroFilterFactoryBean
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]);
}
}
}
Shiro 客戶端配置 spring-client.xml
提供了各應(yīng)用通用的 Shiro 客戶端配置;這樣應(yīng)用只需要導入相應(yīng)該配置即可完成 Shiro 的配置,簡化了整個配置過程。
<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 是用于覆蓋客戶端默認配置,各應(yīng)用應(yīng)該提供該配置文件,然后提供各應(yīng)用個性配置。
<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 文件中配置。
<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 名字、域名、路徑等都是通過配置文件配置。
<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} 占位符替換,需要在配置文件配置。
<bean id="sessionManager"
class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="sessionValidationSchedulerEnabled" value="false"/>//省略其他
</bean>
其他應(yīng)用無須進行會話過期調(diào)度,所以 sessionValidationSchedulerEnabled=false。
<bean id="clientAuthenticationFilter"
class="com.github.zhangkaitao.shiro.chapter23.client.ClientAuthenticationFilter"/>
其他應(yīng)用無須進行會話過期調(diào)度,所以 sessionValidationSchedulerEnabled=false。
<bean id="clientAuthenticationFilter"
class="com.github.zhangkaitao.shiro.chapter23.client.ClientAuthenticationFilter"/>
應(yīng)用的身份認證使用 ClientAuthenticationFilter,即如果沒有身份認證,則會重定向到 Server 模塊完成身份認證,身份認證成功后再重定向回來。
<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(未授權(quán)重定向到的地址)通過占位符替換方式配置;另外 filtersStr 和 filterChainDefinitionsStr 也是使用占位符替換方式配置;這樣就可以在各應(yīng)用進行自定義了。
默認配置 client/shiro-client-default.properties
\#各應(yīng)用的appKey
client.app.key=
\#遠程服務(wù)URL地址
client.remote.service.url=http://localhost/chapter23-server/remoteService
\#登錄地址
client.login.url=http://localhost/chapter23-server/login
\#登錄成功后,默認重定向到的地址
client.success.url=/
\#未授權(quán)重定向到的地址
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
在各應(yīng)用中主要配置 client.app.key、client.filters、client.filter.chain.definitions。
繼承 shiro-example-chapter23-pom 模塊
<parent>
<artifactId>shiro-example-chapter23-pom</artifactId>
<groupId>com.github.zhangkaitao</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
依賴 shiro-example-chapter23-client 模塊
<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
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
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
<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。
<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
@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 即可。
已經(jīng)配置好的 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、服務(wù)器端維護
1、訪問 http://localhost/chapter23-server/
;
2、輸入默認的用戶名密碼:admin/123456 登錄
3、應(yīng)用管理,進行應(yīng)用的 CRUD,主要維護應(yīng)用 KEY(必須唯一)及應(yīng)用安全碼;客戶端就可以使用應(yīng)用 KEY 獲取用戶對應(yīng)應(yīng)用的權(quán)限了。
4、授權(quán)管理,維護在哪個應(yīng)用中用戶的角色列表。這樣客戶端就可以根據(jù)應(yīng)用 KEY 及用戶名獲取到對應(yīng)的角色 / 權(quán)限字符串列表了。
*6、App 模塊身份認證及授權(quán)**
1、在未登錄情況下訪問 http://localhost/chapter23-app1/hello
,看到下圖:
2、登錄地址是 http://localhost/chapter23-app1/login?backUrl=/chapter23-app1
,即登錄成功后重定向回 http://localhost/chapter23-app1
(這是個錯誤地址,為了測試登錄成功后重定向地址),點擊登錄按鈕后重定向到 Server 模塊的登錄界面:
3、登錄成功后,會重定向到相應(yīng)的登錄成功地址;接著訪問 http://localhost/chapter23-app1/hello
,看到如下圖:
4、可以看到 admin 登錄,及其是否擁有 role1/role2 角色;可以在 server 模塊移除 role1 角色或添加 role2 角色看看頁面變化;
5、可以在 http://localhost/chapter23-app1/hello
頁面設(shè)置屬性,如 key=123;接著訪問 http://localhost/chapter23-app2/attr?key=key
就可以看到剛才設(shè)置的屬性,如下圖:
另外在 app2,用戶默認擁有 role2 角色,而沒有 role1 角色。
到此整個測試就完成了,可以看出本示例實現(xiàn)了:會話的分布式及權(quán)限的集中管理。
所以實際應(yīng)用時可能還是需要改造的,但大體思路是差不多的。
更多建議: