Apache Shiro 是一個功能強大且靈活的開放原始碼安全框架,可以細粒度地處理認證 (Authentication),授權 (Authorization),會話 (Session) 管理和加密 (cryptography) 等企業級應用中常見的安全控制流程。 Apache Shiro 的首要目標是易於使用和理解。 有時候安全性的流程控制會非常複雜,對開發人員來說是件很頭疼的事情,但並不一定如此。 框架就應該儘可能地掩蓋複雜性,並公開一個簡潔而直觀的 API,從而簡化開發人員的工作,確保其應用程式安全性。這次我們聊一聊如何在 Spring Web 應用中使用 Shiro 實現許可權控制。
功能
Apache Shiro 是一個具有許多功能的綜合型應用程式安全框架。 下圖為 Shiro 中的最主要的幾個功能:
Shiro 的主要目標是“應用安全的四大基石” - 認證,授權,會話管理和加密:- 身份驗證:也就是通常所說的 “登入”,為了證明使用者的行為所有者。
- 授權:訪問控制的過程,即確定什麼使用者可以訪問哪些內容。
- 會話管理:即使在非 Web 應用程式中,也可以管理使用者特定的會話,這也是 Shiro 的一大亮點。
- 加密技術:使用加密演算法保證資料的安全,非常易於使用。
架構
從整體概念上理解,Shiro 的體系架構有三個主要的概念:Subject (主體,也就是使用者),Security Manager (安全管理器)和 Realms (領域)。 下圖描述了這些元件之間的關係:
這幾大元件可以這樣理解:- Subject (主體):主體是當前正在操作的使用者的特定資料集合。主體可以是一個人,也可以代表第三方服務,守護程式,定時任務或類似的東西,也就是幾乎所有與該應用進行互動的事物。
- Security Manager (安全管理器):它是 Shiro 的體系結構的核心,扮演了類似於一把 “傘” 的角色,它主要負責協調內部的各個元件,形成一張安全網。
- Realms (領域):Shiro 與應用程式安全資料之間的 “橋樑”。當需要實際與使用者帳戶等安全相關資料進行互動以執行認證和授權時,Shiro 將從 Realms 中獲取這些資料。
資料準備
在 Web 應用中,對安全的控制主要有角色、資源、許可權(什麼角色能訪問什麼資源)幾個概念,一個使用者可以有多個角色,一個角色也可以訪問多個資源,也就是角色可以對應多個許可權。落實到資料庫設計上,我們至少需要建 5 張表:使用者表、角色表、資源表、角色-資源表、使用者-角色表,這 5 張表的結構如下: 使用者表:
id | username | password |
---|---|---|
1 | 張三 | 123456 |
2 | 李四 | 666666 |
3 | 王五 | 000000 |
角色表:
id | rolename |
---|---|
1 | 管理員 |
2 | 經理 |
3 | 員工 |
資源表:
id | resname |
---|---|
1 | /user/add |
2 | /user/delete |
3 | /compony/info |
角色-資源表:
id | roleid | resid |
---|---|---|
1 | 1 | 1 |
2 | 1 | 2 |
3 | 2 | 3 |
使用者-角色表:
id | userid | roleid |
---|---|---|
1 | 1 | 1 |
2 | 1 | 2 |
3 | 1 | 3 |
對應的 POJO 類如下:
/**
* 使用者
*/
public class User {
private Integer id;
private String username;
private String password;
//getter & setter...
}
複製程式碼
/**
* 角色
*/
public class Role {
private String id;
private String rolename;
}
複製程式碼
/**
* 資源
*/
public class Resource {
private String id;
private String resname;
}
複製程式碼
/**
* 角色-資源
*/
public class RoleRes {
private String id;
private String roleid;
private String resid;
}
複製程式碼
/**
* 使用者-角色
*/
public class UserRole {
private String id;
private String userid;
private String roleid;
}
複製程式碼
Spring 與 Shiro 整合的詳細步驟,請參閱我的部落格 《 Spring 應用中整合 Apache Shiro 》。
這裡補充一下:需要提前引入 Shiro 的依賴,開啟 mvnrepository.com,搜尋 Shiro,我們需要前三個依賴,也就是 Shiro-Core、Shiro-Web 以及 Shiro-Spring,以 Maven 專案為例,在 pom.xml
中的 <dependencies>
節點下新增如下依賴:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
複製程式碼
在 application-context.xml
中需要這樣配置 shiroFilter
bean:
<!-- 配置shiro的過濾器工廠類,id- shiroFilter要和我們在web.xml中配置的過濾器一致 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<!-- 登入頁面 -->
<property name="loginUrl" value="/login"/>
<!-- 登入成功後的頁面 -->
<property name="successUrl" value="/index"/>
<!-- 非法訪問跳轉的頁面 -->
<property name="unauthorizedUrl" value="/403"/>
<!-- 許可權配置 -->
<property name="filterChainDefinitions">
<value>
<!-- 無需認證即可訪問的靜態資源,還可以新增其他 url -->
/static/** = anon
<!-- 除了上述忽略的資源,其他所有資源都需要認證後才能訪問 -->
/** = authc
</value>
</property>
</bean>
複製程式碼
接下來就需要定義 Realm 了,自定義的 Realm 整合自 AuthorizingRealm
類:
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 驗證許可權
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String loginName = SecurityUtils.getSubject().getPrincipal().toString();
if (loginName != null) {
String userId = SecurityUtils.getSubject().getSession().getAttribute("userSessionId").toString();
// 許可權資訊物件,用來存放查出的使用者的所有的角色及許可權
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 使用者的角色集合
ShiroUser shiroUser = (ShiroUser) principalCollection.getPrimaryPrincipal();
info.setRoles(shiroUser.getRoles());
info.addStringPermissions(shiroUser.getUrlSet());
return info;
}
return null;
}
/**
* 認證回撥函式,登入時呼叫
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
String username = (String) token.getPrincipal();
User user = new User();
sysuser.setUsername(username);
try {
List<SysUser> users = userService.findByNames(user);
List<String> roleList= userService.selectRoleNameListByUserId(users.get(0).getId());
if (users.size() != 0) {
String pwd = users.get(0).getPassword();
// 當驗證都通過後,把使用者資訊放在 session 裡
Session session = SecurityUtils.getSubject().getSession();
session.setAttribute("userSession", users.get(0));
session.setAttribute("userSessionId", users.get(0).getId());
session.setAttribute("userRoles", org.apache.commons.lang.StringUtils.join(roleList,","));
return new SimpleAuthenticationInfo(username,users.get(0).getPassword());
} else {
// 沒找到該使用者
throw new UnknownAccountException();
}
} catch (Exception e) {
System.out.println(e.getMessage());
}
return null;
}
/**
* 更新使用者授權資訊快取.
*/
public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
super.clearCachedAuthorizationInfo(principals);
}
/**
* 更新使用者資訊快取.
*/
public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
super.clearCachedAuthenticationInfo(principals);
}
/**
* 清除使用者授權資訊快取.
*/
public void clearAllCachedAuthorizationInfo() {
getAuthorizationCache().clear();
}
/**
* 清除使用者資訊快取.
*/
public void clearAllCachedAuthenticationInfo() {
getAuthenticationCache().clear();
}
/**
* 清空所有快取
*/
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
}
/**
* 清空所有認證快取
*/
public void clearAllCache() {
clearAllCachedAuthenticationInfo();
clearAllCachedAuthorizationInfo();
}
}
複製程式碼
最後定義一個使用者登入的控制器,接受使用者的登入請求:
@Controller
public class UserController {
/**
* 使用者登入
*/
@PostMapping("/login")
public String login(@Valid User user,BindingResult bindingResult,RedirectAttributes redirectAttributes){
try {
if(bindingResult.hasErrors()){
return "login";
}
//使用許可權工具進行認證,登入成功後跳到 shiroFilter bean 中定義的 successUrl
SecurityUtils.getSubject().login(new UsernamePasswordToken(user.getUsername(), user.getPassword()));
return "redirect:index";
} catch (AuthenticationException e) {
redirectAttributes.addFlashAttribute("message","使用者名稱或密碼錯誤");
return "redirect:login";
}
}
/**
* 登出登入
*/
@GetMapping("/logout")
public String logout(RedirectAttributes redirectAttributes ){
SecurityUtils.getSubject().logout();
return "redirect:login";
}
}
複製程式碼