Shiro多项目权限集中管理平台

[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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,083评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,283评论 6 344
  • 此刻我正在候机,百无聊赖的想起白天买的一个福袋,拿出来把玩,发现里边有祝福语,这祝福语是我看过最拉风的了,没有之一...
    仲璞阅读 669评论 0 0
  • 语音写作,2015年10月1日,第四部分 现在是下午3:20到,下午4点钟之间的写作,就是要看看,在一天的时间中,...
    剑飞在思考阅读 1,397评论 1 4
  • 上次化验血,被告知几项数据不合格,看着吓人。思前想后,以后不能半夜码字了。可是,我不码字,也有其他的事情要去做啊,...
    她慢说阅读 381评论 0 0