【转】基于shiro和redis的sso及鉴权服务

nullcy 2017-05-19

转自  http://www.codeweblog.com/基于shiro和redis的sso及鉴权服务

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

 

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。

 
 

分类:Distributed 时间:2015-02-17 人气:3740

相关推荐