SpringBoot&Shiro實現許可權管理
SpringBoot&Shiro實現許可權管理
引言
相信大家前來看這篇文章的時候,是有SpringBoot和Shiro基礎的,所以本文只介紹整合的步驟,如果哪裡寫的不好,懇請大家能指出錯誤,謝謝!依賴以及一些配置檔案請在原始碼裡參考,請參見 https://github.com/Slags/springboot-learn/tree/master/1.springboot-shiro-authentication ,
個人部落格:www.fqcoder.cn
一、資料庫模板設計
在本文中,我們使用RBAC(Role-Based Access Control,基於角色的訪問控制)模型設計使用者,角色和許可權間的關係。簡單地說,一個使用者擁有若干角色,每一個角色擁有若干許可權。這樣,就構造成“使用者-角色-許可權”的授權模型。在這種模型中,使用者與角色之間,角色與許可權之間,一般者是多對多的關係。如下圖所示:
然後我們在來根據這個模型圖,設計資料庫表,記得自己新增一點測試資料哦
CREATE TABLE `tb_permission` ( `id` int(11) NOT NULL AUTO_INCREMENT, `url` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact; CREATE TABLE `tb_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '角色名稱', `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '描述', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact; CREATE TABLE `tb_role_permission` ( `role_id` int(11) NOT NULL COMMENT '角色id', `permission_id` int(11) NOT NULL COMMENT '許可權id' ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact; CREATE TABLE `tb_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, `create_time` datetime(0) DEFAULT NULL, `status` int(10) DEFAULT NULL COMMENT '狀態', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact; CREATE TABLE `tb_user_role` ( `role_id` int(11) NOT NULL COMMENT '角色id', `user_id` int(11) NOT NULL COMMENT '使用者id' ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;
二、Pojo設計
我們建立對應的類,筆者這裡用了lombok外掛,記得先安裝外掛
@Data public class User implements Serializable { private Integer id; private String username; private String password; private Date createTime; private Integer status; } @Data public class Role implements Serializable { private Integer id; private String name; private String description; } @Data public class Permission implements Serializable { private Integer id; private String url; private String name; }
三、Dao層設計
因為我們只是做一個演示,只涉及到使用者登入,使用者角色、許可權查詢,並未實現過多方法
建立UserMapper
、RolePermissionMapper
、UserRoleMapper
三個介面
注意:記得在Mapper介面上面加一個掃描註解@Mapper或者在boot啟動類上加一個@MapperScan(value = "mapper包路徑")註解
public interface UserMapper {
@Select("select * from tb_user where username=#{username}")
User selectByName(String username);
}
--------------------------
public interface UserRoleMapper {
/**
*
* 查詢使用者角色(可能一個使用者有多個角色)
* @param username
* @return
*/
@Select("select r.id,r.name,r.description from tb_role r " +
"left join tb_user_role ur on(r.id = ur.role_id)" +
"left join tb_user u on(u.id=ur.user_id)" +
"where u.username =#{username}")
List<Role> findByUserName(String username);
}
------------------------------------------------
public interface RolePermissionMapper {
/**
* 通過角色id查詢許可權
* @param roleId
* @return
*/
@Select("select p.id,p.url,p.name from tb_permission p " +
"left join tb_role_permission rp on(p.id=rp.permission_id)" +
"left join tb_role r on(r.id=rp.role_id)" +
"where r.id=#{roleId}")
List<Permission> findByRoleId(Integer roleId);
}
四、Shiro整合實現思路
好了,前面的一些東西,都是可以算是準備工作,現在才是真正開始整合Shiro了,我們先來屢一下思路,實現認證許可權功能主要可以歸納為3點:
1.定義一個ShiroConfig配置類,配置 SecurityManager Bean , SecurityManager為Shiro的安全管理器,管理著所有Subject;
2.在ShiroConfig中配置 ShiroFilterFactoryBean ,它是Shiro過濾器工廠類,依賴SecurityManager ;
3.自定義Realm實現類,包含 doGetAuthorizationInfo()
和doGetAuthenticationInfo()
方法 ,
五、定義ShiroConfig配置類
/**
* @ClassName ShiroConfig
* @Description TODO
* @Author fqCoder
* @Date 2020/2/29 3:08
* @Version 1.0
*/
@Configuration
public class ShiroConfig {
/**
* 這是shiro的大管家,相當於mybatis裡的SqlSessionFactoryBean
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(org.apache.shiro.mgt.SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//登入
shiroFilterFactoryBean.setLoginUrl("/login");
//首頁
shiroFilterFactoryBean.setSuccessUrl("/index");
//錯誤頁面,認證不通過跳轉
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
//頁面許可權控制
shiroFilterFactoryBean.setFilterChainDefinitionMap(ShiroFilterMapFactory.shiroFilterMap());
shiroFilterFactoryBean.setSecurityManager(securityManager);
return shiroFilterFactoryBean;
}
/**
* web應用管理配置
* @param shiroRealm
* @param cacheManager
* @param manager
* @return
*/
@Bean
public DefaultWebSecurityManager securityManager(Realm shiroRealm, CacheManager cacheManager, RememberMeManager manager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setCacheManager(cacheManager);
securityManager.setRememberMeManager(manager);//記住Cookie
securityManager.setRealm(shiroRealm);
securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* session過期控制
* @return
* @author fuce
* @Date 2019年11月2日 下午12:49:49
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager defaultWebSessionManager=new DefaultWebSessionManager();
// 設定session過期時間3600s
Long timeout=60L*1000*60;//毫秒級別
defaultWebSessionManager.setGlobalSessionTimeout(timeout);
return defaultWebSessionManager;
}
/**
* 加密演算法
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");//採用MD5 進行加密
hashedCredentialsMatcher.setHashIterations(1);//加密次數
return hashedCredentialsMatcher;
}
/**
* 記住我的配置
* @return
*/
@Bean
public RememberMeManager rememberMeManager() {
Cookie cookie = new SimpleCookie("rememberMe");
cookie.setHttpOnly(true);//通過js指令碼將無法讀取到cookie資訊
cookie.setMaxAge(60 * 60 * 24);//cookie儲存一天
CookieRememberMeManager manager=new CookieRememberMeManager();
manager.setCookie(cookie);
return manager;
}
/**
* 快取配置
* @return
*/
@Bean
public CacheManager cacheManager() {
MemoryConstrainedCacheManager cacheManager=new MemoryConstrainedCacheManager();//使用記憶體快取
return cacheManager;
}
/**
* 配置realm,用於認證和授權
* @param hashedCredentialsMatcher
* @return
*/
@Bean
public AuthorizingRealm shiroRealm(HashedCredentialsMatcher hashedCredentialsMatcher) {
MyShiroRealm shiroRealm = new MyShiroRealm();
//校驗密碼用到的演算法
// shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher);
return shiroRealm;
}
/**
* 啟用shiro方言,這樣能在頁面上使用shiro標籤
* @return
*/
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
/**
* 啟用shiro註解
*加入註解的使用,不加入這個註解不生效
*/
@Bean
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
}
注意:(當時筆者遇到的一個小問題,貼出來給大家漲姿勢)
註解無效,登入時不會執行驗證角色和許可權的方法,只會執行登入驗證方法,遂查詢資料,得知shiro在subject.login(token)方法時不會執行doGetAuthorizationInfo方法,只有在訪問到有許可權驗證的介面時會呼叫檢視許可權,於是猜想註解無效,發現shiro的許可權註解需要開啟才能有用,新增在配置檔案中加入
advisorAutoProxyCreator
和getAuthorizationAttributeSourceAdvisor
兩個bean開啟shiro註解,解決問題。
六.建立ShiroFilterMapFactory類
注意:
1.這裡要用LinkedHashMap 保證有序
2.filterChain基於短路機制,即最先匹配原則,
3.像anon、authc等都是Shiro為我們實現的過濾器,我給出了一張表,在文章尾附錄,自行檢視
/**
* @ClassName ShiroFilterMapFactory
* @Description TODO
* @Author fqCoder
* @Date 2020/2/29 3:09
* @Version 1.0
*/
public class ShiroFilterMapFactory {
public static Map<String, String> shiroFilterMap() {
// 設定路徑對映,注意這裡要用LinkedHashMap 保證有序
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//對所有使用者認證
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/logout", "logout");
//對所有頁面進行認證
filterChainDefinitionMap.put("/**", "authc");
return filterChainDefinitionMap;
}
}
配置完了ShiroConfig後,實現自己的Realm,然後注入到SecurityManager裡
七、實現自定義Realm類
自定義Realm類需要繼承 AuthorizingRealm 類,實現 doGetAuthorizationInfo()和doGetAuthenticationInfo()方法即可 ,
doGetAuthorizationInfo() 方法是進行授權的方法,獲取角色的許可權資訊
doGetAuthenticationInfo()方法是進行使用者認證的方法,驗證使用者名稱和密碼
/**
* @ClassName MyShiroRealm
* @Description TODO
* @Author fqCoder
* @Date 2020/2/29 3:08
* @Version 1.0
*/
@Service
public class MyShiroRealm extends AuthorizingRealm {
@Autowired
private UserMapper userMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Autowired
private RolePermissionMapper rolePermissionMapper;
/**
* 獲取使用者角色和許可權
* @param principal
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
if(principal == null){
throw new AuthorizationException("principals should not be null");
}
User userInfo= (User) SecurityUtils.getSubject().getPrincipal();
System.out.println("使用者-->"+userInfo.getUsername()+"獲取許可權中");
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//使用者獲取角色集
List<Role> roleList=userRoleMapper.findByUserName(userInfo.getUsername());
Set<String> roleSet=new HashSet<>();
for (Role r:roleList){
Integer roleId=r.getId();//獲取角色id
simpleAuthorizationInfo.addRole(r.getName());//新增角色名字
List<Permission> permissionList=rolePermissionMapper.findByRoleId(roleId);
for (Permission p:permissionList){
//新增許可權
simpleAuthorizationInfo.addStringPermission(p.getName());
}
}
System.out.println("角色為-> " + simpleAuthorizationInfo.getRoles());
System.out.println("許可權為-> " + simpleAuthorizationInfo.getStringPermissions());
return simpleAuthorizationInfo;
}
/**
* 登入認證
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//獲取使用者輸入的使用者名稱密碼
String username= (String) token.getPrincipal();
String password=new String((char[])token.getCredentials());
System.out.println("使用者輸入--->username:"+username+"-->password:"+password);
//在資料庫中查詢
User userInfo=userMapper.selectByName(username);
if (userInfo == null) {
throw new UnknownAccountException("使用者名稱或密碼錯誤!");
}
if (!password.equals(userInfo.getPassword())) {
throw new IncorrectCredentialsException("使用者名稱或密碼錯誤!");
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
userInfo, // 使用者名稱
userInfo.getPassword(), // 密碼
getName() // realm name
);
return authenticationInfo;
}
}
其中UnknownAccountException
等異常為Shiro自帶異常,Shiro具有豐富的執行時AuthenticationException
層次結構,可以準確指出嘗試失敗的原因。
八、控制層設計
1.建立一個LoginController.class類
用來處理登入訪問請求
/**
* @ClassName LoginController
* @Description TODO
* @Author fqCoder
* @Date 2020/2/29 6:06
* @Version 1.0
*/
@Controller
public class LoginController {
@GetMapping("/login")
public String login(){
return "login";
}
@GetMapping("/")
public String home(){
return "redirect:/index";
}
@GetMapping("/index")
public String index(Model model){
User user= (User) SecurityUtils.getSubject().getPrincipal();
model.addAttribute("user",user);
return "index";
}
@PostMapping("login")
@ResponseBody
public AjaxResult login(User user,Boolean rememberMe){
System.out.println("user = " + user);
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
//獲取Subject 物件
Subject subject= SecurityUtils.getSubject();
try {
if (rememberMe){
token.setRememberMe(true);
}
subject.login(token);
return AjaxResult.success("/index");
} catch (UnknownAccountException e) {
return AjaxResult.error(e.getMessage());
} catch (IncorrectCredentialsException e) {
return AjaxResult.error(e.getMessage());
}
}
@GetMapping("/403")
public String forbid(){
return "403";
}
}
2.建立一個UserController.class類
用於處理User類的訪問請求,並使用Shiro許可權註解控制權限:
/**
* @ClassName UserController
* @Description TODO
* @Author fqCoder
* @Date 2020/3/3 15:14
* @Version 1.0
*/
@RestController
@RequestMapping("/user")
public class UserController {
@RequiresPermissions("user:queryAll")
@GetMapping("/queryAll")
public String queryAll(){
//只演示框架...功能不實現
return "查詢列表";
}
@RequiresPermissions("user:add")
@GetMapping("/add")
public String userAdd(){
return "新增使用者";
}
@RequiresPermissions("user:delete")
@GetMapping("/delete")
public String userDelete(){
return "刪除使用者";
}
}
九、前端頁面設計
1.編寫login.html頁面
這裡我只貼重要程式碼,具體的程式碼,到這裡找哦!
<form id="loginForm">
<input type="text" id="username" name="username" class="text" />
<input type="password" id="password" name="password" />
</form>
<div class="signin">
<input id="loginBut" type="button" value="Login" >
</div>
-------js程式碼----
<script type="text/javascript">
$.fn.serializeObject = function () {
var o = {};
var a = this.serializeArray();
$.each(a, function () {
if (o[this.name]) {
if (!o[this.name].push) {
o[this.name] = [o[this.name]];
}
o[this.name].push(this.value);
} else {
o[this.name] = this.value || '';
}
});
return o;
};
$(function () {
$("#loginBut").click(function () {
var arr=$('#loginForm').serializeObject();
$.ajax({
url: '/login',
type: 'post',
data: arr,
dataType: "json",
success: function (data) {
if (data.code==200){
location.href=data.msg;
} else {
alert(data.msg);
}
},
error: function (data) {
alert(data.msg);
}
})
});
});
</script>
當用戶登入進來的時候調到index.html
2.編寫index.html頁面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首頁</title>
</head>
<body>
<h1>番茄歡迎您!</h1>
登入使用者:【[[${user.username}]]】
<a th:href="@{/logout}">登出</a>
<h2>許可權測試</h2>
<a th:href="@{/user/queryAll}">獲取使用者全部資訊</a>
<a th:href="@{/user/add}">新增使用者</a>
<a th:href="@{/user/delete}">刪除使用者</a>
</body>
</html>
3.編寫403頁面
比較簡單,此處能用就行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>403</title>
</head>
<body>
<h1>403許可權不夠</h1>
<a href="/index">首頁</a>
</body>
</html>
十、測試&問題
啟動專案:訪問http://localhost:8080/,它會自動攔截,頁面重定向到 http://localhost:8080/login ,登入成功跳轉到http://localhost:8080/index
問題:
登入測試使用者的時候,訪問沒有許可權的連結請求時,後臺丟擲org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method
異常
當時以為在ShiroConfig配置類中配置了shiroFilterFactoryBean.setUnauthorizedUrl("/403");
沒有許可權的請求會自動從定向到/403,然後卻是丟擲了異常,後來在一篇文章中看到了,說這個設定只對filterChain起作用 ,針對這個問題,我們可以定義一個全域性異常捕獲類:
@ControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler {
@ExceptionHandler(value = AuthorizationException.class)
public String handleAuthorizationException() {
return "403";
}
}
然後再啟動專案,登入測試賬號,訪問沒有許可權的請求,頁面成功定向到/403
原始碼連結: https://github.com/Slags/springboot-learn/tree/master/1.springboot-shiro-authentication
至此,筆者剛開始寫,不是寫的很好,歡迎各位網友踴躍指出不足,謝謝!
附錄:
1.Shiro攔截機制表
Filter Name | Class | Description |
---|---|---|
anon | org.apache.shiro.web.filter.authc.AnonymousFilter | 匿名攔截器,即不需要登入即可訪問;一般用於靜態資源過濾;示例/static/**=anon |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter | 基於表單的攔截器;如/**=authc ,如果沒有登入會跳到相應的登入頁面登入 |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter | Basic HTTP身份驗證攔截器 |
logout | org.apache.shiro.web.filter.authc.LogoutFilter | 退出攔截器,主要屬性:redirectUrl:退出成功後重定向的地址(/),示例/logout=logout |
noSessionCreation | org.apache.shiro.web.filter.session.NoSessionCreationFilter | 不建立會話攔截器,呼叫subject.getSession(false) 不會有什麼問題,但是如果subject.getSession(true) 將丟擲DisabledSessionException 異常 |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter | 許可權授權攔截器,驗證使用者是否擁有所有許可權;屬性和roles一樣;示例/user/**=perms["user:create"] |
port | org.apache.shiro.web.filter.authz.PortFilter | 埠攔截器,主要屬性port(80) :可以通過的埠;示例/test= port[80] ,如果使用者訪問該頁面是非80,將自動將請求埠改為80並重定向到該80埠,其他路徑/引數等都一樣 |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter | rest風格攔截器,自動根據請求方法構建許可權字串;示例/users=rest[user] ,會自動拼出user:read,user:create,user:update,user:delete許可權字串進行許可權匹配(所有都得匹配,isPermittedAll) |
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter | 角色授權攔截器,驗證使用者是否擁有所有角色;示例/admin/**=roles[admin] |
ssl | org.apache.shiro.web.filter.authz.SslFilter | SSL攔截器,只有請求協議是https才能通過;否則自動跳轉會https埠443;其他和port攔截器一樣; |
user | org.apache.shiro.web.filter.authc.UserFilter | 使用者攔截器,使用者已經身份驗證/記住我登入的都可;示例/**=user |