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里面的一些列管理器,过滤器,再配置进指定的管理器中就可以。