後端管理系統登入一般都涉及到許可權控制,許可權管理元件用的最多的就是Apache的Shiro了,任何系統的登入模組,基本都可以使用shiro來實現我們的功能。
什麼是Shiro
相信看到這篇文章的人都知道Shiro是什麼吧,Apache Shiro是Java的一個安全(許可權)框架,Shiro可以非常容易的開發出足夠好的應用,JavaSE和Java EE環境都可以使用。Shiro可以完成:認證、授權、加密、會話管理、與web整合、快取等。官方圖如下:
這裡說下shiro安全的四大基石(上圖中上層4個黃色部分)
- Authentication:認證,身份驗證,有時稱為“登入”,這是證明使用者“我是誰”的行為。
- Authorization:授權,訪問控制的過程,即確定“誰”有權訪問“什麼”。
- Session Management:會話管理,管理特定於使用者的會話,即使在非web或EJB應用程式中也是如此。
- Cryptography:加密,使用密碼演算法保持資料安全,同時仍然易於使用。
使用shiro
shiro的使用也很簡單,如下圖,主要有三個類
- Subject:主體,代表當前使用者,當前登入使用者的所有屬性都可以通過該類獲取
- SecurityManager:安全管理器,判斷Subject是否能登入、是否具有某些許可權等等操作
- Realm:資料訪問器,shiro通過Realm訪問資料庫獲取許可權資訊
三者在程式碼中的體現是: 簡單的說,當你對當前登入使用者Subject進行操作時,Subject會與SecurityManager互動,SecurityManager會使用Realm查詢許可權。
當然,還有另外一個類ShiroFilterFactoryBean也比較重要,下面講。
Spring Boot整合Shiro
話不多說,上程式碼。首先來思考一下正常的網頁登入流程是什麼樣的?我整理的一下,大概流程如下。
- 頁面攔截的設定,系統中有些頁面可以直接訪問,有些頁面需要登入才能訪問,有些頁面不僅需要登入還需要登入的使用者有相關許可權才能訪問
- 如果使用者沒登入直接訪問需要登入或需要許可權的頁面,則調轉到登入頁面讓使用者登入
- 使用者登入認證後,訪問需要許可權的頁面時,系統可以識別該使用者是誰並鑑權。
如果解決以上三個問題,上程式碼。
匯入shiro-spring-boot-web-starter
maven中匯入如下Shiro的座標。
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.5.1</version>
</dependency>
複製程式碼
攔截所有頁面
步驟都在下面程式碼的註釋了,總結一句話就是:通過ShiroFilterFactoryBean配置攔截的頁面,攔截後調轉到登入頁,登入過程中,SecurityManager呼叫Realm來獲取認證、授權的資訊。
package io.github.zebinh.zmall.mall.admin.config;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Shiro配置類
*/
@Configuration
public class ShiroConfig {
/**
* Shiro自帶的過濾器,可以在這裡配置攔截頁面
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Autowired DefaultWebSecurityManager securityManager){
// 1. 初始化一個ShiroFilter工程類
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 2. 我們知道Shiro是通過SecurityManager來管理整個認證和授權流程的,這個SecurityManager可以在下面初始化
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 3. 上面我們講過,有的頁面不需登入任何人可以直接訪問,有的需要登入才能訪問,有的不僅要登入還需要相關許可權才能訪問
Map<String, String> filterMap = new LinkedHashMap<>();
// 4. Shiro過濾器常用的有如下幾種
// 4.1. anon 任何人都能訪問,如登入頁面
filterMap.put("/api/user/login", "anon");
// 4.2. authc 需要登入才能訪問,如系統內的全體通知頁面
filterMap.put("/api/user/notics", "authc");
// 4.3. roles 需要相應的角色才能訪問
filterMap.put("/api/user/getUser", "roles[admin]");
// 5. 讓ShiroFilter按這個規則攔截
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
// 6. 使用者沒登入被攔截後,當然需要調轉到登入頁了,這裡配置登入頁
shiroFilterFactoryBean.setLoginUrl("/api/user/login");
return shiroFilterFactoryBean;
}
/**
* SecurityManager管理認證、授權整個流程
*/
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(@Autowired UserRealm userRealm){
// 7. 新建一個SecurityManager供ShiroFilterFactoryBean使用
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 8. SecurityManager既然管理認證等資訊,那他就需要一個類來幫他查資料庫吧。這就是Realm類
securityManager.setRealm(userRealm);
return securityManager;
}
/**
* 自定義Realm,當SecurityBean需要來查詢相關許可權資訊時,需要有Realm代勞
*/
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
/**
* 為了方便觀看,我將UserRealm類的實現寫在這裡了
*/
class UserRealm extends AuthorizingRealm {
// 9. 前面被authc攔截後,需要認證,SecurityBean會呼叫這裡進行認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("執行認證邏輯");
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
// 9.1. 為了方便演示,我這裡寫死了使用者admin-name密碼admin-pwd才能登入
if (token.getUsername() == null || !token.getUsername().equals("admin-name")){
return null;
}
return new SimpleAuthenticationInfo("", "admin-pwd", "");
}
// 10. 前面被roles攔截後,需要授權才能登入,SecurityManager需要呼叫這裡進行許可權查詢
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("執行授權邏輯");
// 10.1. 為了方便演示,這裡直接寫死返回了admin角色,所有能登入的角色都是admin了
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRole("admin");
return info;
}
}
}
複製程式碼
調轉到登入頁後,使用者登入,直接呼叫Subject類的login方法即可,Subject類會呼叫SecurityManager進行認證。看到這裡,應該就能理解本文開頭的兩幅圖了吧。
package io.github.zebinh.zmall.mall.admin.user.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("api/user")
public class UserController {
@PostMapping("login")
public String login(@RequestBody Map<String, String> user){
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user.get("username"), user.get("password"));
try{
subject.login(token);
}catch (UnknownAccountException e){
System.out.println("使用者名稱不存在");
return "使用者名稱不存在";
}catch (AuthenticationException e) {
System.out.println("認證失敗");
return "認證失敗";
}
return "登入成功";
}
@GetMapping("getUser")
public Object getUser() {
return SecurityUtils.getSubject();
}
@GetMapping("notics")
public String notics(){
return "通知";
}
}
複製程式碼
Shiro如何識別使用者
在登入完成後,shiro會發一個cookie給客戶端,包含了sessionId,因此能識別當前使用者,當訪問需要登入的頁面時,如發現cookie中有sessionId,判斷該使用者已登入,則不需要呼叫Realm再進行認證了。cookie如下所示。
那麼問題來了,cookie只有pc端才有,移動端並沒有cookie,該怎麼解決移動端的shiro認證問題呢,同時這種方式無法解決單點登入的問題,應為cookie只能在同源的網站被髮送到伺服器。
解決方案就是,這時候就要定義自己的Session管理器了。使用者登入後,自定義Session管理器生成session並生成一個token給到前端,前端每次訪問都傳遞token,shiro會呼叫自定義的Session管理器做校驗。Session中可以放置當前登入使用者的資訊並放置在redis中。自定義的Session管理器每次從redis中讀取資料即可。
JWT(JSON Web Token)
上面我們說到了使用token + redis來儲存使用者資訊。此次使用者資訊是儲存在伺服器的redis中的,還有另外一種方式JWT,它是將客戶資訊加密後返回給客戶端,客戶端每次使用該加密字串訪問伺服器,伺服器加密即可獲得使用者資訊。這種方式則是將使用者資訊存放在客戶端了。可以瞭解一下這種方式。
本篇文章討論的只是Shiro的使用,對於設計一個安全的登入(單點)系統還需要很多討論。
以上。