SpringBoot2.0整合Shiro
專案版本:
springboot2.x
shiro:1.3.2
Maven配置:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency>
寫在前面的話:
springboot中整合shiro相對簡單,只需要兩個類:一個是shiroConfig類,一個是CustonRealm類。
ShiroConfig類:
顧名思義就是對shiro的一些配置,相對於之前的xml配置。包括:過濾的檔案和許可權,密碼加密的演算法,其用註解等相關功能。
CustomRealm類:
自定義的CustomRealm繼承AuthorizingRealm。並且重寫父類中的doGetAuthorizationInfo(許可權相關)、doGetAuthenticationInfo(身份認證)這兩個方法。最基本的配置:
shiroConfig配置:
package com.cj.shirodemo.config; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import java.util.LinkedHashMap; import java.util.Map; /** * 描述: * * @author caojing * @create 2019-01-27-13:38 */ @Configuration public class ShiroConfig { @Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setUnauthorizedUrl("/notRole"); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); // <!-- authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問--> filterChainDefinitionMap.put("/webjars/**", "anon"); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/", "anon"); filterChainDefinitionMap.put("/front/**", "anon"); filterChainDefinitionMap.put("/api/**", "anon"); filterChainDefinitionMap.put("/admin/**", "authc"); filterChainDefinitionMap.put("/user/**", "authc"); //主要這行程式碼必須放在所有許可權設定的最後,不然會導致所有 url 都被攔截 剩餘的都需要認證 filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public SecurityManager securityManager() { DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager(); defaultSecurityManager.setRealm(customRealm()); return defaultSecurityManager; } @Bean public CustomRealm customRealm() { CustomRealm customRealm = new CustomRealm(); return customRealm; } }
shiroConfig 也不復雜,基本就三個方法。再說這三個方法之前,我想給大家說一下shiro的三個核心概念:
Subject: 代表當前正在執行操作的使用者,但Subject代表的可以是人,也可以是任何第三方系統帳號。當然每個subject例項都會被繫結到SercurityManger上。
SecurityManger:SecurityManager是Shiro核心,主要協調Shiro內部的各種安全元件,這個我們不需要太關注,只需要知道可以設定自定的Realm。
Realm:使用者資料和Shiro資料互動的橋樑。比如需要使用者身份認證、許可權認證。都是需要通過Realm來讀取資料。
shiroFilter方法:
這個方法看名字就知道了:shiro的過濾器,可以設定登入頁面(setLoginUrl)、許可權不足跳轉頁面(setUnauthorizedUrl)、具體某些頁面的許可權控制或者身份認證。
注意:這裡是需要設定SecurityManager(setSecurityManager)。
預設的過濾器還有:anno、authc、authcBasic、logout、noSessionCreation、perms、port、rest、roles、ssl、user過濾器。
具體的大家可以檢視package org.apache.shiro.web.filter.mgt.DefaultFilter。這個類,常用的也就authc、anno。
securityManager 方法:
檢視原始碼可以知道 securityManager是一個介面類,我們可以看下它的實現類:
具體怎麼實現的,感興趣的同學可以看下。由於專案是一個web專案,所以我們使用的是DefaultWebSecurityManager ,然後設定自己的Realm。
CustomRealm 方法:
將 customRealm的例項化交給spring去管理,當然這裡也可以利用註解的方式去注入。customRealm配置:
package com.cj.shirodemo.config;
import org.apache.shiro.SecurityUtils;
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.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.Set;
/**
* 描述:
*
* @author caojing
* @create 2019-01-27-13:57
*/
public class CustomRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String username = (String) SecurityUtils.getSubject().getPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Set<String> stringSet = new HashSet<>();
stringSet.add("user:show");
stringSet.add("user:admin");
info.setStringPermissions(stringSet);
return info;
}
/**
* 這裡可以注入userService,為了方便演示,我就寫死了帳號了密碼
* private UserService userService;
* <p>
* 獲取即將需要認證的資訊
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("-------身份認證方法--------");
String userName = (String) authenticationToken.getPrincipal();
String userPwd = new String((char[]) authenticationToken.getCredentials());
//根據使用者名稱從資料庫獲取密碼
String password = "123";
if (userName == null) {
throw new AccountException("使用者名稱不正確");
} else if (!userPwd.equals(password )) {
throw new AccountException("密碼不正確");
}
return new SimpleAuthenticationInfo(userName, password,getName());
}
}
說明:
自定義的Realm類繼承AuthorizingRealm類,並且過載doGetAuthorizationInfo和doGetAuthenticationInfo兩個方法。
doGetAuthorizationInfo: 許可權認證,即登入過後,每個身份不一定,對應的所能看的頁面也不一樣。
doGetAuthenticationInfo:身份認證。即登入通過賬號和密碼驗證登陸人的身份資訊。
controller類:
新建一個HomeIndexController類,加入如下程式碼:
@RequestMapping(value = "/login", method = RequestMethod.GET)
@ResponseBody
public String defaultLogin() {
return "首頁";
}
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ResponseBody
public String login(@RequestParam("username") String username, @RequestParam("password") String password) {
// 從SecurityUtils裡邊建立一個 subject
Subject subject = SecurityUtils.getSubject();
// 在認證提交前準備 token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 執行認證登陸
try {
subject.login(token);
} catch (UnknownAccountException uae) {
return "未知賬戶";
} catch (IncorrectCredentialsException ice) {
return "密碼不正確";
} catch (LockedAccountException lae) {
return "賬戶已鎖定";
} catch (ExcessiveAttemptsException eae) {
return "使用者名稱或密碼錯誤次數過多";
} catch (AuthenticationException ae) {
return "使用者名稱或密碼不正確!";
}
if (subject.isAuthenticated()) {
return "登入成功";
} else {
token.clear();
return "登入失敗";
}
}
測試:
我們可以使用postman進行測試:
ok 身份認證是沒問題了,我們再來考慮如何加入許可權。
利用註解配置許可權:
其實,我們完全可以不用註解的形式去配置許可權,因為在之前已經加過了:DefaultFilter類中有perms(類似於perms[user:add])這種形式的。但是試想一下,這種控制的粒度可能會很細,具體到某一個類中的方法,那麼如果是配置檔案配,是不是每個方法都要加一個perms?但是註解就不一樣了,直接寫在方法上面,簡單快捷。
很簡單,主需要在shiroConfig類中再加入如下程式碼,就能開啟註解:
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* *
* 開啟Shiro的註解(如@RequiresRoles,@RequiresPermissions),需藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證
* *
* 配置以下兩個bean(DefaultAdvisorAutoProxyCreator(可選)和AuthorizationAttributeSourceAdvisor)即可實現此功能
* * @return
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
新建一個UserController類。如下:
@RequestMapping("/user")
@Controller
public class UserController {
@RequiresPermissions("user:list")
@ResponseBody
@RequestMapping("/show")
public String showUser() {
return "這是學生資訊";
}
}
重複剛才的登入步驟,登入成功後,postman 輸入localhost:8080/user/show
確實是沒有許可權。方法上是 @RequiresPermissions("user:list"),而customRealm中是 user:show、user:admin。我們可以調整下方法上的許可權改為user:show。除錯一下,發現成功了。
這裡有一個問題:當沒有許可權時,系統會報錯,而沒有跳轉到對應的沒有許可權的頁面,也就是setUnauthorizedUrl這個方法沒起作用,這個問題,下一篇會給出解決方案-。-
密碼採用加密方式進行驗證:
其實上面的功能已經基本滿足我們的需求了,但是唯一一點美中不足的是,密碼都是採用的明文方式進行比對的。那麼shiro是否提供給我們一種密碼加密的方式呢?答案是肯定。
shiroConfig中加入加密配置:
@Bean(name = "credentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 雜湊演算法:這裡使用MD5演算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 雜湊的次數,比如雜湊兩次,相當於 md5(md5(""));
hashedCredentialsMatcher.setHashIterations(2);
// storedCredentialsHexEncoded預設是true,此時用的是密碼加密用的是Hex編碼;false時用Base64編碼
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
customRealm初始化的時候耶需要做一些改變:
@Bean
public CustomRealm customRealm() {
CustomRealm customRealm = new CustomRealm();
// 告訴realm,使用credentialsMatcher加密演算法類來驗證密文
customRealm.setCredentialsMatcher(hashedCredentialsMatcher());
customRealm.setCachingEnabled(false);
return customRealm;
}
流程是這樣的,使用者註冊的時候,程式將明文通過加密方式加密,存到資料庫的是密文,登入時將密文取出來,再通過shiro將使用者輸入的密碼進行加密對比,一樣則成功,不一樣則失敗。
我們可以看到這裡的加密採用的是MD5,而且是加密兩次(MD5(MD5))。
shiro提供了SimpleHash類幫助我們快速加密:
public static String MD5Pwd(String username, String pwd) {
// 加密演算法MD5
// salt鹽 username + salt
// 迭代次數
String md5Pwd = new SimpleHash("MD5", pwd,
ByteSource.Util.bytes(username + "salt"), 2).toHex();
return md5Pwd;
}
也就是說註冊的時候呼叫一下上面的方法得到密文之後,再存入資料庫。
在CustomRealm進行身份認證的時候我們也需要作出改變:
System.out.println("-------身份認證方法--------");
String userName = (String) authenticationToken.getPrincipal();
String userPwd = new String((char[]) authenticationToken.getCredentials());
//根據使用者名稱從資料庫獲取密碼
String password = "2415b95d3203ac901e287b76fcef640b";
if (userName == null) {
throw new AccountException("使用者名稱不正確");
} else if (!userPwd.equals(userPwd)) {
throw new AccountException("密碼不正確");
}
//交給AuthenticatingRealm使用CredentialsMatcher進行密碼匹配
return new SimpleAuthenticationInfo(userName, password,
ByteSource.Util.bytes(userName + "salt"), getName());
這裡唯一需要注意的是:你註冊的加密方式和設定的加密方式還有Realm中身份認證的方式都是要一模一樣的。
本文中的加密 :MD5兩次、salt=u