【Shiro學習筆記】一、Shiro具體使用(基於springboot2.x,前後端分離)

xiao_zhu_kuai_pao發表於2020-11-11

PS:歡迎轉載,但請註明出處,謝謝配合。

一、前言

基於springboot2.x,前後端分離

二、具體步驟

1、加入shiro依賴包

修改pom.xml

<!-- 引入 shiro -->
<dependency>
    <groupId>org.apache.shiro</groupId>
     <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>

2、自定義認證過濾器FormAuthenticationFilter

目的:將原來登入校驗不通過的重定向改為返回Json資料
做法:繼承 authc 的預設認證過濾器(FormAuthenticationFilter)

public class MyFormAuthenticationFilter extends FormAuthenticationFilter {

1)重寫onAccessDenied 方法
針對shiro配置認證規則為“authc”的那些url,當校驗出使用者沒有登入的話,執行該方法

/**
 * 	當shiro校驗使用者未登入時,返回JSON資料(代替原有的跳轉到登入介面)
 */
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
	this.log.error("使用者未登入,拒絕訪問");
	response.setContentType("application/json") ;
	response.setCharacterEncoding("UTF-8") ;
	PrintWriter out = response.getWriter() ;
	String resultJson = JSON.toJSONString(ResultUtil.error("IMS0000","登入已失效,請重新登入!")) ;
	out.write(resultJson) ;
	out.flush() ;
	out.close() ;
	return false ;
}

3、自定義授權過濾器RolesAuthorizationFilter

目的:將原來許可權校驗不通過的重定向改為返回Json資料
做法:繼承 roles 的預設授權過濾器(RolesAuthorizationFilter)

public class MyRolesAuthorizationFilter extends RolesAuthorizationFilter{

1)重寫 onAccessDenied 方法
針對shiro配置授權規則為“roles”的那些url,當校驗出使用者沒有對應許可權的話,執行該方法。
返回json資料格式,由前端決定授權拒絕的後續邏輯。
寫法參考上面的自定義認證過濾器MyFormAuthenticationFilter。

2)重寫 isAccessAllowed 方法(可選)
若role配置多個,預設情況下,是必須多個role同時具備,才有訪問許可權。但一般實際情況,是隻要擁有配置中的某個許可權,即可訪問。因此可以重寫isAccessAllowed 方法,將父類中的“and”邏輯改為“or”邏輯。

// 原邏輯,多個role必須同時滿足,才有許可權訪問
// return subject.hasAllRoles(roles);   

// 修改後邏輯 -- begin (多個role之間是“或”關係,只有一個滿足即可)
for(String role: roles) {
	if(subject.hasRole(role)) {
		return true ;
	}
}
return false ;
// 修改後邏輯 --end

4、編寫shiro配置類

1)編寫一個shiro的配置類

@Configuration
public class ShiroConfig {

2)配置類中,定義shiro的過濾器工廠bean
在該過濾器工廠中,可以 設定自定義過濾器、配置認證規則、配置授權規則

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
	// 建立 ShiroFilterFactoryBean
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    
    // 1、建立過濾器Map,用來裝自定義過濾器
    LinkedHashMap<String, Filter> map = new LinkedHashMap<>();
    // 2、將自定義過濾器放入map中,如果實現了自定義授權過濾器,那就必須在這裡註冊,否則Shiro不會使用自定義的授權過濾器
    map.put("authc", new MyFormAuthenticationFilter());	// 認證過濾器
    map.put("roles", new MyRolesAuthorizationFilter()); // 授權過濾器  
    // 3、將過濾器繫結到shiroFilterFactoryBean上
    shiroFilterFactoryBean.setFilters(map);
    // 4.配置認證規則 // <!-- authc:所有url都必須認證通過才可以訪問;  anon:所有url都都可以匿名訪問-->
    Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
    filterChainDefinitionMap.put("/login/**", "anon");	// 類似/login/xxx請求,可以直接訪問
    filterChainDefinitionMap.put("/jnl/**", "anon");	// 類似/jnl/xxx請求,可以直接訪問
    // 5.配置授權規則
    filterChainDefinitionMap.put("/admin/**", "roles[admin]");	// 類似/admin/**請求,需要admin角色才可訪問(可以寫多個role,預設是多個role同時滿足,可以自定義改成或關係)
    //注意:下面這行程式碼必須放在所有許可權設定的最後,不然會導致所有 url 都被攔截,都需要認證(因為filterChainDefinitions 配置過濾規則,是從上到下的順序匹配)
    filterChainDefinitionMap.put("/**", "authc");		// 剩下的其他請求,需要登入後才可訪問
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    
    return shiroFilterFactoryBean;
}

3)配置類中,定義許可權管理bean
主要包裝我們編寫的業務Realm,該bean會注給上面的過濾器工廠。shiro過濾器攔截的請求,會通過SecurityManager,走到業務Realm的相關方法。

@Bean
public SecurityManager securityManager(UserRealm myShiroRealm) {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(myShiroRealm);
    return securityManager;
}

4)配置類中,定義業務Realm bean
下面程式碼中的兩個引數,是我專案中的資料庫操作Service,大家根據自己的情況實現即可。或者先不用資料庫操作,直接程式碼中模擬資料庫的返回值,先試試Shiro效果,則不用注入下面兩個引數。

@Bean  
public UserRealm myShiroRealm(ShiroUserService shiroUserServiceImpl, ShiroUserRoleService shiroUserRoleServiceImpl) {
    UserRealm userRealm = new UserRealm();
    userRealm.setShiroUserService(shiroUserServiceImpl) ;
    userRealm.setShiroUserRoleService(shiroUserRoleServiceImpl) ;
    return userRealm;
}

5、編寫Realm類

目的:運算元據層,實現使用者的認證和授權
做法:繼承Realm父類 AuthorizingRealm

public class UserRealm extends AuthorizingRealm {

1)實現 doGetAuthenticationInfo(認證方法)

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
	UsernamePasswordToken upToken = (UsernamePasswordToken) token;
	String userName = upToken.getPrincipal().toString() ;
	// 1.根據使用者名稱,查詢使用者資訊
    Map userMap = this.shiroUserService.queryUser(userName) ;
    // 2.判斷使用者名稱是否存在
    if(null == userMap || userMap.isEmpty()) {
    	return null ;  // 這裡返回null後,後面邏輯會報出對應異常(賬號不存在)
    }
    // 3.根據salt,將前端上送的密碼,加鹽加密後,重新放入原token,以便後續邏輯比對兩個加密後的密碼是否一致【我專案中資料庫儲存的使用者密碼是MD5加鹽加密後的,明文儲存的則不需要這步】
    String salt = (String) userMap.get("salt") ; 			// 密碼加密用的鹽值
    char[] oldPassword = upToken.getPassword() ;
    upToken.setPassword(this.md5withSalt(String.valueOf(oldPassword), salt).toCharArray());
    String password = (String) userMap.get("password") ; 	//DB中的密碼(加密後的)
    // 4.根據查詢出的使用者名稱/密碼,構建SimpleAuthenticationInfo認證物件(第一個引數: 使用者名稱 ;第二個引數:DB中查到的密碼 ; 第三個引數:當前Realm名字)
    return new SimpleAuthenticationInfo(userName, password, this.getClass().getName()) ;  //shiro後續會根據該物件,與原上送引數token物件,比對兩者的密碼是否一致,不一致則丟擲IncorrectCredentialsException異常
}

登入交易中,會呼叫Subject.login(token),最終會執行到Realm 的 doGetAuthenticationInfo 方法。

2)實現 doGetAuthorizationInfo(授權方法)

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
	// 1. 獲取授權的使用者
	String userName = principals.getPrimaryPrincipal().toString() ;
	// 2. 根據使用者名稱,從DB中查詢擁有的角色(另:實際可進行快取優化,不應每次都查庫)
	Set<String> roles = this.shiroUserRoleService.queryUserRoles(userName) ;
	// 3. 根據角色集合Set<String>,構建SimpleAuthorizationInfo授權物件
	return new SimpleAuthorizationInfo(roles);
}

shiro過濾器攔截請求後,若對應了role規則配置的路徑,則會判斷是否具有role角色許可權,通過RolesAuthorizationFilter的 isAccessAllowed方法,最終會執行到Realm 的 doGetAuthorizationInfo 方法。

6、編寫登入Controller

登入交易中,呼叫shiro相關類進行認證,主要包括如下內容:

Subject subject = SecurityUtils.getSubject() ; // 獲取Subject-使用者主體(會把操作交給SecurityManager,最後到Realm)
AuthenticationToken token = new UsernamePasswordToken(userName, passWord) ; // 將從獲取的使用者名稱和密碼設定到一個token中

subject.login(token); // 通過捕獲該方法丟擲的異常,返回對應報錯資訊給前端(UnknownAccountException-使用者名稱錯誤,IncorrectCredentialsException-密碼錯誤)

相關文章