Shiro多專案許可權集中管理平臺

怪咖_OOP發表於2017-12-24

[TOC]

更新記錄

客戶端整合示例 2017-12-28

專案起源:

公司隨著業務的增長,各業務進行水平擴充套件面臨拆分;隨著業務的拆分各種管理系統撲面而來,為了方便許可權統一管理,不得不自己開發或使用分散式許可權管理(Spring Security)。Spring Security依賴Spring和初級開發人員學習難度大,中小型公司不推薦使用;Apache Shiro是一個強大易用的安全框架,Shiro的API方便理解。經過網上各路大神對shiro與spring security的比較,最終決定使用shiro開發一個獨立的許可權管理平臺。

該專案是在張開濤跟我學shiro Demo基礎上進行開發、功能完善和管理頁面優化,已上傳GitHub歡迎fork、start及提出改進意見。

公用模組:shiro-distributed-platform-core

自定義註解-CurrentUser

通過註解獲取當前登入使用者

請求攔截器-SysUserFilter

攔截所有請求 1.通過shiro Subject 獲取當前使用者的使用者名稱 2.通過使用者名稱獲取使用者資訊 3.將使用者資訊儲存ServletRequest物件中 程式碼示例:

/**
 * 
* @ClassName: SysUserFilter 
* @Description: 請求攔截器
* @author yangzhao
* @date 2017年12月20日 下午2:10:23
*
 */
public class SysUserFilter extends PathMatchingFilter {

    @Autowired
    private UserService userService;

    @Override
    protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {

        String username = (String)SecurityUtils.getSubject().getPrincipal();
        User user = userService.findByUsername(username);
        request.setAttribute(Constant.CURRENT_USER,user);
        return true;
    }
}
複製程式碼

Spring方法攔截器引數繫結-CurrentUserMethodArgumentResolver

通過SpringAOP攔截容器內所有Java方法引數是否有CurrentUser註解,若果有註解標識從NativeWebRequest中獲取user資訊進行引數繫結 程式碼示例:

/**
 * 
* @ClassName: CurrentUserMethodArgumentResolver 
* @Description: Spring方法攔截器引數繫結
* @author yangzhao
* @date 2017年12月20日 下午2:07:55
*
 */
public class CurrentUserMethodArgumentResolver implements HandlerMethodArgumentResolver {

    public CurrentUserMethodArgumentResolver() {
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        if (parameter.hasParameterAnnotation(CurrentUser.class)) {
            return true;
        }
        return false;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        CurrentUser currentUserAnnotation = parameter.getParameterAnnotation(CurrentUser.class);
        return webRequest.getAttribute(currentUserAnnotation.value(), NativeWebRequest.SCOPE_REQUEST);
    }
}

複製程式碼

認證攔截器-ServerFormAuthenticationFilter

shiro許可權認證通過後進行頁面跳轉 程式碼示例:

/**
 * 
* @ClassName: ServerFormAuthenticationFilter 
* @Description: 認證攔截器-頁面跳轉
* @author yangzhao
* @date 2017年12月20日 下午2:10:18
*
 */
public class ServerFormAuthenticationFilter extends FormAuthenticationFilter {

    @Override
    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);
    }


複製程式碼

公用介面:shiro-distributed-platform-api

許可權系統核心API

AppService-應用API
AreaService-區域API
AuthorizationService-授權API
OrganizationService-組織結構API
ResourceService-資源API
RoleService-角色API
UserService-使用者API

客戶端:shiro-distributed-platform-client

客戶端認證攔截-ClientAuthenticationFilter

1.判斷是否認證通過 若未認證進入下一步 2.從ServletRequest獲取回撥url 3.獲取預設回撥url(客戶端IP和埠) 4.將預設回撥url儲存到session中 5.將第2步中的回撥url儲存到ClientSavedRequest中(方便server回撥時返回到當前請求url) 5.當前請求重定向到server端登入頁面 程式碼示例:

/**
 *
 * @ClassName: AppService
 * @Description: 客戶端認證攔截
 * @author yangzhao
 * @date 2017年12月20日 下午2:03:43
 *
 */
public class ClientAuthenticationFilter extends AuthenticationFilter {

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        Subject subject = getSubject(request, response);
        return subject.isAuthenticated();
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        String backUrl = request.getParameter("backUrl");
        saveRequest(request, backUrl, getDefaultBackUrl(WebUtils.toHttp(request)));
        redirectToLogin(request, response);
        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();
    }

}

複製程式碼

ClientRealm

ClientRealm繼承自Shiro AuthorizingRealm 該類忽略doGetAuthenticationInfo方法實現,所有認證操作會轉到Server端實現 程式碼示例:

/**
 *
 * @ClassName: ClientRealm
 * @Description: 客戶端shiro Realm
 * @author yangzhao
 * @date 2017年12月20日 下午2:03:43
 *
 */
public class ClientRealm extends AuthorizingRealm {
    private RemoteService remoteService;
    private String appKey;
    public void setRemoteService(RemoteService remoteService) {
        this.remoteService = remoteService;
    }
    public void setAppKey(String appKey) {
        this.appKey = appKey;
    }
    @Override
    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;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //永遠不會被呼叫
        throw new UnsupportedOperationException("永遠不會被呼叫");
    }
}
複製程式碼

客戶端Session管理-ClientSessionDAO

實時更新、獲取遠端session

客戶端shiro攔截器工廠管理類-ClientShiroFilterFactoryBean

新增兩個方法setFiltersStr、setFilterChainDefinitionsStr,方便在properties檔案中配置攔截器和定義過濾鏈 程式碼示例:

/**
 *
 * @ClassName: ClientShiroFilterFactoryBean
 * @Description: 新增兩個方法setFiltersStr、setFilterChainDefinitionsStr,方便在properties檔案中配置攔截器和定義過濾鏈
 * @author yangzhao
 * @date 2017年12月20日 下午2:03:43
 *
 */
public class ClientShiroFilterFactoryBean extends ShiroFilterFactoryBean implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    @Override
    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-distributed-platform-server

介面暴露

通過Spring HttpInvokerServiceExporter工具將shiro-distributed-platform-api模組部分API暴露(remoteService、userService、resourceService),請參考spring-mvc-export-service.xml配置檔案

配置ShiroFilterFactoryBean filterChainDefinitions屬性將以上三個介面許可權設定為遊客、匿名(anon),請參考spring-config-shiro.xml配置檔案

客戶端整合:

第一步: 在專案resources目錄下新建shiro-client.properties配置檔案

#各應用的appKey
client.app.key=1f38e90b-7c56-4c1d-b3a5-7b4b6ec94778
#遠端服務URL地址
client.remote.service.url=http://127.0.0.1:8080 #根據實際應用地址配置
#登入地址
client.login.url=http://127.0.0.1:8080/login #根據實際應用地址配置
#登入成功後,預設重定向到的地址
client.success.url=/
#未授權重定向到的地址
client.unauthorized.url=http://127.0.0.1:8080/login #根據實際應用地址配置
#session id 域名
client.cookie.domain=
#session id 路徑
client.cookie.path=/
#cookie中的session id名稱
client.session.id=sid
#cookie中的remember me名稱
client.rememberMe.id=rememberMe
複製程式碼

第二步: 在專案resources目錄下新建spring-shiro.xml配置檔案

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx" xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

    <bean id="remoteRealm" class="com.yz.shiro.client.ClientRealm">
        <property name="appKey" value="${client.app.key}"/>
        <property name="remoteService" ref="remoteService"/>
    </bean>

    <!-- 會話ID生成器 -->
    <bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/>

    <!-- 會話Cookie模板 -->
    <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>

    <!-- rememberMe管理器 -->
    <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
        <!-- rememberMe cookie加密的金鑰 建議每個專案都不一樣 預設AES演算法 金鑰長度(128 256 512 位)-->
        <property name="cipherKey"
                  value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}"/>
        <property name="cookie" ref="rememberMeCookie"/>
    </bean>

    <!-- 會話DAO -->
    <bean id="sessionDAO" class="com.yz.shiro.client.ClientSessionDAO">
        <property name="sessionIdGenerator" ref="sessionIdGenerator"/>
        <property name="appKey" value="${client.app.key}"/>
        <property name="remoteService" ref="remoteService"/>
    </bean>

    <!-- 會話管理器 -->
    <bean id="sessionManager" class="com.yz.shiro.client.ClientWebSessionManager">
        <property name="deleteInvalidSessions" value="false"/>
        <property name="sessionValidationSchedulerEnabled" value="false"/>
        <property name="sessionDAO" ref="sessionDAO"/>
        <property name="sessionIdCookieEnabled" value="true"/>
        <property name="sessionIdCookie" ref="sessionIdCookie"/>
    </bean>

    <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
        <constructor-arg value="${client.rememberMe.id}"/>
        <property name="httpOnly" value="true"/>
        <property name="maxAge" value="2592000"/><!-- 30天 -->
        <property name="domain" value="${client.cookie.domain}"/>
        <property name="path" value="${client.cookie.path}"/>
    </bean>


    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="remoteRealm"/>
        <property name="sessionManager" ref="sessionManager"/>
        <property name="rememberMeManager" ref="rememberMeManager"/>
    </bean>

    <!-- 相當於呼叫SecurityUtils.setSecurityManager(securityManager) -->
    <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
        <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
        <property name="arguments" ref="securityManager"/>
    </bean>

    <bean id="clientAuthenticationFilter" class="com.yz.shiro.client.ClientAuthenticationFilter"/>
    <bean id="sysUserFilter" class="com.yz.shiro.core.filter.SysUserFilter"/>

    <!-- Shiro的Web過濾器 -->
    <bean id="shiroFilter" class="com.yz.shiro.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"/>
                <entry key="sysUser" value-ref="sysUserFilter"/>
            </util:map>
        </property>
        <property name="filterChainDefinitions">
            <value>
                /web/** = anon
                /**= authc,sysUser
            </value>
        </property>
    </bean>
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
    <import resource="classpath*:spring-client-remote-service.xml"/>
</beans>
複製程式碼

第三步 將新增的兩個配置檔案引入spring-context配置檔案中

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
       
<context:property-placeholder location="classpath:shiro-client.properties" ignore-unresolvable="true"/>

<import resource="spring-shiro.xml"/>

</beans>
複製程式碼

以上屬於原創文章,轉載請註明作者@怪咖
QQ:208275451
Email:yangzhao_java@163.com

相關文章