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