企业中使用的主流的权限框架主要有 Apache Shiro或者Spring Security,两者有哪些区别呢?

  • Apache Shiro比Spring Security , 前者使用更简单

  • Shiro 功能强大、 简单、灵活, 不跟任何的框架或者容器绑定,可以独立运行

  • Spring Security 对Spring 体系支持比较好,脱离Spring体系则很难开发

  • SpringSecutiry 支持Oauth鉴权 Spring Security OAuth,Shiro需要自己实现

不过企业中使用shiro的会比较多,因为上手会比较容易

一、Shiro的简单介绍

Apache Shiro官网 Apache Shiro | Simple. Java. Security.

Apache Shiro的四大核心模块为:身份认证,授权,会话管理和加密

  • 什么是身份认证

    • Authentication,身份证认证,一般就是登录
  • 什么是授权

    • Authorization,给用户分配角色或者访问某些资源的权限
  • 什么是会话管理

    • Session Management, 用户的会话管理员,多数情况下是web session
  • 什么是加密

    • Cryptography, 数据加解密,比如密码加解密等

 Shiro中一些常用的api

  • Subject

    • 我们把用户或者程序称为主体(如用户,第三方服务,cron作业),主体去访问系统或者资源
  • SecurityManager

    • 安全管理器,Subject的认证和授权都要在安全管理器下进行
  • Authenticator

    • 认证器,主要负责Subject的认证
  • Realm

    • 数据域,Shiro和安全数据的连接器,好比jdbc连接数据库; 通过realm获取认证授权相关信息
  • Authorizer

    • 授权器,主要负责Subject的授权, 控制subject拥有的角色或者权限
  • Cryptography

    • 加解密,Shiro的包含易于使用和理解的数据加解密方法,简化了很多复杂的api
  • Cache Manager

    • 缓存管理器,比如认证或授权信息,通过缓存进行管理,提高性能

二、使用实践:Springboot2.x整合 Apache Shiro 

  • 整合Shiro相关jar包

  •  <dependency>
          <groupId>org.apache.shiro</groupId>
          <artifactId>shiro-spring</artifactId>
          <version>1.4.0</version>
      </dependency>

    在项目实际使用中,需要自定义realm

  • realm作用:Shiro 从 Realm 获取安全数据(存放在数据库中的用户及权限数据,查询只有封装到realm类中的认证对象信息和授权对象信息中)

  • 默认自带的realm:idae查看realm继承关系,有默认实现和自定义继承的realm

  • 两个概念

    • principal : 主体的标示,可以有多个,但是需要具有唯一性,常见的有用户名,手机号,邮箱等
    • credential:凭证, 一般就是密码
    • 所以一般我们说 principal + credential 就账号 + 密码
  • 开发中,往往是自定义realm , 即继承 AuthorizingRealm抽象类,需要覆写两个方法;分别是用于认证的方法和授权的方法

public class CustomRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    /**
     * 进行权限校验的时候回调用
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("授权 doGetAuthorizationInfo");
        String username = (String)principals.getPrimaryPrincipal();
        User user = userService.findAllUserInfoByUsername(username);

        List<String> stringRoleList = new ArrayList<>();
        List<String> stringPermissionList = new ArrayList<>();


        List<Role> roleList = user.getRoleList();

        for(Role role : roleList){
            stringRoleList.add(role.getName());

            List<Permission> permissionList = role.getPermissionList();

            for(Permission p: permissionList){
                if(p!=null){
                    stringPermissionList.add(p.getName());
                }
            }

        }

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addRoles(stringRoleList);
        simpleAuthorizationInfo.addStringPermissions(stringPermissionList);

        return simpleAuthorizationInfo;
    }



    /**
     * 用户登录的时候会调用
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        System.out.println("认证 doGetAuthenticationInfo");

        //从token获取用户信息,token代表用户输入,获取到用户名,或者是账号之类的
        String username = (String)token.getPrincipal();

        User user =  userService.findAllUserInfoByUsername(username);

        //取密码
        String pwd = user.getPassword();
        if(pwd == null || "".equals(pwd)){
            return null;
        }

        return new SimpleAuthenticationInfo(username, user.getPassword(), this.getClass().getName());
    }
}

或者:

public class ShiroRealm extends AuthorizingRealm{
	
	private static final Logger logger = LoggerFactory.getLogger(ShiroRealm.class);
	private static final String CURRENT_USER = "CURRENT_USER";
	
	
	@Autowired
	private IUserService userService; 
	
	//@Autowired
	//private IMemberService memberService;
	
	// 权限
    @Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        String userId = "";
        Subject currentUser = SecurityUtils.getSubject();
		if (null != currentUser) {
			try {
                Session session = currentUser.getSession();
                if (null != session) {
                	userId = (String) session.getAttribute(CURRENT_USER);
                }
            } catch (InvalidSessionException e) {
                logger.error(e.toString());
            }
		}
		long uId = 0;
		uId = StringUtils.isEmpty(userId) ? uId :Long.parseLong(userId);
        UserVO sysUser = userService.getUserRole(uId);
        info.addRoles(sysUser.getRolesName());
        Map<String,Object> map = new HashMap<String,Object>();
		map.put("ids", sysUser.getRolesId());
		List<Map<String,Object>> list = userService.getRolePrivilege(map);
		Set<String> set = new HashSet<String>();
		for(Map<String,Object> lt :list){
			set.add(String.valueOf(lt.get("privilegeName")));
		}
		info.addStringPermissions(set);
		info.addStringPermission("user");
        return info;
    }

    // 登录验证
    @Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)
        throws AuthenticationException {
		UsernamePasswordToken token=(UsernamePasswordToken) authcToken;
		logger.info("验证当前Subject时获取到token为:" + ReflectionToStringBuilder.toString(token, ToStringStyle.MULTI_LINE_STYLE)); 
		String loginName = token.getUsername();
		long userId = 0; 
		userId = StringUtils.isEmpty(loginName) ? userId :Long.parseLong(loginName);
		StringBuilder sb = new StringBuilder(100);
		for (int i = 0; i < token.getPassword().length; i++) {
			sb.append(token.getPassword()[i]);
		}
		try {
				UserVO user = userService.getUserRole(userId);
				if(user != null){
					if (user.getPassword().equals(sb.toString())) {
						Subject currentUser = SecurityUtils.getSubject();
						logger.info("当前Subject为:"+currentUser);
						if (null != currentUser) {
							Session session = currentUser.getSession();
							logger.info("当前session为:"+session);
							if (null != session) {
								session.setAttribute(CURRENT_USER, userId);
								logger.info("登录验证成功:"+userId);
							}
						}
						//saveSession(user.getUserId());
						AuthenticationInfo authcInfo = new SimpleAuthenticationInfo(user.getUserId(), user.getPassword(),
								user.getUserName());
						return authcInfo;
					}
				}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
    }

以及shiro的其他相关配置,可以在配置类中进行配置:

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {


    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){

        System.out.println("执行 ShiroFilterFactoryBean.shiroFilter()");

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //必须设置securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);


        //需要登录的接口,如果访问某个接口,需要登录却没登录,则调用此接口(如果不是前后端分离,则跳转页面)
        shiroFilterFactoryBean.setLoginUrl("/pub/need_login");

        //登录成功,跳转url,如果前后端分离,则没这个调用
        shiroFilterFactoryBean.setSuccessUrl("/");

        //没有权限,未授权就会调用此方法, 先验证登录-》再验证是否有权限
        shiroFilterFactoryBean.setUnauthorizedUrl("/pub/not_permit");


        //设置自定义filter
        Map<String,Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("roleOrFilter",new CustomRolesOrAuthorizationFilter());
        shiroFilterFactoryBean.setFilters(filterMap);


        //拦截器路径,坑一,部分路径无法进行拦截,时有时无;因为同学使用的是hashmap, 无序的,应该改为LinkedHashMap
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        //退出过滤器
        filterChainDefinitionMap.put("/logout","logout");

        //匿名可以访问,也是就游客模式
        filterChainDefinitionMap.put("/pub/**","anon");

        //登录用户才可以访问
        filterChainDefinitionMap.put("/authc/**","authc");

        //管理员角色才可以访问
        filterChainDefinitionMap.put("/admin/**","roleOrFilter[admin,root]");

        //有编辑权限才可以访问
        filterChainDefinitionMap.put("/video/update","perms[video_update]");


        //坑二: 过滤链是顺序执行,从上而下,一般讲/** 放到最下面

        //authc : url定义必须通过认证才可以访问
        //anon  : url可以匿名访问
        filterChainDefinitionMap.put("/**", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }


    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        //如果不是前后端分离,则不必设置下面的sessionManager
        securityManager.setSessionManager(sessionManager());

        //使用自定义的cacheManager
        securityManager.setCacheManager(cacheManager());

        //设置realm(推荐放到最后,不然某些情况会不生效)
        securityManager.setRealm(customRealm());



        return securityManager;
    }


    /**
     * 自定义realm
     * @return
     */
    @Bean
    public CustomRealm customRealm(){
        CustomRealm customRealm = new CustomRealm();

        customRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return customRealm;
    }


    /**
     * 密码加解密规则
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();

        //设置散列算法:这里使用的MD5算法
        credentialsMatcher.setHashAlgorithmName("md5");

        //散列次数,好比散列2次,相当于md5(md5(xxxx))
        credentialsMatcher.setHashIterations(2);

        return credentialsMatcher;
    }



    //自定义sessionManager
    @Bean
    public SessionManager sessionManager(){

        CustomSessionManager customSessionManager = new CustomSessionManager();

        //超时时间,默认 30分钟,会话超时;方法里面的单位是毫秒
        //customSessionManager.setGlobalSessionTimeout(20000);

        //配置session持久化
        customSessionManager.setSessionDAO(redisSessionDAO());

        return customSessionManager;
    }






    /**
     * 配置redisManager
     *
     */
    public RedisManager getRedisManager(){
        RedisManager redisManager = new RedisManager();
        redisManager.setHost("localhost");
        redisManager.setPort(6379);
        return redisManager;
    }


    /**
     * 配置具体cache实现类
     * @return
     */
    public RedisCacheManager cacheManager(){
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(getRedisManager());

        //设置过期时间,单位是秒,20s
        redisCacheManager.setExpire(20);

        return redisCacheManager;
    }


    /**
     * 自定义session持久化
     * @return
     */
    public RedisSessionDAO redisSessionDAO(){
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(getRedisManager());

        //设置sessionid生成器
        redisSessionDAO.setSessionIdGenerator(new CustomSessionIdGenerator());

        return redisSessionDAO;
    }


    /**
     * 管理shiro一些bean的生命周期 即bean初始化 与销毁
     * @return
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }


    /**
     *  api controller 层面
     *  加入注解的使用,不加入这个AOP,则shiro的注解不生效(shiro的注解 例如 @RequiresGuest,@RequiresRoles,@RequiresPermissions等)
     *
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }


    /**
     *  用来扫描上下文寻找所有的Advistor(通知器),
     *  将符合条件的Advisor应用到切入点的Bean中,需要在LifecycleBeanPostProcessor创建后才可以创建
     * @return
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setUsePrefix(true);
        return defaultAdvisorAutoProxyCreator;
    }



}

shiro整合redis做缓存时,需要加上相关依赖

<!-- shiro+redis缓存插件 -->
		<dependency>
			<groupId>org.crazycake</groupId>
			<artifactId>shiro-redis</artifactId>
			<version>3.1.0</version>
		</dependency>

或者也可以使用ehcache做本地缓存,则加入ehcache相关依赖:

<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-ehcache</artifactId>
			<version>1.4.0</version>
		</dependency>

因为现在基本上都是web开发,所以依赖要加上:

<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-core</artifactId>
			<version>1.4.0</version>
		</dependency>
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-web</artifactId>
			<version>1.4.0</version>
		</dependency>

在项目resources目录下添加一个shiro配置xml文件(ehcache-shiro.xml)

<?xml version="1.0" encoding="UTF-8"?>
<!-- 
	name:缓存名称。 maxElementsInMemory:缓存最大个数。 eternal:对象是否永久有效,一但设置了,timeout将不起作用。 
	timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。 
	timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。 
	overflowToDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。 diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。 
	maxElementsOnDisk:硬盘最大缓存个数。 diskPersistent:是否缓存虚拟机重启期数据 Whether the disk 
	store persists between restarts of the Virtual Machine. The default value 
	is false. diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。 memoryStoreEvictionPolicy:
	当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。 
	clearOnFlush:内存数量最大时是否清除
 -->
<ehcache xmlns:xsi="http://www.w3/2001/XMLSchema-instance"
	xsi:noNamespaceSchemaLocation="http://ehcache/ehcache.xsd"
	updateCheck="false" name="shiroCache">
	<diskStore path="java.io.tmpdir/jeecms/shiro" />
	<defaultCache maxElementsInMemory="1000" eternal="false"
		timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true"
		diskSpoolBufferSizeMB="30" maxElementsOnDisk="1000"
		diskPersistent="false" diskExpiryThreadIntervalSeconds="120" />
</ehcache>

在shiro配置类中,配置缓存bean方法时,替换为:


	@Bean(name = "shiroEhcacheManager")
	public EhCacheManager getEhCacheManager() {
		EhCacheManager em = new EhCacheManager();
		em.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
		return em;
	}

更详细的用法可以参考官网中与spring框架整合的案例:Apache Shiro | Simple. Java. Security.

至于shiro配置类中配置的密码加解密规则,配置了密码解密规则,算法,两次md5,那么在散列用户密码的时候,用法:

   String hashName = "md5";

        String pwd = "123";

        SimpleHash simpleHash = new SimpleHash(hashName, pwd, null, 2);
//        原始密码散列之后的值:d022646351048ac0ba397d12dfafa304
        String result = simpleHash.toString();

在用户表(user表)中插入散列之后的密码 

还有一个自定义的filter

import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.CollectionUtils;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.util.Set;

/**
 * 自定义filter
 */
public class CustomRolesOrAuthorizationFilter extends AuthorizationFilter {


    @Override
    @SuppressWarnings({"unchecked"})
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {

        Subject subject = getSubject(request, response);

        //获取当前访问路径所需要的角色集合
        String[] rolesArray = (String[]) mappedValue;

        //没有角色限制,可以直接访问
        if (rolesArray == null || rolesArray.length == 0) {
            //no roles specified, so nothing to check - allow access.
            return true;
        }


        Set<String> roles = CollectionUtils.asSet(rolesArray);

        //当前subject是roles 中的任意一个,则有权限访问
        for(String role : roles){
            if(subject.hasRole(role)){
                return true;
            }
        }

        return false;
    }
}

也可以自定义生成sessionID

import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.SessionIdGenerator;

import java.io.Serializable;
import java.util.UUID;

/**
 * 自定义sesionid生成
 */
public class CustomSessionIdGenerator implements SessionIdGenerator {

    @Override
    public Serializable generateId(Session session) {

        return UUID.randomUUID().toString().replace("-","");

    }


}

更多推荐

shiro安全框架