Shiro權限控制
0.1傳統的權限認證方式
特點:爲每個人單獨的分配權限模塊,能夠實現權限控制,但是當公司人員龐大之後,非常難管理
上述權限控制如何設計表?
關係:員工和菜單權限的關係:多對多
員工id | 菜單名稱 |
---|---|
1 | 取派管理 |
2 | 快遞員管理 |
2 | 運單管理 |
好處:可以方便的 實現權限控制
缺陷:比如當修改權限的時候,公司統一的給組長級別的人 加一個“計算工資”權限,這時候,得修改權限表中所有組長的權限,每個組長在數據庫中都得增加一條“計算工資”記錄的權限
後來,這個“計算工資”的功能,在給組長之後,發現,這個權限不合適,得收回這個權限,這個時候,需要刪除多條記錄
0.2 RBAC認證方式:
Role Based Access Controller :基於角色的訪問控制
前無古人後無來者
0.3 RBAC認證方式下的數據庫設計
數據庫設計:
1. 權限控制
1.1 概述
1.1.1 什麼是認證和授權
- 認證和授權,控制項目資源的訪問。
- 認證:先進行認證。例如:是否是QQ會員-----在程序中指的就是:登錄
- 授權:再進行授權。例如:QQ會員級別(級別不同權限不同)-------對菜單的訪問控制
1.1.2 權限控制的解決方案
- 方式1:自定義實現
- 自己如何實現一個認證?寫一個Filter過濾器
- 方式2:採用框架,例如:shiro、Spring Security 等
- Shiro:輕量、簡單,apache
- Spring Security:輕量、簡單,Spring
1.1.3 Shiro概述
Apache Shiro是一個強大且易用的Java安全框架,執行身份驗證、授權、密碼和會話管理。使用Shiro的易於理解的API,您可以快速、輕鬆地獲得任何應用程序,從最小的移動應用程序到最大的網絡和企業應用程序。
官網:http://shiro.apache.org/
Authentication:身份認證/登錄,驗證用戶是不是擁有相應的身份;
Authorization:授權,即權限驗證,驗證某個已認證的用戶是否擁有某個權限;即判斷用戶是否能做事情,常見的如:驗證某個用戶是否擁有某個角色。或者細粒度的驗證某個用戶對某個資源是否具有某個權限;
Session Manager:會話管理,即用戶登錄後就是一次會話,在沒有退出之前,它的所有信息都在會話中;會話可以是普通JavaSE環境的,也可以是如Web環境的;
Cryptography:加密,保護數據的安全性,如密碼加密存儲到數據庫,而不是明文存儲;
Web Support:Web支持,可以非常容易的集成到Web環境;
Caching:緩存,比如用戶登錄後,其用戶信息、擁有的角色/權限不必每次去查,這樣可以提高效率;
Concurrency:shiro支持多線程應用的併發驗證,即如在一個線程中開啓另一個線程,能把權限自動傳播過去;
Testing:提供測試支持;
Run As:允許一個用戶假裝爲另一個用戶(如果他們允許)的身份進行訪問;
Remember Me:記住我,這個是非常常見的功能,即一次登錄後,下次再來的話不用登錄了。
1.1.4 三個核心組件
- 三個核心組件:Subject, SecurityManager 和 Realms.
- Subject,主體,即“當前操作用戶”。Subject代表了當前用戶的安全操作。(需要被認證的對象)
- SecurityManager:它是Shiro框架的核心,管理所有用戶的安全操作,並提供安全管理的各種服務。
- Realm: Realm充當了Shiro與應用安全數據間的“橋樑”或者“連接器”。也就是說,當對用戶執行認證(登錄)和授權(訪問控制)驗證時,Shiro會從應用配置的Realm中查找用戶及其權限信息。
1.1.5 傳統的登錄和shiro登錄的比較
shiro的使用不依賴Spring框架,javase可以用,javaee也可以用,移動應用程序可以用,大型的網絡和企業應用程序也可以使用Shiro。
傳統登錄方式:
Shiro安全框架實現登錄
傳統的方式,客戶端發出請求給Controller,Controller接受用戶的用戶名和密碼,然後由Controller調用業務邏輯,在Controller中調用Service,然後service負責調用數據庫進行處理,如果用戶名和密碼正確,則將用戶信息保存到session中,並且進入主頁面
如果用戶名和密碼錯誤,則保存錯誤信息,然後將信息輸出到客戶端。
答:客戶端發送請求到Controller,Controller接受用戶的用戶名和密碼,第一步還是一樣的,但是第二步不一樣了,以前第二步是Controller直接調用Service處理,現在Controller調用Shiro安全框架去處理,也就是說將認證授權抽取出來,有一個框架專門爲你做認證做授權,這裏有框架去幫我們完成認證和授權,然後告訴你這個用戶名和密碼是否可用還是不可用,當然此時,認證成功之後,只要從shiro中取出認證的結果,如果成功的話,將用戶保存至session中,然後在跳轉頁面
這個過程相比於早期的操作,相當於驗證用戶名和密碼的業務邏輯交給shiro安全框架來做。並且加密也交給shiro安全框架來做,然後由shiro安全框架來加密,由shiro拿密文與數據庫中的密文進行比較。
總結一下:shiro就是一個安全框架,幫助我們解決認證、授權、加密和密碼比較的過程。
1.2 整合shiro
步驟1:在common-parent項目中,添加座標
<!--shiro start-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!--shiro end-->
使用Shiro時,需要配置的相關權限過濾器如下(共10個):
anon: 匿名過濾器,未登陸也可以訪問
authc: 認證過濾器, 登陸後訪問
perms : 需要xx權限,才能訪問
roles: 需要xx角色,才能訪問
user: 需要xx用戶,才能訪問
port:指定端口才能訪問
ssl:必須使用https協議才能訪問
logout :登出功能
rest :根據指定HTTP請求訪問才能訪問 ,get方式提交 或者 post方式提交才能訪問
可
shiro的配置步驟
1 配置安全管理器SecurityManager
2 realm域配置:由於SecurityManger需要使用realm域,涉及到用戶信息、權限信息,處理用戶信息的時候需要加密
3 密碼比較器:用戶輸入的銘文進行加密,並且與數據庫中的密文進行比較
4 配置生成過濾器的工廠類
根據配置文件報錯顯示,需要創建如下文件:
1 Realm域
2 CredentialsMatcher密碼比較器
//自定義Realm ,實現安全數據 連接
public class BosRealm extends AuthorizingRealm {
// 認證...
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) {
System.out.println("shiro 認證管理... ");
return null;
}
@Override
// 授權...
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection pc) {
System.out.println("shiro 授權管理...");
return null;
}
}
public class BosCredentialsMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
System.out.println("密碼比較器");
return false;
}
}
1.2.6 實現認證方法
//自定義Realm ,實現安全數據 連接
public class BosRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
// 認證...
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) {
System.out.println("shiro 認證管理... ");
//1 用戶信息
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//2 通過username從數據庫中查找 User對象,如果找到,沒找到.
User user = userService.findUserByUsername(upToken.getUsername());
if(user == null){
//返回null表示賬號不存在
return null;
}
return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
}
@Override
// 授權...
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection pc) {
System.out.println("shiro 授權管理...");
return null;
}
}
1.2.7編寫加密工具類
public class Encrypt {
/*
* 散列算法一般用於生成數據的摘要信息,是一種不可逆的算法,一般適合存儲密碼之類的數據,
* 常見的散列算法如MD5、SHA等。一般進行散列時最好提供一個salt(鹽),比如加密密碼“admin”,
* 產生的散列值是“21232f297a57a5a743894a0e4a801fc3”,
* 可以到一些md5解密網站很容易的通過散列值得到密碼“admin”,
* 即如果直接對密碼進行散列相對來說破解更容易,此時我們可以加一些只有系統知道的干擾數據,
* 如用戶名和ID(即鹽);這樣散列的對象是“密碼+用戶名+ID”,這樣生成的散列值相對來說更難破解。
*/
//高強度加密算法,不可逆
public static String md5(String password, String salt){
return new Md5Hash(password,salt,2).toString();
}
public static void main(String[] args) {
/**
* new Md5Hash("123456","lisi",1) :1b539b60601b934441308049a9526e7d
* new Md5Hash("123456","lisi",2) :42bd4e7685cb11d3ba02716c313cb04b
* new Md5Hash("123456","lisi",3) :16f807d62105b4896034552ee5caeb8a
* new Md5Hash("123456","KMNO4",3):8bd35dc14dc07f756478bb44513694f6
*/
//System.out.println(new Md5Hash("123456","KMNO4",3).toString());
/**
* sha家族加密算法
* sha1:aca1eb31d2dcf8f1fcf3fd7a7104232785afad41 40 位
* sha256: 616a47d8e1e42f23693bb3a85749bf18d4b6e5380ddfd5717aafa61e33d5211e
* sha384:84f5cbb18e2d9f1c81b8cec6f443a2b229993689a2ebae97db37e13af1dfb00ec6168713a53fe19d33a63d4d30889553
* sha512:c3e5102b6a7ec6caa5b255dae2895b11c2ef0c7b9bfea8e848653372b53f3ef665d96ea283a21eac683cc0fe5c4b1f64692c2056a8a9636ee1931151043d2b5d
*/
System.out.println("sha1:"+new Sha1Hash("123456","lisi",2));
System.out.println("sha256:"+new Sha256Hash("123456","lisi",2));
System.out.println("sha384:"+new Sha384Hash("123456","lisi",2));
System.out.println("sha512:"+new Sha512Hash("123456","lisi",2));
}
}
1.2.8 編寫密碼比較器
public class BosCredentialsMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
//向下轉型
UsernamePasswordToken upToken = (UsernamePasswordToken)token;
//獲取用戶頁面輸入的密碼
String pwd = new String(upToken.getPassword());
//加密
String newPwd =Encrypt.md5(pwd, upToken.getUsername()).toString();
//獲取數據庫密碼
String dbPwd = info.getCredentials().toString();
return equals(newPwd, dbPwd);
}
}
5 散列加密算法:加密的時候撒鹽
MD5:
SHA:
1.5.6 修改BosRealm
//自定義Realm ,實現安全數據 連接
public class BosRealm extends AuthorizingRealm {
@Resource
private UserService userService;
@Resource
private RoleService roleService;
@Resource
private PermissionService permissionService;
@Override
public String getName() {
return "bosRealm";
}
@Override
// 認證...
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
System.out.println("shiro 認證管理... ");
//1 獲得密碼
String username = (String)token.getPrincipal();
//2 通過username從數據庫中查找 User對象,如果找到,沒找到.
User user = userService.findUserByUsername(username);;
if(user == null){
//返回null表示賬號不存在
return null;
}
return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
}
@Override
// 授權...
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection pc) {
System.out.println("shiro 授權管理...");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 根據當前登錄用戶 查詢對應角色和權限
// User user = (User) SecurityUtils.getSubject().getPrincipal();
User user = (User) pc.getPrimaryPrincipal();
// 調用業務層,查詢角色
List<Role> roles = roleService.findByUser(user);
for (Role role : roles) {
authorizationInfo.addRole(role.getKeyword());
}
// 調用業務層,查詢權限
List<Permission> permissions = permissionService.findByUser(user);
for (Permission permission : permissions) {
authorizationInfo.addStringPermission(permission.getKeyword());
}
return authorizationInfo;
}
}
1.5.7 確定權限不足時,顯示未授權頁面
- 確定權限過濾器配置信息
- 確定頁面
//攔截器.
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
//配置退出 過濾器,其中的具體的退出代碼Shiro已經替我們實現了
filterChainDefinitionMap.put("/login*", "anon");
filterChainDefinitionMap.put("/validatecode.jsp*", "anon");
filterChainDefinitionMap.put("/user/login*", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/images/**", "anon");
filterChainDefinitionMap.put("/services/**", "anon");
filterChainDefinitionMap.put("/pages/base/courier**", "perms[courier:list]");
filterChainDefinitionMap.put("/pages/base/area**", "roles[base]");
//<!-- 過濾鏈定義,從上向下順序執行,一般將 /**放在最爲下邊 -->:這是一個坑呢,一不小心代碼就不好使了;
//<!-- authc:所有url都必須認證通過纔可以訪問; anon:所有url都都可以匿名訪問-->
filterChainDefinitionMap.put("/**", "authc");
// 如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面
shiroFilterFactoryBean.setLoginUrl("/login.html");
// 登錄成功後要跳轉的鏈接
shiroFilterFactoryBean.setSuccessUrl("/index.html");
//未授權界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized.html");
1.5.9 細粒度方法權限控制
需求:張三可以查看快遞員列表信息,但是無法添加快遞員
Shiro提供了若干註解用於在方法上進行權限控制
註解 | 描述 |
---|---|
@RequiresPermissions() | 用戶必須具有指定【權限】,纔可以訪問被註解修飾的方法。 |
@RequiresRoles() | 用戶必須具有指定【角色】,纔可以訪問被註解修飾的方法。 |
- 步驟1:在授權方法中獲取該用戶的所有的權限:包括role權限和Permission權限
- 步驟2:在運單 CourierController的添加方法上添加courier:list權限標識。
測試:無權限訪問
1.6 動態菜單
動態菜單: 不同用戶登錄後,應該看到不同菜單結構
1、 修改index.html 加載基本菜單 url路徑
2、 在MenuController 添加 showMenu方法
@RestController
@RequestMapping("/menu")
public class MenuController {
@Autowired
private MenuService menuService;
// 加載左側的菜單功能
@GetMapping(value = "/showMenu")
public ResponseEntity<List<Menu>> showMenu(){
// 調用業務層,查詢當前用戶具有菜單列表
Subject subject = SecurityUtils.getSubject();
User user = (User)subject.getPrincipal();
// 查詢菜單列表
List<Menu> result = menuService.findByUser(user);
return new ResponseEntity<List<Menu>>(result,HttpStatus.OK);
}
}
3、 編寫MenuService.java業務層
@Service
@Transactional
public class MenuService {
@Autowired
private MenuMapper menuMapper;
/**查詢用戶*/
public List<Menu> findByUser(User user) {
// 針對admin用戶顯示所有的菜單
if(user.getUsername().equals("admin")){
return menuMapper.selectAll();
}
else{
// 使用用戶ID,查詢當前用戶具有的菜單列表
return menuMapper.findByUser(user.getId());
}
}
}
4、調用DAO
@org.apache.ibatis.annotations.Mapper
public interface MenuMapper extends Mapper<Menu> {
@Select("select m.* from t_menu m,t_user u,t_user_role ur,t_role r,t_role_menu rm "
+ "where m.id = rm.menu_id and rm.role_id = r.id "
+ "and r.id = ur.role_id and ur.user_id = u.id "
+ "and u.id=#{id} order by m.priority")
List<Menu> findByUser(Integer id);
}