SpringSecurity之授權
1. 寫在前面的話
此文並非教程, 而是個人學習SpringSecurity時的記錄以及一些踩坑解決
如果能幫到大家, 那就讓人非常開心了
另外, SpringSecurity的授權分為web授權和方法授權, 本文只說明瞭web授權, 方法授權實際上就是SpringSecurity控制對方法和介面的訪問, 需要學習的小夥伴可以自行學習, 筆者在今後的工作中如果用到的話也會回來新增相關內容
好了, 讓我們看看筆者的一些學習心得吧
2. web授權
個人認為授權較之認證簡單了許多, 大概是本人在認證把該踩的坑都踩了一遍吧...
首先, 我們需要建立資料庫
1. 建庫
我們需要以下的幾張表
-
角色表
-
許可權表
-
使用者表(這個在認證中已經建立了)
-
角色許可權關聯表 (此處為管理員有1和2兩個許可權)
-
使用者角色關聯表
- 關聯表是為了方便擴充, 正常的業務都是這樣的, 存在一對多和多對多的關係
2. 新增查詢許可權的介面
許可權的資訊也是存放在UserDetails中的, 因此我們也要實現通過使用者id查詢使用者對應許可權的功能
-
建立對應的實體類(此處省略)
-
新增介面以及其實現類
-
dao
//根據使用者id查詢使用者許可權 List<PermissionDTO> getPermissionByUserId(String userId);
-
service介面
List<PermissionDTO> getPermissionByUserId(String userId);
-
介面實現類
@Override public List<PermissionDTO> getPermissionByUserId(String userId) { return userMapper.getPermissionByUserId(userId); }
-
-
在xml中新增查詢的sql
<!--根據使用者id查詢使用者許可權--> <select id="getPermissionByUserId" parameterType="string" resultType="permissionDTO"> select * from t_permission where `id` in ( select permission_id from t_role_permission where role_id = ( select role_id from t_user_role where user_id = #{userId} ) ) </select>
- 注意, 這裡我們使用了子查詢, 同時, 由於會查出多條結果, 我們要酌情考慮是否要使用 in 語句, 將多個結果放在select語句的條件中
3. 前端頁面的編寫
我們使用的是 layui的預設後端模板
-
模板的定義
先編寫一個後臺模板檔案
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <title>layout 後臺大布局 - Layui</title> <link rel="stylesheet" th:href="@{css/layui.css}"> </head> <body class="layui-layout-body"> <div class="layui-layout layui-layout-admin"> <div class="layui-header"> <div class="layui-logo">layui 後臺佈局</div> <!-- 頭部區域(可配合layui已有的水平導航) --> <ul class="layui-nav layui-layout-left"> <li class="layui-nav-item"><a href="">控制檯</a></li> <li class="layui-nav-item"><a href="">商品管理</a></li> <li class="layui-nav-item"><a href="">使用者</a></li> <li class="layui-nav-item"> <a href="javascript:;">其它系統</a> <dl class="layui-nav-child"> <dd><a href="">郵件管理</a></dd> <dd><a href="">訊息管理</a></dd> <dd><a href="">授權管理</a></dd> </dl> </li> </ul> <ul class="layui-nav layui-layout-right"> <li class="layui-nav-item"> <a href="javascript:;"> <img src="http://t.cn/RCzsdCq" class="layui-nav-img"> <span sec:authentication="principal.username"></span> </a> <dl class="layui-nav-child"> <dd><a href="">基本資料</a></dd> <dd><a href="">安全設定</a></dd> </dl> </li> <li class="layui-nav-item"><a id="logout" href="javascript:void(0);" onclick="logout()">退了</a></li> </ul> </div> <div class="layui-side layui-bg-black"> <div class="layui-side-scroll"> <!-- 左側導航區域(可配合layui已有的垂直導航) --> <ul class="layui-nav layui-nav-tree" lay-filter="test"> <li class="layui-nav-item layui-nav-itemed"> <a class="" href="javascript:;">所有商品</a> <dl class="layui-nav-child"> <dd><a href="javascript:;">列表一</a></dd> <dd><a href="javascript:;">列表二</a></dd> <dd><a href="javascript:;">列表三</a></dd> <dd><a href="">超連結</a></dd> </dl> </li> <li class="layui-nav-item"> <a href="javascript:;">解決方案</a> <dl class="layui-nav-child"> <dd><a href="javascript:;">列表一</a></dd> <dd><a href="javascript:;">列表二</a></dd> <dd><a href="">超連結</a></dd> </dl> </li> <li class="layui-nav-item"><a href="">雲市場</a></li> <li class="layui-nav-item"><a href="">釋出商品</a></li> </ul> </div> </div> <!-- <div class="layui-body">--> <!-- <!– 內容主體區域 –>--> <!-- <div style="padding: 15px;">內容主體區域</div>--> <!-- </div>--> <div class="layui-footer"> <!-- 底部固定區域 --> © layui.com - 底部固定區域 </div> </div> <script type="text/javascript" th:src="@{js/jquery.min.js}"></script> <script type="text/javascript" th:src="@{js/jquery-ui.min.js}"></script> <script type="text/javascript" th:src="@{js/jquery.mockjax.js}"></script> <script th:src="@{layui.js}"></script> <script> //JavaScript程式碼區域 layui.use('element', function () { var element = layui.element; }); function logout() { layui.use('layer', function () { //退出登入 layer.confirm('確定要退出麼?', {icon: 3, title: '提示'}, function (index) { //do something let url = '/logout'; $.ajax({ url: url, type: "post", dataType: "json", contentType: "application/json;charset=utf-8", success: function (data) { alert("進入success---"); let code = data.code; let url = data.url; let msg = data.msg; if (code == 203) { alert(msg); window.location.href = url; } else { alert("未知錯誤!"); } }, error: function (xhr, textStatus, errorThrown) { alert("進入error---"); alert("狀態碼:" + xhr.status); alert("狀態:" + xhr.readyState); //當前狀態,0-未初始化,1-正在載入,2-已經載入,3-資料進行互動,4-完成。 alert("錯誤資訊:" + xhr.statusText); alert("返回響應資訊:" + xhr.responseText);//這裡是詳細的資訊 alert("請求狀態:" + textStatus); alert(errorThrown); alert("請求失敗"); } }); layer.close(index); }); }); } </script> </body> </html>
- 注意
- 我們為了獲得SpringSecurity中的使用者名稱, 使用了SpringSecurity的thymeleaf方言, 可以實現細粒度的控制(依據許可權是否顯示某些標籤)
- 引入名稱空間, 這樣就有程式碼提示了(不引入其實也無所謂~) xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
- 注意
-
測試用頁面
為了測試許可權, 我們定義了三個按鈕, 分別對應三個許可權(我們在下一節的SpringSecurity配置類中可以看到)
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>頁面</title> </head> <body class="layui-layout-body"> <div th:include="content/layout"></div> <div class="layui-layout layui-layout-admin"> <div class="layui-body"> <!-- 內容主體區域 --> <div style="padding: 15px;">成功應用模板!</div> <div class="layui-btn-container"> <button type="button" class="layui-btn" onclick="btnClick1()">按鈕一</button> <button type="button" class="layui-btn" onclick="btnClick2()">按鈕二</button> <button type="button" class="layui-btn" onclick="btnClick3()">按鈕三</button> </div> </div> </div> <script> $.ajaxSetup({ type: "post", dataType: "json", contentType: "application/json;charset=utf-8", success: function (data) { let code = data.code; let url = data.url; let msg = data.msg; if (code == 204) { alert(msg); window.location.href = url; } else { alert("未知錯誤!"); } }, error: function (xhr, textStatus, errorThrown) { alert("進入error---"); alert("狀態碼:" + xhr.status); alert("狀態:" + xhr.readyState); //當前狀態,0-未初始化,1-正在載入,2-已經載入,3-資料進行互動,4-完成。 alert("錯誤資訊:" + xhr.statusText); alert("返回響應資訊:" + xhr.responseText);//這裡是詳細的資訊 alert("請求狀態:" + textStatus); alert(errorThrown); alert("請求失敗"); } }); function btnClick1() { let url = "/toR1"; $.ajax({ url: url, type: "post", dataType: "json", contentType: "application/json;charset=utf-8", }); } function btnClick2() { let url = "/toR2"; $.ajax({ url: url, type: "post", dataType: "json", contentType: "application/json;charset=utf-8", }); } function btnClick3() { let url = "/toR3"; $.ajax({ url: url, type: "post", dataType: "json", contentType: "application/json;charset=utf-8", }); } </script> </body> </html>
4. SpringSecurity配置
//授權
http
.authorizeRequests()
.antMatchers("/r/r1").hasAnyAuthority("p1")
.antMatchers("/r/r2").hasAnyAuthority("p2")
.antMatchers("/r/r3").access("hasAuthority('p1') and hasAuthority('p2')")
.antMatchers("/r/**").authenticated().anyRequest().permitAll();
在配置類中配置授權的規則
注意
- Authority和Role都是角色管理, 區別是Role會加一個 ROLE_ 字首, 具體的區別可以在網上自行檢視, 一般來說, 我們使用Authority就行了
5. Controller配置
- RestController處理AJAX
package com.wang.spring_security_framework.controller;
import com.alibaba.fastjson.JSON;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class RestfulJumpController {
@RequestMapping("/toR1")
public String toR1Page() {
String url = "/r/r1";
String code = "204";
String msg = "即將跳轉!";
Map<String, String> resultMap = new HashMap<>();
resultMap.put("url", url);
resultMap.put("code", code);
resultMap.put("msg", msg);
return JSON.toJSONString(resultMap);
}
@RequestMapping("/toR2")
public String toR2Page() {
String url = "/r/r2";
String code = "204";
String msg = "即將跳轉!";
Map<String, String> resultMap = new HashMap<>();
resultMap.put("url", url);
resultMap.put("code", code);
resultMap.put("msg", msg);
return JSON.toJSONString(resultMap);
}
@RequestMapping("/toR3")
public String toR3Page() {
String url = "/r/r3";
String code = "204";
String msg = "即將跳轉!";
Map<String, String> resultMap = new HashMap<>();
resultMap.put("url", url);
resultMap.put("code", code);
resultMap.put("msg", msg);
return JSON.toJSONString(resultMap);
}
}
我們用RestController處理了Ajax的跳轉請求, 並返回了JSON資訊, 讓前端進行url跳轉
- 測試的授權Controller
這裡的Controller用於顯示對應的資源目錄下的一些內容, 其中我們從會話中獲取了當前登入的使用者名稱
package com.wang.spring_security_framework.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/r")
public class AuthorizeTestController {
//從會話中獲取當前登入使用者名稱
private String getUserName(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//未登入, 返回null
if(!authentication.isAuthenticated()) {
return null;
}
Object principal = authentication.getPrincipal();
String username;
if (principal instanceof UserDetails) {
username = ((UserDetails) principal).getUsername();
} else {
username = principal.toString();
}
return username;
}
@RequestMapping("/r1")
public String R1() {
return getUserName() + "訪問資源1";
}
@RequestMapping("/r2")
public String R2() {
return getUserName() + "訪問資源2";
}
@RequestMapping("/r3")
public String R3() {
return getUserName() + "訪問資源3";
}
}
3. 結語
本篇看起來很簡單, 事實上也確實很簡單......
主要需要理解的是資料庫的建立以及關聯表的處理, 其中的SQL語句才是最難搞的
訪問沒有授權的頁面, 會爆出 405 錯誤, 我們可以自定義錯誤頁面來處理這種錯誤(此處就不敘述了, 屬於SpringBoot的內容~)
歡迎小夥伴批評與交流~