在作一些企業內部項目時或一些互聯網後臺時;可能會涉及到集中權限管理,統一進行多項目的權限管理;另外也須要統一的會話管理,即實現單點身份認證和受權控制。html
學習本章以前,請務必先學習《第十章 會話管理》和《第十六章 綜合實例》,本章代碼都是基於這兩章的代碼基礎上完成的。java
本章示例是同域名的場景下完成的,若是跨域請參考《第十五章 單點登陸》和《第十七章 OAuth2集成》瞭解使用CAS或OAuth2實現跨域的身份驗證和受權。另外好比客戶端/服務器端的安全校驗可參考《第二十章 無狀態Web應用集成》。mysql
部署架構
一、有三個應用:用於用戶/權限控制的Server(端口:8080);兩個應用App1(端口9080)和App2(端口10080);nginx
二、使用Nginx反向代理這三個應用,nginx.conf的server配置部分以下: git
Java代碼
- 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;github
訪問http://localhost/chapter23-app1會自動轉發到http://localhost:9080/chapter23-app1;訪問http://localhost/chapter23-app3會自動轉發到http://localhost:10080/chapter23-app3;web
Nginx的安裝及使用請自行搜索學習,本文再也不闡述。 redis
項目架構
一、首先經過用戶/權限Server維護用戶、應用、權限信息;數據都持久化到MySQL數據庫中;spring
二、應用App1/應用App2使用客戶端Client遠程調用用戶/權限Server獲取會話及權限信息。sql
此處使用mysql存儲會話,而不是使用如Memcached/Redis之類的,主要目的是下降學習成本;若是換成如redis也不會很難;如:
使用如Redis還一個好處就是無需在用戶/權限Server中開會話過時調度器,能夠藉助Redis自身的過時策略來完成。
模塊關係依賴
一、shiro-example-chapter23-pom模塊:提供了其餘全部模塊的依賴;這樣其餘模塊直接繼承它便可,簡化依賴配置,如shiro-example-chapter23-server:
Java代碼
- <parent>
- <artifactId>shiro-example-chapter23-pom</artifactId>
- <groupId>com.github.zhangkaitao</groupId>
- <version>1.0-SNAPSHOT</version>
- </parent>
二、shiro-example-chapter23-core模塊:提供給shiro-example-chapter23-server、shiro-example-chapter23-client、shiro-example-chapter23-app*模塊的核心依賴,好比遠程調用接口等;
三、shiro-example-chapter23-server模塊:提供了用戶、應用、權限管理功能;
四、shiro-example-chapter23-client模塊:提供給應用模塊獲取會話及應用對應的權限信息;
五、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模塊:
Java代碼
- <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;這樣才能從一個應用重定向另外一個(要否則只能在一個應用內重定向):
Java代碼
- 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();
- }
-
一、若是從外部傳入了successUrl(登陸成功以後重定向的地址),且以http://或https://開頭那麼直接返回(相應的攔截器直接重定向到它便可);
二、若是successUrl有值但沒有上下文,拼上上下文;
三、不然,若是successUrl有值,直接賦值給requestUrl便可;不然,若是successUrl沒值,那麼requestUrl就是當前請求的地址;
五、拼上url前邊的schema,如http或https;
六、拼上域名;
七、拼上重定向到的地址(帶上下文);
八、若是successUrl沒值,且有查詢參數,拼上;
9返回該地址,相應的攔截器直接重定向到它便可。
shiro-example-chapter23-server模塊
簡單的實體關係圖
簡單數據字典
用戶(sys_user)
名稱 |
類型 |
長度 |
描述 |
id |
bigint |
|
編號 主鍵 |
username |
varchar |
100 |
用戶名 |
password |
varchar |
100 |
密碼 |
salt |
varchar |
50 |
鹽 |
locked |
bool |
|
帳戶是否鎖定 |
應用(sys_app)
名稱 |
類型 |
長度 |
描述 |
id |
bigint |
|
編號 主鍵 |
name |
varchar |
100 |
應用名稱 |
app_key |
varchar |
100 |
應用key(惟一) |
app_secret |
varchar |
100 |
應用安全碼 |
available |
bool |
|
是否鎖定 |
受權(sys_authorization)
名稱 |
類型 |
長度 |
描述 |
id |
bigint |
|
編號 主鍵 |
user_id |
bigint |
|
所屬用戶 |
app_id |
bigint |
|
所屬應用 |
role_ids |
varchar |
100 |
角色列表 |
用戶:比《第十六章 綜合實例》少了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配置文件把服務暴露出去:
Java代碼
- <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中添加以下配置,即遠程調用不須要身份認證:
Java代碼
- /remoteService = anon
對於userRealm的緩存配置直接禁用;由於若是開啓,修改了用戶權限不會自動同步到緩存;另外請參考《第十一章 緩存機制》進行緩存的正確配置。
服務器端數據維護
一、首先開啓ngnix反向代理;而後就能夠直接訪問http://localhost/chapter23-server/;
二、輸入默認的用戶名密碼:admin/123456登陸
三、應用管理,進行應用的CRUD,主要維護應用KEY(必須惟一)及應用安全碼;客戶端就可使用應用KEY獲取用戶對應應用的權限了。
四、受權管理,維護在哪一個應用中用戶的角色列表。這樣客戶端就能夠根據應用KEY及用戶名獲取到對應的角色/權限字符串列表了。
shiro-example-chapter23-client模塊
Client模塊提供給其餘應用模塊依賴,這樣其餘應用模塊只須要依賴Client模塊,而後再在相應的配置文件中配置如登陸地址、遠程接口地址、攔截器鏈等等便可,簡化其餘應用模塊的配置。
配置遠程服務spring-client-remote-service.xml
Java代碼
- <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),當用戶沒有身份認證時;
一、首先獲得請求參數backUrl,即登陸成功重定向到的地址;
二、而後保存保存請求到會話,並重定向到登陸地址(server模塊);
三、登陸成功後,返回地址按照以下順序獲取: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]);
- }
- }
- }
一、setFiltersStr:設置攔截器,設置格式如「filterName=filterBeanName; filterName=filterBeanName」;多個之間分號分隔;而後經過applicationContext獲取filterBeanName對應的Bean註冊到攔截器Map中;
二、setFilterChainDefinitionsStr:設置攔截器鏈,設置格式如「url=filterName1[config],filterName2; url=filterName1[config],filterName2」;多個之間分號分隔;
Shiro客戶端配置spring-client.xml
提供了各應用通用的Shiro客戶端配置;這樣應用只須要導入相應該配置便可完成Shiro的配置,簡化了整個配置過程。
Java代碼
- <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是用於覆蓋客戶端默認配置,各應用應該提供該配置文件,而後提供各應用個性配置。
Java代碼
- <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文件中配置。
Java代碼
- <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名字、域名、路徑等都是經過配置文件配置。
Java代碼
- <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 }佔位符替換,須要在配置文件配置。
Java代碼
- <bean id="sessionManager"
- class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
- <property name="sessionValidationSchedulerEnabled" value="false"/>//省略其餘
- </bean>
其餘應用無須進行會話過時調度,因此sessionValidationSchedulerEnabled=false。
Java代碼
- <bean id="clientAuthenticationFilter"
- class="com.github.zhangkaitao.shiro.chapter23.client.ClientAuthenticationFilter"/>
應用的身份認證使用ClientAuthenticationFilter,即若是沒有身份認證,則會重定向到Server模塊完成身份認證,身份認證成功後再重定向回來。
Java代碼
- <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
Java代碼
- #各應用的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模塊
Java代碼
- <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
Java代碼
- 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
Java代碼
- 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
Java代碼
- <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。
Java代碼
- <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。
其餘配置請參考源碼。
測試
一、安裝配置啓動nginx
一、首先到http://nginx.org/en/download.html下載,好比我下載的是windows版本的;
二、而後編輯conf/nginx.conf配置文件,在server部分添加以下部分:
Java代碼
- 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;
- }
三、最後雙擊nginx.exe啓動Nginx便可。
已經配置好的nginx請到shiro-example-chapter23-nginx模塊下下週nginx-1.5.11.rar便可。
二、安裝依賴
一、首先安裝shiro-example-chapter23-core依賴,到shiro-example-chapter23-core模塊下運行mvn install安裝core模塊。
二、接着到shiro-example-chapter23-client模塊下運行mvn install安裝客戶端模塊。
三、啓動Server模塊
到shiro-example-chapter23-server模塊下運行mvn jetty:run啓動該模塊;使用http://localhost:8080/chapter23-server/便可訪問,由於啓動了nginx,那麼能夠直接訪問http://localhost/chapter23-server/。
四、啓動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/。
五、服務器端維護
一、訪問http://localhost/chapter23-server/;
二、輸入默認的用戶名密碼:admin/123456登陸
三、應用管理,進行應用的CRUD,主要維護應用KEY(必須惟一)及應用安全碼;客戶端就可使用應用KEY獲取用戶對應應用的權限了。
四、受權管理,維護在哪一個應用中用戶的角色列表。這樣客戶端就能夠根據應用KEY及用戶名獲取到對應的角色/權限字符串列表了。
六、App*模塊身份認證及受權
一、在未登陸狀況下訪問http://localhost/chapter23-app1/hello,看到下圖:
二、登陸地址是http://localhost/chapter23-app1/login?backUrl=/chapter23-app1,即登陸成功後重定向回http://localhost/chapter23-app1(這是個錯誤地址,爲了測試登陸成功後重定向地址),點擊登陸按鈕後重定向到Server模塊的登陸界面:
三、登陸成功後,會重定向到相應的登陸成功地址;接着訪問http://localhost/chapter23-app1/hello,看到以下圖:
四、能夠看到admin登陸,及其是否擁有role1/role2角色;能夠在server模塊移除role1角色或添加role2角色看看頁面變化;
五、能夠在http://localhost/chapter23-app1/hello頁面設置屬性,如key=123;接着訪問http://localhost/chapter23-app2/attr?key=key就能夠看到剛纔設置的屬性,以下圖:
另外在app2,用戶默認擁有role2角色,而沒有role1角色。
到此整個測試就完成了,能夠看出本示例實現了:會話的分佈式及權限的集中管理。
本示例缺點
一、沒有加緩存;
二、客戶端每次獲取會話/權限都須要經過客戶端訪問服務端;形成服務端單點和請求壓力大;單點能夠考慮使用集羣來解決;請求壓力大須要考慮配合緩存服務器(如Redis)來解決;即每次會話/權限獲取時首先查詢緩存中是否存在,若是有直接獲取便可;不然再查服務端;下降請求壓力;
三、會話的每次更新(好比設置屬性/更新最後訪問時間戳)都須要同步到服務端;也形成了請求壓力過大;能夠考慮在請求的最後只同步一次會話(須要對Shiro會話進行改造,經過如攔截器在執行完請求後完成同步,這樣每次請求只同步一次);
四、只能同域名才能使用,即會話ID是從同一個域名下獲取,若是跨域請考慮使用CAS/OAuth2之實現。
因此實際應用時可能仍是須要改造的,但大致思路是差很少的。