### 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/)