Shiro 多項(xiàng)目

2020-12-04 09:51 更新

多項(xiàng)目集中權(quán)限管理及分布式會(huì)話

在做一些企業(yè)內(nèi)部項(xiàng)目時(shí)或一些互聯(lián)網(wǎng)后臺(tái)時(shí);可能會(huì)涉及到集中權(quán)限管理,統(tǒng)一進(jìn)行多項(xiàng)目的權(quán)限管理;另外也需要統(tǒng)一的會(huì)話管理,即實(shí)現(xiàn)單點(diǎn)身份認(rèn)證和授權(quán)控制。

學(xué)習(xí)本章之前,請(qǐng)務(wù)必先學(xué)習(xí)《第十章 會(huì)話管理》和《第十六章 綜合實(shí)例》,本章代碼都是基于這兩章的代碼基礎(chǔ)上完成的。

本章示例是同域名的場(chǎng)景下完成的,如果跨域請(qǐng)參考《第十五章 單點(diǎn)登錄》和《第十七章 OAuth2 集成》了解使用 CAS 或 OAuth2 實(shí)現(xiàn)跨域的身份驗(yàn)證和授權(quán)。另外比如客戶端 / 服務(wù)器端的安全校驗(yàn)可參考《第二十章 無狀態(tài) Web 應(yīng)用集成》。

部署架構(gòu)

  1. 有三個(gè)應(yīng)用:用于用戶 / 權(quán)限控制的 Server(端口:8080);兩個(gè)應(yīng)用 App1(端口 9080)和 App2(端口 10080);
  2. 使用 Nginx 反向代理這三個(gè)應(yīng)用,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 會(huì)自動(dòng)轉(zhuǎn)發(fā)到 http://localhost:8080/chapter23-server;
訪問 http://localhost/chapter23-app1 會(huì)自動(dòng)轉(zhuǎn)發(fā)到 http://localhost:9080/chapter23-app1
訪問 http://localhost/chapter23-app3 會(huì)自動(dòng)轉(zhuǎn)發(fā)到 http://localhost:10080/chapter23-app3;

Nginx 的安裝及使用請(qǐng)自行搜索學(xué)習(xí),本文不再闡述。

項(xiàng)目架構(gòu)

  1. 首先通過用戶 / 權(quán)限 Server 維護(hù)用戶、應(yīng)用、權(quán)限信息;數(shù)據(jù)都持久化到 MySQL 數(shù)據(jù)庫(kù)中;
  2. 應(yīng)用 App1 / 應(yīng)用 App2 使用客戶端 Client 遠(yuǎn)程調(diào)用用戶 / 權(quán)限 Server 獲取會(huì)話及權(quán)限信息。

此處使用 Mysql 存儲(chǔ)會(huì)話,而不是使用如 Memcached/Redis 之類的,主要目的是降低學(xué)習(xí)成本;如果換成如 Redis 也不會(huì)很難;如:

使用如 Redis 還一個(gè)好處就是無需在用戶 / 權(quán)限 Server 中開會(huì)話過期調(diào)度器,可以借助 Redis 自身的過期策略來完成。

模塊關(guān)系依賴

1、shiro-example-chapter23-pom 模塊:提供了其他所有模塊的依賴;這樣其他模塊直接繼承它即可,簡(jiǎn)化依賴配置,如 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 * 模塊的核心依賴,比如遠(yuǎn)程調(diào)用接口等;

3、shiro-example-chapter23-server 模塊:提供了用戶、應(yīng)用、權(quán)限管理功能;

4、shiro-example-chapter23-client 模塊:提供給應(yīng)用模塊獲取會(huì)話及應(yīng)用對(duì)應(yīng)的權(quán)限信息;

5、shiro-example-chapter23-app * 模塊:各個(gè)子應(yīng)用,如一些內(nèi)部管理系統(tǒng)應(yīng)用;其登錄都跳到 shiro-example-chapter23-server 登錄;另外權(quán)限都從 shiro-example-chapter23-server 獲?。ㄈ缤ㄟ^遠(yuǎn)程調(diào)用)。

shiro-example-chapter23-pom 模塊

其 pom.xml 的 packaging 類型為 pom,并且在該 pom 中加入其他模塊需要的依賴,然后其他模塊只需要把該模塊設(shè)置為 parent 即可自動(dòng)繼承這些依賴,如 shiro-example-chapter23-server 模塊:

<parent>
    <artifactId>shiro-example-chapter23-pom</artifactId>
    <groupId>com.github.zhangkaitao</groupId>
    <version>1.0-SNAPSHOT</version>
</parent>

簡(jiǎn)化其他模塊的依賴配置等。

shiro-example-chapter23-core 模塊

提供了其他模塊共有的依賴,如遠(yuǎn)程調(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);
}

提供了會(huì)話的 CRUD,及根據(jù)應(yīng)用 key 和用戶名獲取權(quán)限上下文(包括角色和權(quán)限字符串);shiro-example-chapter23-server 模塊服務(wù)端實(shí)現(xiàn);shiro-example-chapter23-client 模塊客戶端調(diào)用。

另外提供了 com.github.zhangkaitao.shiro.chapter23.core.ClientSavedRequest,其擴(kuò)展了 org.apache.shiro.web.util.SavedRequest;用于 shiro-example-chapter23-app * 模塊當(dāng)訪問一些需要登錄的請(qǐng)求時(shí),自動(dòng)把請(qǐng)求保存下來,然后重定向到 shiro-example-chapter23-server 模塊登錄;登錄成功后再重定向回來;因?yàn)?SavedRequest 不保存 URL 中的 schema://domain:port 部分;所以才需要擴(kuò)展 SavedRequest;使得 ClientSavedRequest 能保存 schema://domain:port;這樣才能從一個(gè)應(yīng)用重定向另一個(gè)(要不然只能在一個(gè)應(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();
    }
  1. 如果從外部傳入了 successUrl(登錄成功之后重定向的地址),且以 http://https:// 開頭那么直接返回(相應(yīng)的攔截器直接重定向到它即可);
  2. 如果 successUrl 有值但沒有上下文,拼上上下文;
  3. 否則,如果 successUrl 有值,直接賦值給 requestUrl 即可;否則,如果 successUrl 沒值,那么 requestUrl 就是當(dāng)前請(qǐng)求的地址;
  4. 拼上 url 前邊的 schema,如 http 或 https;
  5. 拼上域名;
  6. 拼上重定向到的地址(帶上下文);
  7. 如果 successUrl 沒值,且有查詢參數(shù),拼上;
  8. 返回該地址,相應(yīng)的攔截器直接重定向到它即可。

shiro-example-chapter23-server 模塊

簡(jiǎn)單的實(shí)體關(guān)系圖

簡(jiǎn)單數(shù)據(jù)字典

用戶 (sys_user)

名稱

類型

長(zhǎng)度

描述

id

bigint

 

編號(hào) 主鍵

username

varchar

100

用戶名

password

varchar

100

密碼

salt

varchar

50

locked

bool

 

賬戶是否鎖定

應(yīng)用 (sys_app)

名稱

類型

長(zhǎng)度

描述

id

bigint

 

編號(hào) 主鍵

name

varchar

100

應(yīng)用名稱

app_key

varchar

100

應(yīng)用 key(唯一)

app_secret

varchar

100

應(yīng)用安全碼

available

bool

 

是否鎖定

授權(quán) (sys_authorization)

名稱

類型

長(zhǎng)度

描述

id

bigint

 

編號(hào) 主鍵

user_id

bigint

 

所屬用戶

app_id

bigint

 

所屬應(yīng)用

role_ids

varchar

100

角色列表

用戶:比《第十六章 綜合實(shí)例》少了 role_ids,因?yàn)楸菊率嵌囗?xiàng)目集中權(quán)限管理;所以授權(quán)時(shí)需要指定相應(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 時(shí)需要指定自己的 app_key 和用戶名來獲取該 app 對(duì)應(yīng)用戶權(quán)限信息;另外 app_secret 可以認(rèn)為 app 的密碼,比如需要安全訪問時(shí)可以考慮使用它,可參考《第二十章 無狀態(tài) Web 應(yīng)用集成》。另外 available 屬性表示該應(yīng)用當(dāng)前是否開啟;如果 false 表示該應(yīng)用當(dāng)前不可用,即不能獲取到相應(yīng)的權(quán)限信息。

授權(quán):給指定的用戶在指定的 app 下授權(quán),即角色是與用戶和 app 存在關(guān)聯(lián)關(guān)系。

因?yàn)楸菊率褂昧恕兜谑?綜合實(shí)例》代碼,所以還有其他相應(yīng)的表結(jié)構(gòu)(本章未使用到)。

表 / 數(shù)據(jù) SQL

具體請(qǐng)參考

  • sql/shiro-schema.sql (表結(jié)構(gòu))
  • sql/shiro-data.sql (初始數(shù)據(jù))

實(shí)體

具體請(qǐng)參考 com.github.zhangkaitao.shiro.chapter23.entity 包下的實(shí)體,此處就不列舉了。

DAO

具體請(qǐng)參考 com.github.zhangkaitao.shiro.chapter23.dao 包下的 DAO 接口及實(shí)現(xiàn)。

Service

具體請(qǐng)參考 com.github.zhangkaitao.shiro.chapter23.service 包下的 Service 接口及實(shí)現(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)用中對(duì)于的角色和權(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)限字符串集合。其他的和《第十六章 綜合實(shí)例》代碼一樣。

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àn)槭嵌囗?xiàng)目登錄,比如如果是從其他應(yīng)用中重定向過來的,首先檢查 Session 中是否有 “authc.fallbackUrl” 屬性,如果有就認(rèn)為它是默認(rèn)的重定向地址;否則使用 Server 自己的 successUrl 作為登錄成功后重定向到的地址。

MySqlSessionDAO

將會(huì)話持久化到 Mysql 數(shù)據(jù)庫(kù);此處大家可以將其實(shí)現(xiàn)為如存儲(chǔ)到 Redis/Memcached 等,實(shí)現(xiàn)策略請(qǐng)參考《第十章 會(huì)話管理》中的會(huì)話存儲(chǔ) / 持久化章節(jié)的 MySessionDAO,完全一樣。

MySqlSessionValidationScheduler

和《第十章 會(huì)話管理》中的會(huì)話驗(yàn)證章節(jié)部分中的 MySessionValidationScheduler 完全一樣。如果使用如 Redis 之類的有自動(dòng)過期策略的 DB,完全可以不用實(shí)現(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;
    }
}

將會(huì)使用 HTTP 調(diào)用器暴露為遠(yuǎn)程服務(wù),這樣其他應(yīng)用就可以使用相應(yīng)的客戶端調(diào)用這些接口進(jìn)行 Session 的集中維護(hù)及根據(jù) AppKey 和用戶名獲取角色 / 權(quán)限字符串集合。此處沒有實(shí)現(xiàn)安全校驗(yàn)功能,如果是局域網(wǎng)內(nèi)使用可以通過限定 IP 完成;否則需要使用如《第二十章 無狀態(tài) Web 應(yīng)用集成》中的技術(shù)完成安全校驗(yàn)。

然后在 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

和《第十六章 綜合實(shí)例》配置類似,但是需要在 shiroFilter 中的 filterChainDefinitions 中添加如下配置,即遠(yuǎn)程調(diào)用不需要身份認(rèn)證:

/remoteService = anon

對(duì)于 userRealm 的緩存配置直接禁用;因?yàn)槿绻_啟,修改了用戶權(quán)限不會(huì)自動(dòng)同步到緩存;另外請(qǐng)參考《第十一章 緩存機(jī)制》進(jìn)行緩存的正確配置。

服務(wù)器端數(shù)據(jù)維護(hù)

1、首先開啟 ngnix 反向代理;然后就可以直接訪問 http://localhost/chapter23-server/; 2、輸入默認(rèn)的用戶名密碼:admin/123456 登錄 3、應(yīng)用管理,進(jìn)行應(yīng)用的 CRUD,主要維護(hù)應(yīng)用 KEY(必須唯一)及應(yīng)用安全碼;客戶端就可以使用應(yīng)用 KEY 獲取用戶對(duì)應(yīng)應(yīng)用的權(quán)限了。

4、授權(quán)管理,維護(hù)在哪個(gè)應(yīng)用中用戶的角色列表。這樣客戶端就可以根據(jù)應(yīng)用 KEY 及用戶名獲取到對(duì)應(yīng)的角色 / 權(quán)限字符串列表了。

shiro-example-chapter23-client 模塊

Client 模塊提供給其他應(yīng)用模塊依賴,這樣其他應(yīng)用模塊只需要依賴 Client 模塊,然后再在相應(yīng)的配置文件中配置如登錄地址、遠(yuǎn)程接口地址、攔截器鏈等等即可,簡(jiǎn)化其他應(yīng)用模塊的配置。

配置遠(yuǎn)程服務(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 是遠(yuǎn)程服務(wù)暴露的地址;通過相應(yīng)的 properties 配置文件配置,后續(xù)介紹。然后就可以通過 remoteService 獲取會(huì)話及角色 / 權(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 {
        //永遠(yuǎn)不會(huì)被調(diào)用
        throw new UnsupportedOperationException("永遠(yuǎn)不會(huì)被調(diào)用");
    }
}

ClientRealm 提供身份認(rèn)證信息和授權(quán)信息,此處因?yàn)槭瞧渌麘?yīng)用依賴客戶端,而這些應(yīng)用不會(huì)實(shí)現(xiàn)身份認(rèn)證,所以 doGetAuthenticationInfo 獲取身份認(rèn)證信息直接無須實(shí)現(xiàn)。另外獲取授權(quán)信息,是通過遠(yuǎ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 的維護(hù)通過遠(yuǎn)程暴露接口實(shí)現(xiàn),即本地不維護(hù)會(huì)話。

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 是用于實(shí)現(xiàn)身份認(rèn)證的攔截器(authc),當(dāng)用戶沒有身份認(rèn)證時(shí);

  1. 首先得到請(qǐng)求參數(shù) backUrl,即登錄成功重定向到的地址;
  2. 然后保存保存請(qǐng)求到會(huì)話,并重定向到登錄地址(server 模塊);
  3. 登錄成功后,返回地址按照如下順序獲?。篵ackUrl、保存的當(dāng)前請(qǐng)求地址、defaultBackUrl(即設(shè)置的 successUrl);

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]);
        }
    }
}
  1. setFiltersStr:設(shè)置攔截器,設(shè)置格式如 “filterName=filterBeanName; filterName=filterBeanName”;多個(gè)之間分號(hào)分隔;然后通過 applicationContext 獲取 filterBeanName 對(duì)應(yīng)的 Bean 注冊(cè)到攔截器 Map 中;
  2. setFilterChainDefinitionsStr:設(shè)置攔截器鏈,設(shè)置格式如 “url=filterName1[config],filterName2; url=filterName1[config],filterName2”;多個(gè)之間分號(hào)分隔;

Shiro 客戶端配置 spring-client.xml

提供了各應(yīng)用通用的 Shiro 客戶端配置;這樣應(yīng)用只需要導(dǎo)入相應(yīng)該配置即可完成 Shiro 的配置,簡(jiǎn)化了整個(gè)配置過程。

<context:property-placeholder location= 
    "classpath:client/shiro-client-default.properties,classpath:client/shiro-client.properties"/>

提供給客戶端配置的 properties 屬性文件,client/shiro-client-default.properties 是客戶端提供的默認(rèn)的配置;classpath:client/shiro-client.properties 是用于覆蓋客戶端默認(rèn)配置,各應(yīng)用應(yīng)該提供該配置文件,然后提供各應(yīng)用個(gè)性配置。

<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)用無須進(jìn)行會(huì)話過期調(diào)度,所以 sessionValidationSchedulerEnabled=false。

<bean id="clientAuthenticationFilter" 
  class="com.github.zhangkaitao.shiro.chapter23.client.ClientAuthenticationFilter"/>

其他應(yīng)用無須進(jìn)行會(huì)話過期調(diào)度,所以 sessionValidationSchedulerEnabled=false。

<bean id="clientAuthenticationFilter" 
class="com.github.zhangkaitao.shiro.chapter23.client.ClientAuthenticationFilter"/>

應(yīng)用的身份認(rèn)證使用 ClientAuthenticationFilter,即如果沒有身份認(rèn)證,則會(huì)重定向到 Server 模塊完成身份認(rèn)證,身份認(rèn)證成功后再重定向回來。

<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(登錄成功后默認(rèn)的重定向地址)、unauthorizedUrl(未授權(quán)重定向到的地址)通過占位符替換方式配置;另外 filtersStr 和 filterChainDefinitionsStr 也是使用占位符替換方式配置;這樣就可以在各應(yīng)用進(jìn)行自定義了。

默認(rèn)配置 client/shiro-client-default.properties

\#各應(yīng)用的appKey
client.app.key=
\#遠(yuǎn)程服務(wù)URL地址
client.remote.service.url=http://localhost/chapter23-server/remoteService
\#登錄地址
client.login.url=http://localhost/chapter23-server/login
\#登錄成功后,默認(rèn)重定向到的地址
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-app * 模塊

繼承 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 模塊維護(hù)的,直接拷貝過來即可;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 模塊維護(hù)的,直接拷貝過來即可;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。

其他配置請(qǐng)參考源碼。

測(cè)試

1、安裝配置啟動(dòng) 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 啟動(dòng) Nginx 即可。

已經(jīng)配置好的 nginx 請(qǐng)到 shiro-example-chapter23-nginx 模塊下下周 nginx-1.5.11.rar 即可。

2、安裝依賴

1、首先安裝 shiro-example-chapter23-core 依賴,到 shiro-example-chapter23-core 模塊下運(yùn)行 mvn install 安裝 core 模塊。

2、接著到 shiro-example-chapter23-client 模塊下運(yùn)行 mvn install 安裝客戶端模塊。

3、啟動(dòng) Server 模塊

到 shiro-example-chapter23-server 模塊下運(yùn)行 mvn jetty:run 啟動(dòng)該模塊;使用 http://localhost:8080/chapter23-server/ 即可訪問,因?yàn)閱?dòng)了 nginx,那么可以直接訪問 http://localhost/chapter23-server/。

4、啟動(dòng) App* 模塊

到 shiro-example-chapter23-app1 和 shiro-example-chapter23-app2 模塊下分別運(yùn)行 mvn jetty:run 啟動(dòng)該模塊;使用 http://localhost:9080/chapter23-app1/http://localhost:10080/chapter23-app2/ 即可訪問,因?yàn)閱?dòng)了 nginx,那么可以直接訪問 http://localhost/chapter23-app1/http://localhost/chapter23-app2/

5、服務(wù)器端維護(hù)

1、訪問 http://localhost/chapter23-server/;

2、輸入默認(rèn)的用戶名密碼:admin/123456 登錄

3、應(yīng)用管理,進(jìn)行應(yīng)用的 CRUD,主要維護(hù)應(yīng)用 KEY(必須唯一)及應(yīng)用安全碼;客戶端就可以使用應(yīng)用 KEY 獲取用戶對(duì)應(yīng)應(yīng)用的權(quán)限了。

4、授權(quán)管理,維護(hù)在哪個(gè)應(yīng)用中用戶的角色列表。這樣客戶端就可以根據(jù)應(yīng)用 KEY 及用戶名獲取到對(duì)應(yīng)的角色 / 權(quán)限字符串列表了。

*6、App 模塊身份認(rèn)證及授權(quán)**

1、在未登錄情況下訪問 http://localhost/chapter23-app1/hello,看到下圖:

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

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

4、可以看到 admin 登錄,及其是否擁有 role1/role2 角色;可以在 server 模塊移除 role1 角色或添加 role2 角色看看頁(yè)面變化;

5、可以在 http://localhost/chapter23-app1/hello 頁(yè)面設(shè)置屬性,如 key=123;接著訪問 http://localhost/chapter23-app2/attr?key=key 就可以看到剛才設(shè)置的屬性,如下圖:

另外在 app2,用戶默認(rèn)擁有 role2 角色,而沒有 role1 角色。

到此整個(gè)測(cè)試就完成了,可以看出本示例實(shí)現(xiàn)了:會(huì)話的分布式及權(quán)限的集中管理。

本示例缺點(diǎn)

  1. 沒有加緩存;
  2. 客戶端每次獲取會(huì)話 / 權(quán)限都需要通過客戶端訪問服務(wù)端;造成服務(wù)端單點(diǎn)和請(qǐng)求壓力大;單點(diǎn)可以考慮使用集群來解決;請(qǐng)求壓力大需要考慮配合緩存服務(wù)器(如 Redis)來解決;即每次會(huì)話 / 權(quán)限獲取時(shí)首先查詢緩存中是否存在,如果有直接獲取即可;否則再查服務(wù)端;降低請(qǐng)求壓力;
  3. 會(huì)話的每次更新(比如設(shè)置屬性 / 更新最后訪問時(shí)間戳)都需要同步到服務(wù)端;也造成了請(qǐng)求壓力過大;可以考慮在請(qǐng)求的最后只同步一次會(huì)話(需要對(duì) Shiro 會(huì)話進(jìn)行改造,通過如攔截器在執(zhí)行完請(qǐng)求后完成同步,這樣每次請(qǐng)求只同步一次);
  4. 只能同域名才能使用,即會(huì)話 ID 是從同一個(gè)域名下獲取,如果跨域請(qǐng)考慮使用 CAS/OAuth2 之實(shí)現(xiàn)。

所以實(shí)際應(yīng)用時(shí)可能還是需要改造的,但大體思路是差不多的。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)