在作一些企业内部项目时或一些互联网后台时;可能会涉及到集中权限管理,统一进行多项目的权限管理;另外也须要统一的会话管理,即实现单点身份认证和受权控制。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之实现。
因此实际应用时可能仍是须要改造的,但大致思路是差很少的。