Shiro权限管理实现(详解)

前言
Apache Shiro 是 Java 的一个安全框架。功能强大,使用简单的Java安全框架,它为开发人员提供一个直观而全面的认证,授权,加密及会话管理的解决方案。

功能介绍
资源-角色-权限
登录认证,密码加密(Authentication, Authorization, Cryptography)
用户角色和权限放入缓存(Caching)
会话管理(Session Management)

功能实现
实现说明

基于Spring开发Shiro的话,我们只需要实现ShiroFilterFactoryBean即可。


ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
filterFactoryBean.setLoginUrl("/login");// 未登录时候跳转URL
filterFactoryBean.setSuccessUrl("/index");// 成功后欢迎页面
filterFactoryBean.setUnauthorizedUrl("/unAuthorized");// 未认证页面

filterFactoryBean.setSecurityManager(securityManager);
filterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

上面我们看到:

SecurityManager是登录认证,缓存管理和会话管理等的具体实现;filterChainDefinitionMap是资源对应的各种Filter的实现;

资源-角色-权限


Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// /user/下面的需要ROLE_USER角色
filterChainDefinitionMap.put("/user/**", "roles[ROLE_USER]");
// /admin/下面的所有需要ROLE_ADMIN的角色才能访问
filterChainDefinitionMap.put("/admin/**", "roles[ROLE_ADMIN]");
//登录注册不需要认证
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/register", "anon");
// 其他资源地址全部需要用户认证才能访问
filterChainDefinitionMap.put("/**", "authc");

上面是设置当用户请求某个api的时候,使用对应的filter进行拦截处理,从而判断是否有对应的权限;默认的filter有以下几种:

/**
 * Enum representing all of the default Shiro Filter instances available to web applications.  Each filter instance is
 * typically accessible in configuration the {@link #name() name} of the enum constant.
 *
 * @since 1.0
 */
public enum DefaultFilter {

    anon(AnonymousFilter.class),
    authc(FormAuthenticationFilter.class),
    authcBasic(BasicHttpAuthenticationFilter.class),
    logout(LogoutFilter.class),
    noSessionCreation(NoSessionCreationFilter.class),
    perms(PermissionsAuthorizationFilter.class),
    port(PortFilter.class),
    rest(HttpMethodPermissionFilter.class),
    roles(RolesAuthorizationFilter.class),
    ssl(SslFilter.class),
    user(UserFilter.class);

登录认证,密码加密

因为用户信息和对应的角色权限信息,都是由应用方提供,所以Shiro抽象了一个接口,由应用方去实现这个接口(AuthorizingRealm),有两个方法需要应用方自己去实现:

//用户登录认证
protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;

//用户权限认证
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);

以下是自己抽象了一个类:

@Slf4j
public abstract class AbstractAuthorizingRealm extends AuthorizingRealm {

    public AbstractAuthorizingRealm() {
        HashedCredentialsMatcher credentialsMatcher = new RetryLimitHashedCredentialsMatcher(EhCacheManagerFactory.getCacheManager());
        credentialsMatcher.setHashAlgorithmName(ShiroConstant.hashAlgorithmName);
        credentialsMatcher.setHashIterations(ShiroConstant.hashIterations);//加密次数
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        setCredentialsMatcher(credentialsMatcher);
    }

    /**
     * 获取当前用户的角色和权限
     */
    public abstract RoleAndPermissions getRoleAndPermissionsFromUsername(String username);

    /**
     * 获取认证信息
     * @param username 用户名
     */
    public abstract UserAuthInfo getAuthInfoFromUsername(String username);

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("##################执行Shiro权限认证(默认)##################");
        // 获取用户名
        String loginName = (String) principals.fromRealm(getName()).iterator().next();
        // 判断用户名是否存在
        if (loginName == null || loginName.length() == 0) {
            return null;
        }
        // 创建一个授权对象
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //角色和权限设置
        RoleAndPermissions rps = getRoleAndPermissionsFromUsername(loginName);
        if (rps.getPermissions() != null) {
            info.addStringPermissions(rps.getPermissions());
        }
        if (rps.getRoles() != null) {
            info.addRoles(rps.getRoles());
        }

        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        log.info("##################执行Shiro登陆认证(默认)##################");
        UsernamePasswordToken authenticationToken = (UsernamePasswordToken) token;
        // 用户名
        String username = authenticationToken.getUsername();
        if (username != null && !"".equals(username)) {
            UserAuthInfo userAuthInfo =  getAuthInfoFromUsername(username);
            if (userAuthInfo != null) {
                Object principal = token.getPrincipal();
                // shiro的用户认证对象
                return new SimpleAuthenticationInfo(principal, userAuthInfo.getPassword(), ByteSource.Util.bytes(userAuthInfo.getSalt()), getName());
            }
        }
        return null;
    }
}

业务方只需要继承AbstractAuthorizingRealm,并且实现获取用户信息和角色权限的方法即可。而且还对密码进行了加密处理:

public class RetryLimitHashedCredentialsMatcher  extends HashedCredentialsMatcher {

    // 这里使用的是Ehcache对密码重试次数进行缓存
    private Cache<String, AtomicInteger> passwordRetryCache;

    public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) {
        passwordRetryCache = cacheManager.getCache("passwordRetryCache");
    }

    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        String username = (String) token.getPrincipal();
        AtomicInteger retryCount = passwordRetryCache.get(username);
        if (retryCount == null) {
            retryCount = new AtomicInteger(0);
            passwordRetryCache.put(username, retryCount);
        }
        // 当用户连续输入密码错误5次以上禁止用户登录一段时间
        if (retryCount.incrementAndGet() > 5) {
            throw new ExcessiveAttemptsException();
        }
        boolean match = super.doCredentialsMatch(token, info);
        if (match) {
            passwordRetryCache.remove(username);
        }
        return match;
    }
}

用户角色和权限放入缓存

用户登录成功后,没访问一个资源的时候不可能都去查一次角色和权限信息,可以将这些信息放入缓存中。

//缓存可以有多种实现,可以是EhCacheManager或RedisCacheManager
securityManager.setCacheManager(cm);

会话管理

如果使用DefaultWebSecurityManager, 默认会有一个SessionManager

public DefaultWebSecurityManager() {
    super();
    ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator());
    this.sessionMode = HTTP_SESSION_MODE;
    setSubjectFactory(new DefaultWebSubjectFactory());
    setRememberMeManager(new CookieRememberMeManager());
    setSessionManager(new ServletContainerSessionManager());
}

默认是使用ServletContainerSessionManager和我们平时的Session管理是一致的,客户端通过JSESSIONID和后台的session进行匹配,如果后台服务关闭或挂掉,session信息就丢失了,我们可以自己实现SessionManager,自己去管理Session的创建,销毁等。

@Slf4j
public class ShiroRedisSessionDao extends AbstractSessionDAO {

    private RedisCache<String, Object> cache;

    public ShiroRedisSessionDao(RedisCacheManager redisCacheManager) {
        this.cache = (RedisCache)redisCacheManager.getCache("session-cache-manager");
    }

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = this.generateSessionId(session);
        this.assignSessionId(session, sessionId);
        this.saveSession(session);
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        if(sessionId == null){
            log.error("session id is null");
            return null;
        }

        return (Session)cache.get(sessionId.toString());
    }

    @Override
    public void update(Session session) throws UnknownSessionException {
        this.saveSession(session);
    }

    @Override
    public void delete(Session session) {
        if (session == null || session.getId() == null) {
            log.error("session or session id is null");
            return;
        }
        cache.remove(session.getId().toString());
    }

    @Override
    public Collection<Session> getActiveSessions() {
        Set<Session> sessions = new HashSet<>();

        /** 这里最好别用keys,可能会阻塞
        Set<String> keys = cache.keys();
        if(keys != null && keys.size()>0){
            for(String key : keys){
                Session s = (Session)cache.get(key);
                sessions.add(s);
            }
        }
        **/
        return sessions;
    }

    private void saveSession(Session session) throws UnknownSessionException{
        if (session == null || session.getId() == null) {
            log.error("session or session id is null");
            return;
        }
//        //设置过期时间
//        long expireTime = 1800000l;
//        session.setTimeout(expireTime);
        cache.put(session.getId().toString(), session);
    }
}

上面是继承了AbstractSessionDAO, 使用Redis对Session进行缓存,这样即使后台服务重启了,Session依然可以匹配。

封装

把上面的各部分实现封装到一起就构成了shiro权限管理的核心,业务方只需要实现简单的接口即可。

public abstract class AbstractShiroConfig {

    /**
     * 此方法由业务方重写
     * @return 具体的Realm
     */
    public abstract AbstractAuthorizingRealm getRealm();

    /**
     * 动态获取角色-资源信息
     * @return filterChainDefinitions
     */
    public abstract Map<String, String> loadFilterChainDefinitions() throws Exception;

    /**
     * 创建ShiroFilterFactoryBean
     *      默认使用Ehcache缓存
     * @return
     */
    public ShiroFilterFactoryBean createShiroFilterBean() {
        return createShiroFilterBean(defaultWebSecurityManager(CacheType.EHCACHE), defaultFilterChainMap());
    }

    public ShiroFilterFactoryBean createShiroFilterBean(DefaultWebSecurityManager securityManager, Map<String, String> filterChainDefinitionMap) {
        return ShiroFilterFactory.create(securityManager, filterChainDefinitionMap, this);
    }

    public ShiroFilterFactoryBean createShiroFilterBean(Map<String, String> filterChainDefinitionMap) {
        return ShiroFilterFactory.create(defaultWebSecurityManager(CacheType.EHCACHE), filterChainDefinitionMap, this);
    }

    //    public ShiroFilterFactoryBean createShiroFilterBean(CacheType cacheType) {
//        return createShiroFilterBean(defaultWebSecurityManager(cacheType), defaultFilterChainMap());
//    }
//
//    public ShiroFilterFactoryBean createShiroFilterBean(DefaultWebSecurityManager securityManager) {
//        return ShiroFilterFactory.create(securityManager, defaultFilterChainMap());
//    }
//

//
//    public ShiroFilterFactoryBean createShiroFilterBean(CacheType cacheType, Map<String, String> filterChainDefinitionMap) {
//        return ShiroFilterFactory.create(defaultWebSecurityManager(cacheType), filterChainDefinitionMap);
//    }
//
//    public ShiroFilterFactoryBean createShiroFilterBean(CacheManager cacheManager) {
//        return ShiroFilterFactory.create(defaultWebSecurityManager(cacheManager), defaultFilterChainMap());
//    }
//
//    public ShiroFilterFactoryBean createShiroFilterBean(CacheManager cacheManager, Map<String, String> filterChainDefinitionMap) {
//        return ShiroFilterFactory.create(defaultWebSecurityManager(cacheManager), filterChainDefinitionMap);
//    }

    /***
     * 默认的安全管理配置
     */
    public DefaultWebSecurityManager defaultWebSecurityManager(CacheType cacheType) {
       return defaultWebSecurityManager(cacheType, null);
    }

    public DefaultWebSecurityManager defaultWebSecurityManager(CacheManager cacheManager) {
        return defaultWebSecurityManager(null, cacheManager);
    }

    public DefaultWebSecurityManager defaultWebSecurityManager(CacheType cacheType, CacheManager cacheManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //设置realm
        securityManager.setRealm(getRealm());
        // 配置securityManager
        SecurityUtils.setSecurityManager(securityManager);
        // 根据情况选择缓存器
        CacheManager cm = cacheManager == null ? (cacheType == null ? defaultShiroCacheManager() : getCacheManager(cacheType)) : cacheManager;
        securityManager.setCacheManager(cm);

        //设置session manager
        securityManager.setSessionManager(new ShiroSessionManager());
        return securityManager;
    }

    /**
     * shiro缓存:ehcache缓存 (用户认证信息和权限信息等)
     */
    public CacheManager defaultShiroCacheManager() {
        return EhCacheManagerFactory.getCacheManager();
    }

    /**
     * redis缓存:redis缓存 (用户认证信息和权限信息等)
     */
    public CacheManager defaultShiroRedisCacheManager() {
        return new RedisCacheManager(new RedisManager(), "auth");
    }

    private Map<String, String> defaultFilterChainMap() {
        // 配置拦截地址和拦截器, 使用LinkedHashMap,因为拦截有先后顺序
        // authc:所有url都必须认证通过才可以访问;
        // anon:所有url都都可以匿名访问
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        // 以下配置同样可以通过注解@RequiresPermissions("user:edit")来配置访问权限和角色注解@RequiresRoles(value={"ROLE_USER"})方式定义
//        filterChainDefinitionMap.put("/user/**", "roles[ROLE_USER]");// /user/下面的需要ROLE_USER角色或者query权限才能访问
//        filterChainDefinitionMap.put("/admin/**", "roles[ROLE_ADMIN]");// /admin/下面的所有需要ROLE_ADMIN的角色才能访问
        //登录注册不需要认证
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/register", "anon");
        // 其他资源地址全部需要用户认证才能访问
        filterChainDefinitionMap.put("/**", "authc");

        return filterChainDefinitionMap;
    }

    /**
     * 根据缓存类型选择对应的缓存管理器
     * @param cacheType 缓存类型
     * @return
     */
    private CacheManager getCacheManager(CacheType cacheType) {
        CacheManager cacheManager = defaultShiroCacheManager();
        if (cacheType == CacheType.REDIS) {
            cacheManager = defaultShiroRedisCacheManager();
        }
        return cacheManager;
    }
}

业务方只需要继承AbstractShiroConfig即可


@Configuration
public class ShiroConfig extends AbstractShiroConfig {

    @Bean
    public ShiroFilterFactoryBean filterFactoryBean() throws Exception {
        return createShiroFilterBean();
    }


    @Override
    public AbstractAuthorizingRealm getRealm() {
        return new AbstractAuthorizingRealm() {
            @Override
            public RoleAndPermissions getRoleAndPermissionsFromUsername(String s) {
                User user = null;
                try {
                    user = userService.findByUsername(s);
                    if (user != null) {
                        List<String> roles = new ArrayList<>();
                        List<String> permissions = new ArrayList<>();
                        if (user.getRoleIds()!=null) {
                            user.getRoleIds().forEach(roleId -> {
                                try {
                                    Role role = roleService.findByRoleId(roleId);
                                    if (role != null) {
                                        roles.add(role.getRoleName());

                                        if (role.getResIds() != null) {
                                            role.getResIds().forEach(resId -> {
                                                try {
                                                    permissions.add(resMapper.findById(resId).getUrl());
                                                } catch (Exception e) {
                                                    e.printStackTrace();
                                                }
                                            });
                                        }


                                    }

                                } catch (Exception e) {
                                    e.printStackTrace();
                                }
                            });
                        }
                        return new RoleAndPermissions(roles, permissions);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }

                return null;
            }

            @Override
            public UserAuthInfo getAuthInfoFromUsername(String s) {
                try {
                    User user = userService.findByUsername(s);
                    if (user != null) {
                        return new UserAuthInfo(user.getLoginname(), user.getPassword(), user.getSalt());
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return null;
            }

        };
    }

}

createShiroFilterBean的时候可以自己传securityManager和filterChainDefinitionMap,服务启动后即可实现权限的管理了。

动态权限
上面自己对shiro进行了封装,业务方只需要实现几个简单的方法即可享受到Shiro权限的管理,但是我们可以看到上面的权限资源等是初始化的时候加载了一次,后面如果更新了就不起作用了,那怎么实现动态的权限呢,看代码:

public void updatePermission() throws Exception {

    Map<String, String> filterChainMap = null;
    try {
        filterChainMap = shiroConfig.loadFilterChainDefinitions();
    } catch (Exception e) {
        log.error("loadFilterChainDefinitions error:", e);
    }

    if (filterChainMap != null && filterChainMap.size() > 0) {
        synchronized (filterFactoryBean) {

            AbstractShiroFilter shiroFilter = (AbstractShiroFilter) filterFactoryBean.getObject();
            PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter
                    .getFilterChainResolver();
            DefaultFilterChainManager manager = (DefaultFilterChainManager) filterChainResolver
                    .getFilterChainManager();

            // 清空老的权限控制
            manager.getFilterChains().clear();
            filterFactoryBean.getFilterChainDefinitionMap().clear();
            for (Map.Entry<String, Filter> filterEntry : manager.getFilters().entrySet()) {
                if (("roles".equals(filterEntry.getKey()) || "perms".equals(filterEntry.getKey())) && PathMatchingFilter.class.isInstance(filterEntry.getValue())) {
                    PathMatchingFilter filter = PathMatchingFilter.class.cast(filterEntry.getValue());
                    Field f = ReflectionUtils.findField(PathMatchingFilter.class, "appliedPaths");
                    f.setAccessible(true);
                    Map<String, Object> appliedPaths = (Map<String, Object>) ReflectionUtils.getField(f, filter);
                    appliedPaths.clear();
                }
            }

            //创建新的权限
            filterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
            Map<String, String> chains = filterFactoryBean
                    .getFilterChainDefinitionMap();
            for (Map.Entry<String, String> entry : chains.entrySet()) {
                String url = entry.getKey();
                String chainDefinition = entry.getValue().trim()
                        .replace(" ", "");
                manager.createChain(url, chainDefinition);
            }
        }
    }
}
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值