1、参考代码
http://git.codeweblog.com/chunanyong/springrain
2、主要说明
(1)SSO,即单点登录认证,采用的是shiro+redis的方式,实现集中式的session管理
(2)鉴权,即权限校验,基于经典的role-user-resource(这里一般指menu)模型,还是采用shiro,自己实现鉴权方法与shiro的securityManager集成即可
3、添加依赖
主要是shiro、redis的依赖
<!--shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>${shiro.version}</version> </dependency> <!--spring redis as share session--> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <version>${spring-data-redis.version}</version> </dependency> <!-- Redis Java Driver --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.6.0</version> </dependency>
<shiro.version>1.2.3</shiro.version>
<spring-data-redis.version>1.4.0.RELEASE</spring-data-redis.version>
4、xml配置
(1)配置web.xml
<!--add shiro filter--> <filter> <!--需要在(parent) context中声明id为shiroFilter的bean--> <filter-name>shiroFilter</filter-name> <!-- DelegatingFilterProxy,该类其实并不能说是一个过滤器,它的原型是FilterToBeanProxy,即将Filter作为spring的bean,由spring来管理--> <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> <dispatcher>REQUEST</dispatcher> <dispatcher>FORWARD</dispatcher> <dispatcher>INCLUDE</dispatcher> <dispatcher>ERROR</dispatcher> </filter-mapping>
(2)配置application-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" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd" default-lazy-init="false" > <!-- shiro的主过滤器,beanId 和web.xml中配置的filter name需要保持一致 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!-- 安全管理器 --> <property name="securityManager" ref="securityManager" /> <!-- 默认的登陆访问url --> <property name="loginUrl" value="/login" /> <!-- 登陆成功后跳转的url --> <property name="successUrl" value="/index" /> <!-- 没有权限跳转的url --> <property name="unauthorizedUrl" value="/unauth" /> <!-- 访问地址的过滤规则,从上至下的优先级,如果有匹配的规则,就会返回,不会再进行匹配 --> <property name="filterChainDefinitions"> <value> /js/** = anon /css/** = anon /images/** = anon /unauth = anon /getCaptcha=anon /login = anon /auto/login = anon /favicon.ico = anon /index = user /logout = logout /system/menu/leftMenu=user /**/ajax/** = user /** = user,permissionCheck </value> </property> <!-- 声明自定义的过滤器 --> <property name="filters"> <map> <entry key="permissionCheck" value-ref="shiroSSOUpmFilter"></entry> </map> </property> </bean> <!-- session 集群 --> <bean id="shiroCacheManager" class="com.persia.shiro.cache.ShiroRedisCacheManager"> <!--在applicationContext-redis.xml里头声明--> <property name="cached" ref="redisCacheService" /> </bean> <!-- 权限管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!-- 基于数据库登录校验的实现 com.persia.upm.ShiroDbRealm --> <property name="realm" ref="shiroDbRealm" /> <!-- session 管理器 --> <property name="sessionManager" ref="sessionManager" /> <!-- 缓存管理器 --> <property name="cacheManager" ref="shiroCacheManager" /> </bean> <!-- session管理器 --> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <!-- 超时时间 --> <property name="globalSessionTimeout" value="1800000" /> <!-- session存储的实现 --> <property name="sessionDAO" ref="shiroSessionDao" /> <!-- sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID --> <property name="sessionIdCookie" ref="sharesession" /> <!-- 定时检查失效的session --> <property name="sessionValidationSchedulerEnabled" value="true" /> </bean> <!-- sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID --> <bean id="sharesession" class="org.apache.shiro.web.servlet.SimpleCookie"> <!-- cookie的name,对应的默认是 JSESSIONID --> <constructor-arg name="name" value="SHAREJSESSIONID" /> <!-- jsessionId的path为 / 用于多个系统共享jsessionId --> <property name="path" value="/" /> </bean> <!-- session存储的实现 --> <bean id="shiroSessionDao" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO" /> </beans>
(3)配置application-redis.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:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd" default-lazy-init="false"> <context:property-placeholder location="classpath:config.properties" /> <!--基于redis分布的session共享--> <bean id="redisCacheService" class="com.persia.shiro.cache.RedisCachedImpl"> <property name="redisTemplate" ref="redisTemplate" /> <property name="expire" value="86400" /> </bean> <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"> <property name="connectionFactory" ref="jedisConnectionFactory" /> </bean> <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> <property name="hostName" value="${redis.host}" /> <property name="port" value="${redis.port}" /> <property name="poolConfig" ref="jedisPoolConfig" /> </bean> <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxTotal" value="${redis.pool.maxTotal}" /> <property name="maxIdle" value="${redis.pool.maxIdle}" /> <property name="maxWaitMillis" value="${redis.pool.maxWaitMillis}" /> <property name="testOnBorrow" value="${redis.pool.testOnBorrow}" /> </bean> </beans>
5、代码
(1)ShiroSSOUpmFilter
import com.persia.Constants; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.SecurityUtils; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheManager; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import javax.annotation.Resource; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; @Service public class ShiroSSOUpmFilter extends PermissionsAuthorizationFilter { public Logger logger = LoggerFactory.getLogger(getClass()); @Resource private CacheManager shiroCacheManager; @Override public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException { //upm with shiro subject/principal Subject user = SecurityUtils.getSubject(); ShiroUser shiroUser = (ShiroUser) user.getPrincipal(); //get sso session Session session = user.getSession(false); Cache<Object, Object> cache = shiroCacheManager.getCache(Constants.SSO_CACHE); Object cachedSession = cache.get(Constants.SSO_CACHE + "-" + shiroUser.getAccount()); if(cachedSession == null){ user.logout(); return false; } String cachedSessionId =cachedSession.toString(); String sessionId = (String) session.getId(); if (!sessionId.equals(cachedSessionId)) { user.logout(); } HttpServletRequest req = (HttpServletRequest) request; //get shiro upm Subject subject = getSubject(request, response); String uri = req.getRequestURI(); String contextPath = req.getContextPath(); int i = uri.indexOf(contextPath); if (i > -1) { uri = uri.substring(i + contextPath.length()); } if (StringUtils.isBlank(uri)) { uri = "/"; } boolean permitted = false; if ("/".equals(uri)) { permitted = true; } else { //check has right using shiro permitted = subject.isPermitted(uri); } return permitted; } }
(2)ShiroCacheManager即ShiroRedisCacheManager
import org.apache.shiro.cache.AbstractCacheManager; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; public class ShiroRedisCacheManager extends AbstractCacheManager { private ICached cached; @Override protected Cache createCache(String cacheName) throws CacheException { return new ShiroRedisCache<String, Object>(cacheName,cached); } public ICached getCached() { return cached; } public void setCached(ICached cached) { this.cached = cached; } }
(3)ShiroDbRealm
import com.persia.Constants; import com.persia.service.UpmService; import com.persia.shiro.ShiroUser; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheManager; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.session.Session; import org.apache.shiro.subject.PrincipalCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.Resource; //认证数据库存储 @Component("shiroDbRealm") public class ShiroDbRealm extends AuthorizingRealm { public Logger logger = LoggerFactory.getLogger(getClass()); @Resource private UpmService upmService; @Resource private CacheManager shiroCacheManager; public static final String HASH_ALGORITHM = "MD5"; public static final int HASH_INTERATIONS = 1; private static final int SALT_SIZE = 8; public ShiroDbRealm() { // 认证 super.setAuthenticationCacheName(Constants.SSO_CACHE); super.setAuthenticationCachingEnabled(false); // 授权 super.setAuthorizationCacheName(Constants.AUTH_CACHE); super.setName(Constants.AUTH_REALM); } // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principalCollection) { // 因为非正常退出,即没有显式调用 SecurityUtils.getSubject().logout() // (可能是关闭浏览器,或超时),但此时缓存依旧存在(principals),所以会自己跑到授权方法里。 if (!SecurityUtils.getSubject().isAuthenticated()) { doClearCache(principalCollection); SecurityUtils.getSubject().logout(); return null; } ShiroUser shiroUser = (ShiroUser) principalCollection .getPrimaryPrincipal(); // String userId = (String) // principalCollection.fromRealm(getName()).iterator().next(); String userId = shiroUser.getId(); if (StringUtils.isBlank(userId)) { return null; } // 添加角色及权限信息 SimpleAuthorizationInfo sazi = new SimpleAuthorizationInfo(); try { sazi.addRoles(upmService.getRolesAsString(userId)); sazi.addStringPermissions(upmService.getPermissionsAsString(userId)); } catch (Exception e) { logger.error(e.getMessage(),e); } return sazi; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken = (UsernamePasswordToken) token; /* * String pwd = new String(upToken.getPassword()); if * (StringUtils.isNotBlank(pwd)) { pwd = DigestUtils.md5Hex(pwd); } */ // 调用业务方法 User user = null; String userName = upToken.getUsername(); try { user = upmService.findLoginUser(userName, null); } catch (Exception e) { logger.error(e.getMessage(),e); throw new AuthenticationException(e); } if (user != null) { // 要放在作用域中的东西,请在这里进行操作 // SecurityUtils.getSubject().getSession().setAttribute("c_user", // user); // byte[] salt = EncodeUtils.decodeHex(user.getSalt()); Session session = SecurityUtils.getSubject().getSession(false); AuthenticationInfo authinfo = new SimpleAuthenticationInfo( new ShiroUser(user), user.getPassword(), getName()); Cache<Object, Object> cache = shiroCacheManager.getCache(Constants.SSO_CACHE); cache.put(Constants.SSO_CACHE + "-" + userName,session.getId()); return authinfo; } // 认证没有通过 return null; } /** * 设定Password校验的Hash算法与迭代次数. */ @PostConstruct public void initCredentialsMatcher() { HashedCredentialsMatcher matcher = new HashedCredentialsMatcher( HASH_ALGORITHM); matcher.setHashIterations(HASH_INTERATIONS); setCredentialsMatcher(matcher); } }
这段代码只是本机的实现,对于分布式应用来说,这个应该将upmService改成远程调用的形式。
6、各个系统如何集成
(1)web.xml注册ssoFilter
(2)applicationContext里头注册ssoFilter实现
(3)注入upmService(远程调用形式)
问题:如果是采用原来的shiroFilter这样的话,对于第一二步来说,每个应用都得配置redis和securityManager,这样对系统入侵太大,不够轻量,但是可以充分利用shiro提供的服务。
解决:对于各个系统来说,需要一个ssoFilter,对每个url进行拦截,若需要登录,则取cookie中的sessionId,远程访问shiro/sso server,判断session是否存在,如果存在,则返回继续下一步的鉴权判断,若不存在,则跳转到登录页面。因此,ssoFilter采用正常的servlet filter即可,若需要组合authFilter,则还是采取DelegatingFilterProxy的形式。
(或者看是否可以改造shiroFilter,不注入cacheManager,看是否有问题)
缺点:这样使用的话,其实对shiro的变向实现(对upm的集成进行解耦),可以借鉴shiro部分思路,实现自己的sso/upm server。