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。