spring boot 整合 shiro

2018-06-08 来源: 吴振照 发布在  https://www.cnblogs.com/wuzhenzhao/p/9155576.html

shrio官网:https://shiro.apache.org/

Apache Shiro是一个功能强大且易于使用的Java安全框架,可执行身份验证,授权,加密和会话管理。借助Shiro易于理解的API,您可以快速轻松地保护任何应用程序 - 从最小的移动应用程序到最大的Web和企业应用程序。spring中也有自带的安全框架spring security。shrio是通过对其的再封装,实现了自己的一套全新架构。

正巧spring boot项目中也需要用到用户的身份验证以及权限控制,本来想用AOP自己写一套的,但是最终还是选择了shiro,通过与前辈的共同战斗,最终还是把它实现了出来。

导入依赖

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

1.直接上配置类:下面会对配置的一些重要bean进行稍加解释

@Configuration
public class ShiroConfiguration {
    /**
     * LifecycleBeanPostProcessor,这是个DestructionAwareBeanPostProcessor的子类,
     * 负责org.apache.shiro.util.Initializable类型bean的生命周期的,初始化和销毁。
     * 主要是AuthorizingRealm类的子类,以及EhCacheManager类。
     */
    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * HashedCredentialsMatcher,这个类是为了对密码进行编码的,
     * 防止密码在数据库里明码保存,当然在登陆认证的时候,
     * 这个类也负责对form里输入的密码进行编码。
     */
    @Bean(name = "hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        credentialsMatcher.setHashAlgorithmName("MD5");
        credentialsMatcher.setHashIterations(1024);
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }

    /**ShiroRealm,这是个自定义的认证类,继承自AuthorizingRealm,
     * 负责用户的认证和权限的处理,可以参考JdbcRealm的实现。
     */
    @Bean(name = "shiroRealm")
    @DependsOn("lifecycleBeanPostProcessor")
    public PermissionsShiroRealm shiroRealm() {
    	PermissionsShiroRealm realm = new PermissionsShiroRealm();//这个类需要自己编写 下面会贴出其实现
        realm.setCredentialsMatcher(hashedCredentialsMatcher());
        return realm;
    }

    /**
     * EhCacheManager,缓存管理,用户登陆成功后,把用户信息和权限信息缓存起来,
     * 然后每次用户请求时,放入用户的session中,如果不设置这个bean,每个请求都会查询一次数据库。
     */
//    @Bean(name = "ehCacheManager")
//    @DependsOn("lifecycleBeanPostProcessor")
//    public EhCacheManager getEhCacheManager(){
//        EhCacheManager ehcacheManager = new EhCacheManager();
//        ehcacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
//        return ehcacheManager;
//    }  

    /**
     * SecurityManager,权限管理,这个类组合了登陆,登出,权限,session的处理,是个比较重要的类。
//     */
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager(PermissionsShiroRealm shiroRealm ,SessionManager sessionManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm);
//        securityManager.setCacheManager(getEhCacheManager());
        securityManager.setSessionManager(sessionManager);
        return securityManager;
    }

    /**
     * ShiroFilterFactoryBean,是个factorybean,为了生成ShiroFilter。
     * 它主要保持了三项数据,securityManager,filters,filterChainDefinitionManager。
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(org.apache.shiro.mgt.SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

//        Map<String, Filter> filters = new LinkedHashMap<>();
//        LogoutFilter logoutFilter = new LogoutFilter();
//        logoutFilter.setRedirectUrl("/api/1.0/loginout");
//        filters.put("logout",null);
//        shiroFilterFactoryBean.setFilters(filters);
        Map<String, String> filterChainDefinitionManager = new LinkedHashMap<String, String>();
        filterChainDefinitionManager.put("/api/1.0/logout", "logout");//登出URL
        filterChainDefinitionManager.put("/api/1.0/login", "anon");//登陆URL
        filterChainDefinitionManager.put("/api/1.0/nologin", "anon");//未登录跳转的URL
//      filterChainDefinitionManager.put("/user/edit/**", "authc,perms[user:edit]");// 这里为了测试,固定写死的值,也可以从数据库或其他配置中读取,此处是用权限控制
        filterChainDefinitionManager.put("/**", "user");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionManager);
        shiroFilterFactoryBean.setLoginUrl("/api/1.0/nologin");

//        shiroFilterFactoryBean.setUnauthorizedUrl("/api/1.0/unauth");
        return shiroFilterFactoryBean;
    }

    /**
     * DefaultAdvisorAutoProxyCreator,Spring的一个bean,由Advisor决定对哪些类的方法进行AOP代理。
     */
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }

    /**
     * AuthorizationAttributeSourceAdvisor,shiro里实现的Advisor类,
     * 内部使用AopAllianceAnnotationsAuthorizingMethodInterceptor来拦截用以下注解的方法。
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor aASA = new AuthorizationAttributeSourceAdvisor();
        aASA.setSecurityManager(securityManager);
        return aASA;
    }

    @Bean
    public DefaultWebSessionManager configWebSessionManager(RedisSessionDao sessionDao) {
		MySessionManager manager = new MySessionManager();
		manager.setSessionDAO(sessionDao);// 设置SessionDao
		manager.setDeleteInvalidSessions(true);// 删除过期的session
		manager.setSessionValidationSchedulerEnabled(false);// 是否定时检查session
        return manager;
    }
}

LifecycleBeanPostProcessor: 这个类 实现了DestructionAwareBeanPostProcessor接口,而DestructionAwareBeanPostProcessor接口继承了spring的 BeanPostProcessor

知道LifecycleBeanPostProcessor将Initializable和Destroyable的实现类统一在其内部自动分别调用了Initializable.init()和Destroyable.destroy()方法,从而达到管理shiro bean生命周期的目的。

public class LifecycleBeanPostProcessor implements DestructionAwareBeanPostProcessor, PriorityOrdered {

    private static final Logger log = LoggerFactory.getLogger(LifecycleBeanPostProcessor.class);

    private int order;

    public LifecycleBeanPostProcessor() {
        this(LOWEST_PRECEDENCE);
    }

    public LifecycleBeanPostProcessor(int order) {
        this.order = order;
    }

    public Object postProcessBeforeInitialization(Object object, String name) throws BeansException {
        if (object instanceof Initializable) {
            try {
                if (log.isDebugEnabled()) {
                    log.debug("Initializing bean [" + name + "]...");
                }

                ((Initializable) object).init();
            } catch (Exception e) {
                throw new FatalBeanException("Error initializing bean [" + name + "]", e);
            }
        }
        return object;
    }
    public Object postProcessAfterInitialization(Object object, String name) throws BeansException {
        // Does nothing after initialization
        return object;
    }

    public void postProcessBeforeDestruction(Object object, String name) throws BeansException {
        if (object instanceof Destroyable) {
            try {
                if (log.isDebugEnabled()) {
                    log.debug("Destroying bean [" + name + "]...");
                }

                ((Destroyable) object).destroy();
            } catch (Exception e) {
                throw new FatalBeanException("Error destroying bean [" + name + "]", e);
            }
        }
    }

    public int getOrder() {
        // LifecycleBeanPostProcessor needs Order. See https://issues.apache.org/jira/browse/SHIRO-222
        return order;
    }
}

spring的后置处理器BeanPostProcessor的作用是在spring初始化bean的前后进行一些特定操作。如果自己实现了多个后置处理器,并想按照自己的意愿顺序去执行这些处理器,那么这时候可以通过getOrder()方法去实现。order越小,执行优先级越高。

DefaultWebSecurityManager: shiro的默认安全管理器,是整个配置的核心,必不可少的。可以通过设置自定义的realm,缓存管理器,会话管理器等等。

ShiroFilterFactoryBean:核心过滤工厂类,里面可以配置需要过滤的路径,以及未登录,登陆等跳转地址。

DefaultWebSessionManager:会话管理器。可以设置自定义的sessionDao sessionManager。

配置过程中涉及了自定义的sessionDao,自定义realm,自定义的sessionManager.其中的会话管理是通过redis去实现的,下面先贴出这3个实现类的 代码。

RedisSessionDao:实现自己的sessionDao的管理需要继承AbstractSessionDAO类,实现其中对于Session的增删改查的一些基本功能,并将该sessionDao配置好:

@Component
public class RedisSessionDao extends AbstractSessionDAO {

	@Value("${session.expireTime}")
	private long expireTime ;

	@Autowired
	private StringRedisTemplate redisTemplate;
	// 创建session,保存到数据库
	@Override
	protected Serializable doCreate(Session session) throws UnknownSessionException {

		Assert.notNull(session);
		if(session.getId() == null) {
			Serializable sessionId = generateSessionId(session);
	        assignSessionId(session, sessionId);
		}
		String sessionId = session.getId().toString() ;

		//判断session是否已经存在
		Boolean exist = redisTemplate.execute(new RedisCallback<Boolean>() {
	        public Boolean doInRedis(RedisConnection connection) {
	        	Boolean result =   connection.exists(sessionId.getBytes())  ;
	            return result ;
	        }
	    }); 

		if(exist) {
			throw new DisasterSessionException(SessionErrorType.SESSION_ALREADY_EXIST, "session " + sessionId + "已经存在") ;
		}

		Boolean success = redisTemplate.execute(new RedisCallback<Boolean>() {
	        public Boolean doInRedis(RedisConnection connection) {
	        	Boolean result =  connection.setNX(sessionId.getBytes(),sessionToByte(session)) ;
	        	return result ;
	        }
	    }); 

		if(!success) {
			throw new DisasterSessionException(SessionErrorType.SESSION_CREATE_FAIL,"session " + sessionId + "创建失败") ;
		}
		//设置Session超时间间
		redisTemplate.expire(sessionId, expireTime, TimeUnit.MINUTES)  ;

		return session.getId();
	}

	// 获取session
	@Override
	protected Session doReadSession(Serializable sessionId) {

		Session session = redisTemplate.execute(new RedisCallback<Session>() {
	        public Session doInRedis(RedisConnection connection) {
	        	   byte[] bytes = connection.get(sessionId.toString().getBytes());
	        	   if( null == bytes || bytes.length == 0) {
	        		   return null ;
	        	   }
	        	   return ByteToSession(bytes) ;

	        }
	    });
		return session ;
	}

	// 更新session的最后一次访问时间
	@Override
	public void update(Session session) {

		Assert.notNull(session);
		if(session.getId() == null) {
			Serializable sessionId = generateSessionId(session);
	        assignSessionId(session, sessionId);
		}
		String sessionId = session.getId().toString() ;

		//判断session是否已经存在
		Boolean exist = redisTemplate.execute(new RedisCallback<Boolean>() {
	        public Boolean doInRedis(RedisConnection connection) {
	        	Boolean result =   connection.exists(sessionId.getBytes())  ;
	            return result ;
	        }
	    }); 

		if(!exist) {
			throw new DisasterSessionException(SessionErrorType.SESSION_NOT_EXIST, "session " + sessionId + "不存在") ;
		}

		Boolean success = redisTemplate.execute(new RedisCallback<Boolean>() {
	        public Boolean doInRedis(RedisConnection connection) {
	        	try {
	        		connection.set(sessionId.getBytes(),sessionToByte(session));
	        	}catch(Exception e) {
	        		return false ;
	        	}
	        	return true ;
	        }
	    }); 

		if(!success) {
			throw new DisasterSessionException(SessionErrorType.SESSION_UPDATE_FAIL,"session " + sessionId + "更新失败") ;
		}
		//设置Session超时间间
		redisTemplate.expire(sessionId, expireTime, TimeUnit.MINUTES)  ;
		Object principal = SecurityUtils.getSubject().getPrincipal();
		if(principal != null) {
			redisTemplate.expire(principal.toString(), expireTime, TimeUnit.MINUTES)  ;
		}

	}

	// 删除session
	@Override
	public void delete(Session session) {
		redisTemplate.delete(session.getId().toString());
	}

	@Override
	public Collection<Session> getActiveSessions() {
		return Collections.emptySet();
	}

	/**
	 * session转成字节数组流
	 * @param session
	 * @return
	 */
	private byte[] sessionToByte(Session session){
        ByteArrayOutputStream bo = new ByteArrayOutputStream();
        byte[] bytes = null;
        try {
            ObjectOutput oo = new ObjectOutputStream(bo);
            oo.writeObject(session);
            bytes = bo.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bytes;
    }  

	/**
	 * 获取redis中的流转session
	 * @param bytes
	 * @return
	 */
	private Session ByteToSession (byte[] bytes) {
        Session session = null;
        try {
            ByteArrayInputStream bi = new ByteArrayInputStream(bytes);
            ObjectInputStream oi = new ObjectInputStream(bi);

            Object o = oi.readObject();
            session = (Session)o ;
            bi.close();
            oi.close();
        } catch (Exception e) {
            System.out.println("translation" + e.getMessage());
            e.printStackTrace();
        }
        return session;
    }
}

PermissionsShiroRealm :该类是实现自己的认证(doGetAuthorizationInfo()方法)及登陆(doGetAuthenticationInfo()方法); 有了这个实现类,才能自己对登录和权限进行控制

public class PermissionsShiroRealm  extends AuthorizingRealm{

	@Autowired
	private AccountReposity accountReposity;

	@Autowired
	private StringRedisTemplate redisTemplate;

	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		// TODO Auto-generated method stub
		//1.PrincipalCollection获取登陆用户的信息
		Object principal = principals.getPrimaryPrincipal();
		Set<String> roles =new HashSet<>();
		//2.获取当前用户再缓存中的角色或权限
		 String str = redisTemplate.opsForValue().get(principal);
		 LoginUser user = JSON.parseObject(str, LoginUser.class);
		 Set<Permission> permissionsList = user.getPermissionsList();
		 for(Permission permissions:permissionsList) {
			 roles.add(permissions.getName());
		 }
		//3.创建并设置其对应属性roles  返回
		SimpleAuthorizationInfo info =new SimpleAuthorizationInfo();
		info.addStringPermissions(roles);
		return info;
	}

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

	//1.强转
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        //2.获取username
        String username=token.getUsername();
        //3.获取数据库用户信息
         Account account = accountReposity.findByLoginName(username);
        //4.若用户不存在 抛异常
        if(account == null) {
        	throw new UnknownAccountException("用户不存在");
        }
        //数据库获取的密码
        Object hashedCredentials = account.getLoginPwd();
        Object principal = username;
        String realmName = getName();
        ByteSource credentialsSalt = ByteSource.Util.bytes(username);
        SimpleAuthenticationInfo info =new SimpleAuthenticationInfo(principal, hashedCredentials, credentialsSalt, realmName);
        return info;
	}
}

MySessionManager :实现默认的session管理器DefaultWebSessionManager,复写了其中的getSessionId方法。

public class MySessionManager  extends DefaultWebSessionManager {

    private static final String AUTHORIZATION = "token";  

    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";  

    public MySessionManager() {
        super();
    }

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
    	HttpServletRequest req = (HttpServletRequest)request ;
        String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
        return id;  

    } 

}

通过以上的配置,就可以进行登陆了

Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 获取subject 登陆
subject.login(token);

权限控制可以通过shiro的注解进行对对应的角色,或者权限的控制。

shiro整合中 Subject   与 principal 是比较重要的两个名词,个人理解前者像是一系列的登陆用户组成的一个实体,就好比一个人有多种登陆账号,而后者便是实际登陆该系统的账号密码。

跟其源码可以发现 Subject  的创建时通过 org.apache.shiro.subject 接口里面的内部类Builder里的这个方法去创建,

public Subject buildSubject() {
            return this.securityManager.createSubject(this.subjectContext);
}

而实际上创建subject的是DefaultSecurityManager类,也就是我们配置的DefaultWebSecurityManager类的父类里面的

    public Subject createSubject(SubjectContext subjectContext) {
        //create a copy so we don't modify the argument's backing map:
        SubjectContext context = copy(subjectContext);

        //ensure that the context has a SecurityManager instance, and if not, add one:
        context = ensureSecurityManager(context);

        //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
        //sending to the SubjectFactory.  The SubjectFactory should not need to know how to acquire sessions as the
        //process is often environment specific - better to shield the SF from these details:
        context = resolveSession(context);

        //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
        //if possible before handing off to the SubjectFactory:
        context = resolvePrincipals(context);

        Subject subject = doCreateSubject(context);

        //save this subject for future reference if necessary:
        //(this is needed here in case rememberMe principals were resolved and they need to be stored in the
        //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
        //Added in 1.2:
        save(subject);

        return subject;
    }

通过该方法去绑定session,principl...等等需要绑定的信息。

注:

  需要满足自己的业务需求。可以通过重写shiro里面的一些列管理器,过滤器,再配置进指定的管理器中就可以。

  

相关文章