🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
### 1. 概述 本文中,我们来看看[Apache Shiro](https://shiro.apache.org/),一种通用Java安全框架。 该框架高可定制且模块化,提供authentication(身份验证),authorization(授权),cryptography(加密)和sesion management(会话管理)。 ### 2. 依赖 Apache Shiro有如此多的[modules](https://shiro.apache.org/download.html)。然而,在本教程中,我们只使用*shiro-core* artifact。 将它添加到我们的*pom.xml*: ~~~ <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.0</version> </dependency> ~~~ 最新版的 Apache Shiro modules可以再[Maven中心](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.apache.shiro%22)找到。 ### 3.配置安全管理 *SecurityManager*是Apache Shiro框架的核心部分,在运行时通常只会有一个实例。 在本教程中,我们桌面环境探讨框架。为了配置框架,我们需要在resource文件夹中创建一个*shiro.ini*文件,内容如下: ~~~ [users] user = password, admin user2 = password2, editor user3 = password3, author [roles] admin = * editor = articles:* author = articles:compose,articles:save ~~~ *shiro.ini*配置文件中的[users]部分定义了供*SecurityManager*识别的用户凭证。格式为:principal (username) = password, role1, role2, …, role。 角色与其关联的权限在[roles]中定义。*admin*角色被赋予了应用全部访问权限。使用通配符(*)标识。 *editor*角色拥有“文章”下的全部权限,然而*author*角色只能编写和保存“文章”。 *SecurityManager*用于配置*SecurityUtils*类。从*SecurityUtils*中我们可以获取当前用户与系统的交互, 并执行身份验证和授权操作。 让我们使用*IniRealm*从*shiro.ini*文件中加载我们用户和角色定义,并使用它来配置*DefaultSecurityManager*对象: ~~~ IniRealm iniRealm = new IniRealm("classpath:shiro.ini"); SecurityManager securityManager = new DefaultSecurityManager(iniRealm); SecurityUtils.setSecurityManager(securityManager); Subject currentUser = SecurityUtils.getSubject(); ~~~ 现在我们持有一个知道*shrio.ini*文件所定义的用户凭证和角色的*SecurityManager*。让我们接着进行用户认证和授权。 ### 4.身份验证 在Apache Shiro的术语中,*Subject*是一种与系统交互的任意实体。可能是一个人,一个脚本,或者一个REST客户端。 调用*SecurityUtils.getSubject()*返回一个当前*Subject*实例,即*currentUser*。 既然已有*currentUser*对象。我们可以执行身份认证基于现有凭证: ~~~ if (!currentUser.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken("user", "password"); token.setRememberMe(true); try { currentUser.login(token); } catch (UnknownAccountException uae) { log.error("Username Not Found!", uae); } catch (IncorrectCredentialsException ice) { log.error("Invalid Credentials!", ice); } catch (LockedAccountException lae) { log.error("Your Account is Locked!", lae); } catch (AuthenticationException ae) { log.error("Unexpected Error!", ae); } } ~~~ 首先,我们检查当前用户是否未曾验证。然后我们创建一个认证带有主角(用户名)和凭证(密码)的token。 接下来,我们尝试用token登录。如果提供的凭证正确,一切顺利。 不同情况对应不同的异常。也有可能抛出自定义异常来满足应用需求。可以通过继承*AccountException*类来实现。 ### 5.授权 身份认证是尝试着校验用户身份,然而授权是尝试控制系统某资源的访问。 回顾下,在我们创建*shiro.ini*文件时我们赋予了一个或多个角色给用户。而且,在roles部分,我们给每个角色定义了不同权限和访问级。 现在让我们~~荡起双桨~~看看在我们的应用中实行用户访问控制怎样使用。 在*shiro.ini*文件中,我们给了*admin*全部系统权限,*editor*拥有关于文章每个资源/操作全部权限,并且*author*被限制只能编写和保存文章的权限。 让我们基于角色欢迎当前用户: ~~~ if (currentUser.hasRole("admin")) { log.info("Welcome Admin"); } else if(currentUser.hasRole("editor")) { log.info("Welcome, Editor!"); } else if(currentUser.hasRole("author")) { log.info("Welcome, Author"); } else { log.info("Welcome, Guest"); } ~~~ 现在,让我们看看当前用户在系统里允许做什么: ~~~ if(currentUser.isPermitted("articles:compose")) { log.info("You can compose an article"); } else { log.info("You are not permitted to compose an article!"); } if(currentUser.isPermitted("articles:save")) { log.info("You can save articles"); } else { log.info("You can not save articles"); } if(currentUser.isPermitted("articles:publish")) { log.info("You can publish articles"); } else { log.info("You can not publish articles"); } ~~~ ### 6.Realm配置 在真实应用中,我们将需要从数据库一个得到用户凭证而不是从*shiro.ini*文件中。这就是*Realm*概念的用武之地。在Apache Shiro术语中,一个[Realm](https://shiro.apache.org/realm.html)就是一个指向存储用于验证授权的用户凭证的DAO。 创建一个realm,我们只需要实现*Realm*接口。这很令人讨厌;然而,我们可以继承框架提供的默认实现。其中一个就是*JdbcRealm*。 我们创建一个自定义realm实现来继承*JdbcRealm*类并重下以下方法:*doGetAuthenticationInfo(),doGetAuthorizationInfo(),getRoleNamesForUser()和getPermissions()*。 让我们创建一个继承*JdbcRealm*类的realm: ~~~ public class MyCustomRealm extends JdbcRealm { //... } ~~~ 为了简单起见,我们使用*java.util.Map*来模拟一个数据库: ~~~ private Map<String, String> credentials = new HashMap<>(); private Map<String, Set<String>> roles = new HashMap<>(); private Map<String, Set<String>> perm = new HashMap<>(); { credentials.put("user", "password"); credentials.put("user2", "password2"); credentials.put("user3", "password3"); roles.put("user", new HashSet<>(Arrays.asList("admin"))); roles.put("user2", new HashSet<>(Arrays.asList("editor"))); roles.put("user3", new HashSet<>(Arrays.asList("author"))); perm.put("admin", new HashSet<>(Arrays.asList("*"))); perm.put("editor", new HashSet<>(Arrays.asList("articles:*"))); perm.put("author", new HashSet<>(Arrays.asList("articles:compose", "articles:save"))); } ~~~ 接着我们重写*doGetAuthenticationInfo()*: ~~~ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken uToken = (UsernamePasswordToken) token; if(uToken.getUsername() == null || uToken.getUsername().isEmpty() || !credentials.containsKey(uToken.getUsername())) { throw new UnknownAccountException("username not found!"); } return new SimpleAuthenticationInfo( uToken.getUsername(), credentials.get(uToken.getUsername()), getName()); } ~~~ 我们首先将提供的*AuthenticationToken*强制转换*UsernamePasswordToken*。从*uToken*,我们提取用户名(*uToken.getUsername()*) 并且以此从数据库获取用户凭证(密码)。 如果没有找到记录——我们抛出一个*UnknownAccountException*(这里还比较了密码是否相同,译者注),否则我们使用用户名和凭证构成一个*SimpleAuthenticatioInfo*对象然后返回。 如果用户凭证加盐哈希(一种加密策略,译者注),我们需要返回一个带盐的*SimpleAuthenticationInfo*: ~~~ return new SimpleAuthenticationInfo( uToken.getUsername(), credentials.get(uToken.getUsername()), ByteSource.Util.bytes("salt"), getName() ); ~~~ 我们也需要重写*doGetAuthorizationInfo()*,*getRoleNamesForUser()* 和*getPermissions()*。 最后,将自定义realm交给*securityManager*。我们所需做的就是用我们自定义的realm替换*IniRealm*,并且传给*DefaultSecurityManager*的构造器。 ~~~ Realm realm = new MyCustomRealm(); SecurityManager securityManager = new DefaultSecurityManager(realm); ~~~ 其余部分代码跟之前相同。这就是我们正确使用自定义realm所需配置的*securityManager*。 现在的问题是——框架如何匹配凭证?缺省的,JdbcRealm使用*SimpleCredentialsMatcher*,它只检查*AuthenticationToken*和*AuthenticationInfo*中的凭证是否相等。 如果我们哈希我们的密码。我们需要告知框架使用*HashedCredentialsMatcher *替换。哈希密码的realm的INI配置可在[这里](https://shiro.apache.org/realm.html#Realm-HashingCredentials)找到。 ### 7.注销 我们已经认证了用户,是时候实现注销了。调用一个简单的方法就能完成,使得用户session无效化并且退出: ~~~ currentUser.logout(); ~~~ ### 8.Session管理 框架通常伴随着session管理系统。如果在web环境使用,默认为HttpSession接口。 对于单应用,它使用企业session管理系统。好处是即使是在桌面环境和典型的web环境,你也可以使用session对象。 让我们看一个当前用户session交互的简单例子: ~~~ Session session = currentUser.getSession(); session.setAttribute("key", "value"); String value = (String) session.getAttribute("key"); if (value.equals("value")) { log.info("Retrieved the correct value! [" + value + "]"); } ~~~ ### 9.Sping Web应用下的Shiro 到目前为止我们概述了Apache Shiro的基本结构,并且我们在桌面环境中实现了。让我们继续把框架和Spring Boot应用整合起来。 注意重点在Shiro,而不是Spring应用——我们只用来快速搭建简单实例app。 #### 9.1.依赖 首先,我们需要添加Spring Boot parent 依赖到我们的*pom.xml*: ~~~ <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.2.RELEASE</version> </parent> ~~~ 接着,我们将以下依赖加到同个*pom.xml*文件里: ~~~ <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>${apache-shiro-core-version}</version> </dependency> ~~~ #### 9.2.配置 添加*shiro-spring-boot-web-starter*依赖到我们的*pom.xml*里将默认配置一些Apache Shiro应用的特性,比如*SecurityManager*。 然而,我们仍需要配置*Realm *和*Shiro security*过滤器。我们将 和上边一样的自定义realm。 因此,在Spring Boot应用运行主类上,我们加上如下*Bean *定义: ~~~ @Bean public Realm realm() { return new MyCustomRealm(); } @Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition filter = new DefaultShiroFilterChainDefinition(); filter.addPathDefinition("/secure", "authc"); filter.addPathDefinition("/**", "anon"); return filter; } ~~~ 在*ShiroFilterChainDefinition*里,我们应用*authc *过滤*/secure*路径,又应用*anon*过滤通配符表示的其他路径。 *authc*和*anon*都是默认为web应用提供的。其他默认的过滤器可在[这里](https://shiro.apache.org/web.html#Web-DefaultFilters)找到。 如果我们不定义*Realm*bean。*ShiroAutoConfiguration*将默认提供一个*IniRealm *实现,它会去找*src/main/resources*或者*src/main/resources/META-INF*下的*shiro.ini *文件。 如果我们不定义一个*ShiroFilterChainDefinition*bean。框架保护所有的路径并且设置登录URL为*login.jsp*。 我们可以改变默认的登录URL并且添加其他默认项到我们的*application.properties*: ~~~ shiro.loginUrl = /login shiro.successUrl = /secure shiro.unauthorizedUrl = /login ~~~ 现在*authc *过滤器作用与*/secure*。该路径的所有请求将要求身份认证。 #### 9.3.认证和授权 创建一个*ShiroSpringController*来映射以下路径:*/index,/login, /logout* 和 */secure*。 我们实际实现上述的用户认证在*login() *方法中。如果认证成功,用户重定向到安全级别页面: ~~~ Subject subject = SecurityUtils.getSubject(); if(!subject.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken( cred.getUsername(), cred.getPassword(), cred.isRememberMe()); try { subject.login(token); } catch (AuthenticationException ae) { ae.printStackTrace(); attr.addFlashAttribute("error", "Invalid Credentials"); return "redirect:/login"; } } return "redirect:/secure"; ~~~ 现在来实现*secure()*,当前用户通过*SecurityUtils.getSubject()*获取*currentUser *。角色和用户权限还有用户名通过安全级别页面传递: ~~~ Subject currentUser = SecurityUtils.getSubject(); String role = "", permission = ""; if(currentUser.hasRole("admin")) { role = role + "You are an Admin"; } else if(currentUser.hasRole("editor")) { role = role + "You are an Editor"; } else if(currentUser.hasRole("author")) { role = role + "You are an Author"; } if(currentUser.isPermitted("articles:compose")) { permission = permission + "You can compose an article, "; } else { permission = permission + "You are not permitted to compose an article!, "; } if(currentUser.isPermitted("articles:save")) { permission = permission + "You can save articles, "; } else { permission = permission + "\nYou can not save articles, "; } if(currentUser.isPermitted("articles:publish")) { permission = permission + "\nYou can publish articles"; } else { permission = permission + "\nYou can not publish articles"; } modelMap.addAttribute("username", currentUser.getPrincipal()); modelMap.addAttribute("permission", permission); modelMap.addAttribute("role", role); return "secure"; ~~~ 自此,我们完成了怎样继承Apache Shiro到Spring Boot应用。 . 同时,注意可以使用框架提供的额外[注解](https://shiro.apache.org/spring.html)来配合过滤器链定义来保证我们应用的安全。 ### 10.JEE整合 整合Apache Shiro到一个JEE应用可以看成一个配置web.xml文件的问题。像往常一样, 配置会先在class路径下找*shiro.ini*。一个详细的配置例子在[这](https://shiro.apache.org/web.html#Web-%7B%7Bweb.xml%7D%7D)。此外,JSP标签可以在[这](https://shiro.apache.org/web.html#Web-JSP%2FGSPTagLibrary)找到。 ### 11.结语 在本教程中,我们认识了Apache Shiro的认证与授权机制。我们也关注到怎样定义一个自定义realm并且把它交个SecurityManager。 一如既往,完整源码可以[在GitHub](https://github.com/eugenp/tutorials/tree/master/apache-shiro)获取。 译者主页:[rebey.cn](http://rebey.cn/)