Apache Shiro
是Java的一个安全框架。目前,使用Apache Shiro
的人越来越多,相比Spring Security
而言相当简单,
可能没有Spring Security
做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,
所以使用小而简单的Shiro就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。
本教程只介绍基本的Shiro使用,不会过多分析源码等,重在使用。
Shiro架构 Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。
Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。这不就是我们想要的嘛,
而且Shiro的API也是非常简单;其基本功能点如下图所示:
Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web支持,可以非常容易的集成到Web环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
记住一点,Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。
接下来我们分别从外部和内部来看看Shiro的架构,对于一个好的框架,从外部来看应该具有非常简单易于使用的API,
且API契约明确;从内部来看的话,其应该有一个可扩展的架构,即非常容易插入用户自定义实现,因为任何框架都不能满足所有需求。
可以看到:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject。
Subject:主体,代表了当前”用户”
,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;
Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
也就是说对于我们而言,最简单的一个Shiro应用:
应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;
我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。
从以上也可以看出,Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入。
接下来我们来从Shiro内部来看下Shiro的架构,如下图所示:
Subject:主体,可以看到主体可以是任何可以与应用交互的”用户”;
SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。
Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication
Strategy),即什么情况下算用户认证通过了;
Authorizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;
SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到Memcached服务器);
SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached
SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;
CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。
参考 Shiro官网
SpringBoot集成 实现一个最长见的验证码登录、记住我、权限自定义(or),缓存功能的SpringBoot应用,模板使用Thymeleaf 3
Maven依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-thymeleaf</artifactId > </dependency > <dependency > <groupId > com.github.theborakompanioni</groupId > <artifactId > thymeleaf-extras-shiro</artifactId > <version > 2.0.0</version > </dependency > <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-spring</artifactId > <version > 1.4.0</version > <exclusions > <exclusion > <artifactId > slf4j-api</artifactId > <groupId > org.slf4j</groupId > </exclusion > </exclusions > </dependency > <dependency > <groupId > org.apache.shiro</groupId > <artifactId > shiro-ehcache</artifactId > <version > 1.4.0</version > <exclusions > <exclusion > <artifactId > slf4j-api</artifactId > <groupId > org.slf4j</groupId > </exclusion > </exclusions > </dependency > <dependency > <groupId > com.github.axet</groupId > <artifactId > kaptcha</artifactId > <version > 0.0.9</version > </dependency >
新建一个配置类ShiroConfig.java
,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 @Configuration @Order(1) public class ShiroConfig { @Bean public ServletRegistrationBean kaptchaServlet () { ServletRegistrationBean servlet = new ServletRegistrationBean (new KaptchaServlet (), "/kaptcha.jpg" ); servlet.addInitParameter(Constants.KAPTCHA_SESSION_CONFIG_KEY, Constants.KAPTCHA_SESSION_KEY); servlet.addInitParameter(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, "50" ); servlet.addInitParameter(Constants.KAPTCHA_BORDER, "no" ); servlet.addInitParameter(Constants.KAPTCHA_BORDER_COLOR, "105,179,90" ); servlet.addInitParameter(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, "45" ); servlet.addInitParameter(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4" ); servlet.addInitParameter(Constants.KAPTCHA_TEXTPRODUCER_FONT_NAMES, "宋体,楷体,微软雅黑" ); servlet.addInitParameter(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue" ); servlet.addInitParameter(Constants.KAPTCHA_IMAGE_WIDTH, "125" ); servlet.addInitParameter(Constants.KAPTCHA_IMAGE_HEIGHT, "60" ); return servlet; } @Bean public MyExceptionResolver myExceptionResolver () { return new MyExceptionResolver (); } @Bean public ShiroFilterFactoryBean shirFilter (SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean (); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, Filter> filtersMap = shiroFilterFactoryBean.getFilters(); KaptchaFilter kaptchaFilter = new KaptchaFilter (); filtersMap.put("kaptchaFilter" , kaptchaFilter); shiroFilterFactoryBean.setFilters(filtersMap); Map<String, String> filterChainDefinitionMap = new LinkedHashMap <String, String>(); filterChainDefinitionMap.put("/logout" , "logout" ); filterChainDefinitionMap.put("/index" , "user" ); filterChainDefinitionMap.put("/" , "user" ); filterChainDefinitionMap.put("/login" , "kaptchaFilter" ); filterChainDefinitionMap.put("/favicon.ico" , "anon" ); filterChainDefinitionMap.put("/static/**" , "anon" ); filterChainDefinitionMap.put("/kaptcha.jpg" , "anon" ); filterChainDefinitionMap.put("/api/v1/**" , "anon" ); filterChainDefinitionMap.put("/v2/api-docs" , "anon" ); filterChainDefinitionMap.put("/webjars/**" , "anon" ); filterChainDefinitionMap.put("/swagger-resources/**" , "anon" ); filterChainDefinitionMap.put("/swagger-ui.html" , "anon" ); filterChainDefinitionMap.put("/**" , "authc" ); shiroFilterFactoryBean.setLoginUrl("/login" ); shiroFilterFactoryBean.setSuccessUrl("/index" ); shiroFilterFactoryBean.setUnauthorizedUrl("/errorView/403_error.html" ); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public SecurityManager securityManager () { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager (); securityManager.setRealm(myShiroRealm()); securityManager.setCacheManager(ehCacheManager()); securityManager.setRememberMeManager(rememberMeManager()); return securityManager; } @Bean public MyShiroRealm myShiroRealm () { MyShiroRealm myShiroRealm = new MyShiroRealm (); myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myShiroRealm; } @Bean public HashedCredentialsMatcher hashedCredentialsMatcher () { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher (); hashedCredentialsMatcher.setHashAlgorithmName("md5" ); hashedCredentialsMatcher.setHashIterations(2 ); hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true ); return hashedCredentialsMatcher; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor (SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor (); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } @Bean public EhCacheManager ehCacheManager () { EhCacheManager cacheManager = new EhCacheManager (); cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml" ); return cacheManager; } @Bean public SimpleCookie rememberMeCookie () { SimpleCookie simpleCookie = new SimpleCookie ("rememberMe" ); simpleCookie.setMaxAge(259200 ); return simpleCookie; } @Bean public CookieRememberMeManager rememberMeManager () { CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager (); cookieRememberMeManager.setCookie(rememberMeCookie()); return cookieRememberMeManager; } @Bean(name = "shiroDialect") public ShiroDialect shiroDialect () { return new ShiroDialect (); } }
代码中解释都非常清楚。
MyShiroRealm.java
的内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 public class MyShiroRealm extends AuthorizingRealm { private static final Logger _logger = LoggerFactory.getLogger(MyShiroRealm.class); @Autowired ManagerInfoService managerInfoService; @Override protected AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken token) throws AuthenticationException { _logger.info("MyShiroRealm.doGetAuthenticationInfo()" ); String username = (String) token.getPrincipal(); ManagerInfo managerInfo = managerInfoService.findByUsername(username); if (managerInfo == null ) { return null ; } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo ( managerInfo, managerInfo.getPassword(), ByteSource.Util.bytes(managerInfo.getCredentialsSalt()), getName() ); return authenticationInfo; } @Override protected AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principals) { _logger.info("权限配置-->MyShiroRealm.doGetAuthorizationInfo()" ); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo (); ManagerInfo managerInfo = (ManagerInfo) principals.getPrimaryPrincipal(); for (SysRole role : managerInfo.getRoles()) { authorizationInfo.addRole(role.getRole()); for (Permission p : role.getPermissions()) { authorizationInfo.addStringPermission(p.getPermission()); } } return authorizationInfo; } @Override public void setCredentialsMatcher (CredentialsMatcher credentialsMatcher) { HashedCredentialsMatcher md5CredentialsMatcher = new HashedCredentialsMatcher (); md5CredentialsMatcher.setHashAlgorithmName(ShiroKit.HASH_ALGORITHM_NAME); md5CredentialsMatcher.setHashIterations(ShiroKit.HASH_ITERATIONS); super .setCredentialsMatcher(md5CredentialsMatcher); } }
自定义异常处理类MyExceptionResolver.java
:
1 2 3 4 5 6 7 8 9 10 public class MyExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { if (ex instanceof UnauthorizedException) { return new ModelAndView ("error/shiro_403" ); } return null ; } }
接口控制 配置好了Shiro后就可以通过注解方式来限制某些接口调用需要相应的角色或权限了:
1 2 3 4 5 @RequestMapping(value = "/index") @RequiresRoles("admin") public String index (HttpServletRequest request, Model model) { _logger.info("进入项目管理首页..." ); }
其他的注解请参考官网的 Shiro注解
Thymeleaf的shiro标签 可以在Thymeleaf模板中使用shiro的权限标签来控制某些菜单或按钮是否显示。
maven中添加依赖,这个前面已经有了:
1 2 3 4 5 6 <dependency > <groupId > com.github.theborakompanioni</groupId > <artifactId > thymeleaf-extras-shiro</artifactId > <version > 2.0.0</version > </dependency >
在ShiroConfig中添加一个Bean配置:
1 2 3 4 @Bean(name = "shiroDialect") public ShiroDialect shiroDialect () { return new ShiroDialect (); }
在html页面添加如下内容:
1 2 3 4 <html xmlns ="http://www.w3.org/1999/xhtml" xmlns:th ="http://www.thymeleaf.org" xmlns:shiro ="http://www.pollix.at/thymeleaf/shiro" >
添加完后在html页面调用如下:
1 2 3 4 5 6 7 8 <p shiro:user ="" > Welcome back John! Not John? Click <a href ="login.html" > here</a > to login. </p > <p shiro:notAuthenticated ="" > 未身份验证(包括记住我) </p > <p shiro:guest ="" > <span style ="white-space:pre;" > </span > Please <a href ="login.html" > login5555</a > </p >
第二种:
1 2 3 4 5 6 7 8 <shiro:guest > <a > 登录</a > <a > 注册</a > </shiro:guest > <shiro:user > 欢迎 <shiro:principal property ="name" /> </shiro:user >
权限数据库设计 一般来讲都会讲用户的角色和权限保存到数据库,这里设计一种最通用的模型,
使用RBAC(Role-Based Access Control,基于角色的访问控制)模型设计用户,角色和权限间的关系。简单地说,
一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成”用户-角色-权限”的授权模型。
在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。如下图所示:
根据这个模型,设计数据库表,并插入一些测试数据,具体可参考源码中的schema.sql
,后面我将t_user
表名字改成t_manager
然后我们通过MyBatis实现ManagerInfoService
,
1 2 3 4 5 6 7 8 9 10 11 12 @Service public class ManagerInfoService { @Resource private ManagerInfoDao managerInfoDao; public ManagerInfo findByUsername (String username) { return managerInfoDao.findByUsername(username); } }
然后对应的ManagerInfoDao.xml
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <resultMap id ="ManagerInfoMap" type ="managerInfo" > <id property ="id" column ="id" /> <result property ="username" column ="username" /> <result property ="name" column ="name" /> <result property ="password" column ="password" /> <result property ="salt" column ="salt" /> <result property ="state" column ="state" /> <collection property ="roles" ofType ="sysRole" > <id property ="id" column ="role_id" /> <result property ="role" column ="role_role" /> <collection property ="permissions" ofType ="permission" > <id property ="id" column ="perm_id" /> <result property ="permission" column ="perm_permission" /> </collection > </collection > </resultMap > <select id ="findByUsername" resultMap ="ManagerInfoMap" > SELECT DISTINCT A.id AS id, A.username AS username, A.name AS name, A.password AS password, A.salt AS salt, A.state AS state, C.id AS role_id, C.role AS role_role, E.id AS perm_id, E.permission AS perm_permission FROM t_manager A LEFT JOIN t_manager_role B ON A.id=B.manager_id LEFT JOIN t_role C ON B.role_id=C.id LEFT JOIN t_role_permission D ON C.id=D.role_id LEFT JOIN t_permission E ON D.permission_Id=E.id WHERE username=#{username} LIMIT 1 </select >
ManagerInfo.java
如下:
1 2 3 4 5 6 7 8 9 public class ManagerInfo extends Manager implements Serializable { private static final long serialVersionUID = 1L ; private List<SysRole> roles; }
最后来几张效果图:
admin角色登录之后:
参考文章
GitHub源码 springboot-shiro