基于Shiro和Redis的SSO及鉴权服务

1、参考代码

http://git.oschina.net/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。

 


你可能感兴趣的:(redis,shiro,SSO)