相關
一、realm的作用
Realm充當了Shiro與應用安全數據間的“橋樑”或者“連接器”。也就是說,當對用戶執行認證(登錄)和授權(訪問控制)驗證時,Shiro會從應用配置的Realm中查找用戶及其權限信息。
從這個意義上講,Realm實質上是一個安全相關的DAO:它封裝了數據源的連接細節,並在需要時將相關數據提供給Shiro。當配置Shiro時,你必須至少指定一個Realm,用於認證和(或)授權。配置多個Realm是可以的,但是至少需要一個。
Shiro內置了可以連接大量安全數據源(又名目錄)的Realm,如LDAP、關係數據庫(JDBC)、類似INI的文本配置資源以及屬性文件等。如果缺省的Realm不能滿足需求,你還可以插入代表自定義數據源的自己的Realm實現。
二、數據庫設計
下圖是一個簡單的權限關係庫,主要是有三個表,用戶、角色、權限,爲了容易管理,用戶只關聯角色,而不同的角色對應不同的權限,shiro框架允許我們隨意配置權限粗細度,可以精確到角色,也可以細微至某個權限(比如:用戶添加權限等),我們需要了解的是用戶和角色是一對多的關係(一個用戶對應多個角色),而權限和角色關係亦是一對多的關係。
三、創建自定義realm
即使shiro提供瞭如jdbcrealm,jndiRealm,和使用配置文件的realm,但是實際開發中,大部分情況仍需要我們根據實際情況定製適合自己項目的realm處理器,shiro提供了 AuthorizingRealm 抽象類,創建 MyShiroRealm 類繼承 AuthorizingRealm 抽象類,重寫 doGetAuthorizationInfo 方法,和 doGetAuthenticationInfo 方法,完成自定義realm,下面是我配置的realm。
需要注意的是,doGetAuthenticationInfo 是每次登錄會調用,而 doGetAuthorizationInfo 方法則是訪問到需要權限、角色的接口的時候纔會調用!
import java.util.Set;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
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.springframework.beans.factory.annotation.Autowired;
import cn.junengxiong.bean.User;
import cn.junengxiong.service.UserService;
/**
* 自定義登錄權限認證
*
* @ClassName: MyShiroRealm
* @Description TODO
* @version
* @author jh
* @date 2019年8月27日 下午4:12:40
*/
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
UserService userService;
/**
* 權限設置
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
if (userService == null) {
userService = (UserService) SpringBeanFactoryUtil.getBeanByName("userServiceImpl");
}
System.out.println("進入自定義權限設置方法!");
String username = (String) principals.getPrimaryPrincipal();
// 從數據庫或換村中獲取用戶角色信息
User user = userService.findByUsername(username);
// 獲取用戶角色
Set<String> roles = user.getRole();
// 獲取用戶權限
Set<String> permissions = user.getPermission();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 設置權限
simpleAuthorizationInfo.setStringPermissions(permissions);
// 設置角色
simpleAuthorizationInfo.setRoles(roles);
return simpleAuthorizationInfo;
}
/**
* 身份驗證
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("進入自定義登錄驗證方法!");
if (userService == null) {
userService = (UserService) SpringBeanFactoryUtil.getBeanByName("userServiceImpl");
}
// 通過username從數據庫中查找 User對象,如果找到,沒找到.
// 實際項目中,這裏可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重複執行該方法
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String username = usernamePasswordToken.getUsername();
String pwd = String.valueOf(usernamePasswordToken.getPassword());// 獲取用戶輸入的密碼
User user = userService.findByUsername(username);
if (user == null) {
throw new UnknownAccountException();// 用戶不存在
}
String password = user.getPassword();// 數據庫獲取的密碼
// 此處對用戶輸入密碼進行加密,對比數據庫查詢出來加密後的密碼,自定義加密方式
// ............................
// password = new SimpleHash("MD5", password, username, 1024).toString();
if (!password.equals(pwd)) {
throw new IncorrectCredentialsException();// 憑證錯誤
}
// 主要的(可以使用戶名,也可以是用戶對象),資格證書(數據庫獲取的密碼),區域名稱(當前realm名稱)
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, password, getName());
// 加鹽,使用每個用戶各自的用戶名加鹽,保證密碼相同時但是加密後密碼仍然不同,如果不適用shiro自帶憑證比較器,或者自定義憑證比較器,可以不設置加鹽
// simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(username));
return simpleAuthenticationInfo;
}
}
3.1 userService爲null問題
因爲springboot的bean注入機制,會導致在此realm生成的時候userService不會被注入,所有需要我們手動把UserService注入進來,用到了SpringBeanFactoryUtil工具類:
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
*
* @ClassName: SpringBeanFactoryUtil
* @Description 手動bean注入工具類,用於解決filter中注入service爲null問題
* @version
* @author jh
* @date 2019年8月28日 上午10:29:37
*/
@Component
public class SpringBeanFactoryUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (SpringBeanFactoryUtil.applicationContext == null) {
SpringBeanFactoryUtil.applicationContext = applicationContext;
}
}
private SpringBeanFactoryUtil() {
// TODO Auto-generated constructor stub
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
// 根據名稱-------@Resource 註解
public static Object getBeanByName(String name) {
return getApplicationContext().getBean(name);
}
// 根據類型-------@Autowired 註解
public static <T> T getBeanByType(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
// 根據名稱和類型--------@Autowired+@Qualifier
public static <T> T getBeanByNameAndType(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
}
四、把自定義的Realm交給SecurityManager
我們聲明好了自定義的realm後,需要把它交給SecurityManager來管理,需要在ShiroConfig類中,添加如下代碼
- 返回自定義realm,放入SecurityManager
/**
* 自定義身份認證 realm;
* <p>
* 必須寫這個類,並加上 @Bean 註解,目的是注入 MyShiroRealm, 否則會影響 MyShiroRealm類 中其他類的依賴注入
*/
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
return myShiroRealm;
}
- SecurityManager 配置
/**
* 注入 securityManager
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
五、user類
上面已經講過了,一個用戶對應着多個角色,而一個角色對應多個權限,
爲了方便,不進行數據庫的操作,在service中僞造三個用戶,而每個用戶擁有n個角色和n個權限,我們用set集合表示,可以根據情況隨意拓展。
import java.util.Set;
public class User {
private String username;
private String password;
private Set<String> role;
private Set<String> permission;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Set<String> getRole() {
return role;
}
public void setRole(Set<String> role) {
this.role = role;
}
public Set<String> getPermission() {
return permission;
}
public void setPermission(Set<String> permission) {
this.permission = permission;
}
@Override
public String toString() {
return "User [username=" + username + ", password=" + password + ", role=" + role + ", permission=" + permission
+ "]";
}
}
六、 業務層
service
public interface UserService {
User findByUsername(String username);
}
serviceImpl
import java.util.HashSet;
import java.util.Set;
import org.springframework.stereotype.Service;
import cn.junengxiong.bean.User;
import cn.junengxiong.service.UserService;
@Service
public class UserServiceImpl implements UserService {
@Override
public User findByUsername(String username) {
User user = new User();
user.setUsername(username);
Set<String> roleList = new HashSet<>();
Set<String> permissionsList = new HashSet<>();
switch (username) {
case "admin":
roleList.add("admin");
user.setPassword("admin");
permissionsList.add("user:add");
permissionsList.add("user:delete");
break;
case "consumer":
roleList.add("consumer");
user.setPassword("consumer");
permissionsList.add("consumer:modify");
break;
default:
roleList.add("guest");
user.setPassword("guest");
break;
}
user.setRole(roleList);
user.setPermission(permissionsList);
return user;
}
}
七、controller
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import cn.junengxiong.bean.ReturnMap;
@RestController
public class ShiroController {
@RequestMapping("/consumer/{str}")
@RequiresRoles(value = { "admin", "consumer" }, logical = Logical.OR)
public ReturnMap getMessage(@PathVariable(value = "str") String str) {
return new ReturnMap().success().data(str);
}
@RequestMapping("/admin/{str}")
@RequiresRoles("admin")
public ReturnMap getMessageAdmin(@PathVariable(value = "str") String str) {
return new ReturnMap().success().data(str);
}
@RequestMapping("/guest/{str}")
public ReturnMap getMessageGuest(@PathVariable(value = "str") String str) {
return new ReturnMap().success().data(str);
}
@RequestMapping("/login")
public ReturnMap login(String username, Boolean rememberMe, String password) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
if (rememberMe != null) {
token.setRememberMe(rememberMe);
}
try {
// 登錄
subject.login(token);
} catch (UnknownAccountException uae) {
// 用戶名未知...
return new ReturnMap().fail().message("用戶不存在!");
} catch (IncorrectCredentialsException ice) {
// 憑據不正確,例如密碼不正確 ...
return new ReturnMap().fail().message("密碼不正確!");
} catch (LockedAccountException lae) {
// 用戶被鎖定,例如管理員把某個用戶禁用...
return new ReturnMap().fail().message("用戶被鎖定!");
} catch (ExcessiveAttemptsException eae) {
// 嘗試認證次數多餘系統指定次數 ...
return new ReturnMap().fail().message("嘗試認證次數過多,請稍後重試!");
} catch (AuthenticationException ae) {
// 其他未指定異常
return new ReturnMap().fail().message("未知異常!");
}
return new ReturnMap().success().data("登錄成功!");
}
@RequestMapping("/loginout")
public ReturnMap getMessageGuest() {
Subject subject = SecurityUtils.getSubject();
// 登出
subject.logout();
return new ReturnMap().success().message("登出成功!");
}
/**
* 無權限訪問時
*
* @return
*/
@RequestMapping("/unauthorized")
public ReturnMap unauthorized() {
return new ReturnMap().invalid();
}
}
八、添加對權限註解支持
這一步卡了我一些時間,剛開始接觸shiro框架,但是加入權限控制註解,每次只會在登錄的時候調用登錄驗證接口,但是訪問有權限的接口時,並不會進入自定義realm中的 doGetAuthorizationInfo 方法,經查詢,該方法只會在用戶訪問到需要權限的接口的時候纔會調用,如果未使用緩存,則每次訪問權限接口均會調用一次 doGetAuthorizationInfo 方法,另外權限註解需要加入兩個配置項來支持,在shiroConfig配置類中添加如下代碼,啓動對權限註解的支持。
class ShiroConfig
....................
/**
* 開啓Shiro的註解(如@RequiresRoles,@RequiresPermissions),需藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證
* 配置以下兩個bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可實現此功能
*
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 開啓aop註解支持
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
九、補充
在學習shiro中查看了很多資料,博客,發現一些教學中會在ShiroConfig配置中添加如下配置
/**
* 配置Shiro生命週期處理器
*
* @return
*/
// @Bean(name = "lifecycleBeanPostProcessor")
// public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
// return new LifecycleBeanPostProcessor();
// }
但是,如果配合spring食用時,是無需配置此項的,可以查看
* shiro-spring-config.ShiroBeanConfiguration文件,此配置已經幫我們配置好了
十、測試頁面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登錄</title>
</head>
<body>
<table>
<tr>
<td>請輸入姓名</td>
<td><input type="text" name="username" id="username" /></td>
</tr>
<tr>
<td>請輸入密碼</td>
<td><input type="password" name="password" id="password" /></td>
</tr>
<tr>
<td>記住我</td>
<td><input type="checkbox" name="rememberMe" id="rememberMe" /></td>
</tr>
</table>
<button type="button" onclick="login()">登錄</button>
<button type="button" onclick="loginout()">登出</button>
<table>
<tr>
<td><button onclick="authority('/admin/可以訪問admin')">admin 管理員接口</button></td>
<td><button onclick="authority('/consumer/可以訪問consumer')">consumer 用戶接口</button></td>
<td><button onclick="authority('/guest/可以訪問guest')">guest 訪客接口</button></td>
</tr>
</table>
<textarea rows="5" cols="60" id="textarea"></textarea>
<script src="https://cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
</body>
<script type="text/javascript">
function login() {//登錄
var username = $('#username').val();
var password = $('#password').val();
var flag = $('#rememberMe').prop('checked');
console.log(flag)
$.ajax({
url : '/login',
data : {
'username' : username,
'password' : password,
'rememberMe' : flag
},
success : function(res) {
console.log(res)
$('#textarea').val(JSON.stringify(res));
}
})
}
function loginout() {//登出
$.ajax({
url : '/loginout',
success : function(res) {
$('#textarea').val(JSON.stringify(res));
}
})
}
//權限測試
function authority(url){
$.ajax({
url : url,
success : function(res) {
$('#textarea').val(JSON.stringify(res));
}
})
}
</script>
</html>
十一、測試結果
下面是這次的controller配置,可以看到
- admin可以訪問所有接口
- consumer可以訪問本身和guest接口
- guest只可以訪問自己的接口
進行測試
1.admin登錄,進入登錄認證方法,登錄成功!
2. 測試管理員接口,進入權限驗證方法,權限驗證通過
3. consumer接口測試,可以看到,因爲此接口設置了允許兩種角色訪問,shiro訪問了兩次權限認證方法
4. guest接口測試,可以看到,在沒有加權限註解時,shiro是不會訪問權限認證方法的
5. 換一個角色嘗試一下,首先登錄成功!
6. 訪問admin接口,得到無權限訪問,可以看到後臺進行權限驗證後拋出了 org.apache.shiro.authz.UnauthorizedException: Subject does not have role [admin] 權限驗證異常:缺少角色admin,成功攔截訪問!
7. 訪問自己的consumer接口,可以訪問,並且仍然進行了兩次權限認證方法,即使我在註解上面填寫所需角色爲or關係也不行,有點像 非短路與 的意思,這裏猜想如果填寫三個是不是訪問三次。。。。。。
8. 馬上更改代碼嘗試,在consumer接口新增可訪問角色‘superman’,直接點擊訪問執行了三次權限認證,猜想成功!
十二、源碼
如果需要源碼可以在下面地址得到,如果幫助到了你可以
."`".
.-./ _=_ \.-.
{ (,(oYo),) }}
{{ | " |} }
{ { \(---)/ }}
{{ }'-=-'{ } }
{ { }._:_.{ }}
{{ } -:- { } }
{_{ }`===`{ _}
((((\) (/))))