一套簡單的登入、鑑權工具
前言
無論是SpringSecruity、Shiro,對於一些小專案來說都太過複雜,有些情況下我們就想使用簡單的登入、鑑權功能,本文記錄手寫一套簡單的登入、鑑權工具
思路
1、封裝工具類,整合查詢系統使用者、系統角色,根據登入使用者許可權進行當前URL請求鑑權
2、在攔截器中呼叫工具類進行鑑權,通過放行、不通過則丟擲對應業務異常資訊
首先需要三張基礎表:系統使用者表、系統角色表、使用者角色關聯表
-- ---------------------------- -- Table structure for sys_user -- ----------------------------DROP TABLE IF EXISTS `sys_user`; CREATE TABLE `sys_user` ( `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '表id', `nick_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '暱稱', `user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULLDEFAULT NULL COMMENT '賬號', `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密碼', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '系統使用者表' ROW_FORMAT = Compact; -- ------------------------------ Records of sys_user -- ---------------------------- INSERT INTO `sys_user` VALUES ('1', '系統管理員', 'admin', '000000'); INSERT INTO `sys_user` VALUES ('2', '張三-部門經理', 'zhangsan', '111111'); INSERT INTO `sys_user` VALUES ('3', '小芳-前臺接待', 'xiaofang', '222222'); -- ---------------------------- -- Table structure for sys_role -- ---------------------------- DROP TABLE IF EXISTS `sys_role`; CREATE TABLE `sys_role` ( `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '表id', `role_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色名稱', `role_menu` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '角色選單可視許可權(可以不關聯選單,單獨做成選單管理直接與使用者關聯)', `role_url` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '角色URL訪問許可權', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '系統角色表' ROW_FORMAT = Compact; -- ---------------------------- -- Records of sys_role -- ---------------------------- INSERT INTO `sys_role` VALUES ('1', '管理員', '[{\"menuName\":\"系統管理\",\"menuPath\":\"/sys/xtgl\"},{\"menuName\":\"使用者管理\",\"menuPath\":\"/sys/yhgl\"},{\"menuName\":\"網站門戶管理\",\"menuPath\":\"/portal/mhgl\"}]', '/sys/*,/portal/mhgl,/getLoginUser'); INSERT INTO `sys_role` VALUES ('2', '部門領導', '[{\"menuName\":\"使用者管理\",\"menuPath\":\"/sys/yhgl\"},{\"menuName\":\"網站門戶管理\",\"menuPath\":\"/portal/mhgl\"}]', '/sys/yhgl,/portal/mhgl,/getLoginUser'); INSERT INTO `sys_role` VALUES ('3', '普通員工', '[{\"menuName\":\"網站門戶管理\",\"menuPath\":\"/portal/mhgl\"}]', '/portal/mhgl,/getLoginUser'); -- ---------------------------- -- Table structure for sys_user_role -- ---------------------------- DROP TABLE IF EXISTS `sys_user_role`; CREATE TABLE `sys_user_role` ( `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '表id', `user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '使用者id', `role_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色id', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '系統使用者-角色關聯表' ROW_FORMAT = Compact; -- ---------------------------- -- Records of sys_user_role -- ---------------------------- INSERT INTO `sys_user_role` VALUES ('1', '1', '1'); INSERT INTO `sys_user_role` VALUES ('2', '1', '2'); INSERT INTO `sys_user_role` VALUES ('3', '1', '3'); INSERT INTO `sys_user_role` VALUES ('4', '2', '2'); INSERT INTO `sys_user_role` VALUES ('5', '3', '3');
在工具類中定義三個實體類方便傳參接參(如果嫌麻煩也可以直接使用Map物件),使用自定義DbUtil查詢資料庫表資料(此操作,應交由專案ORM框架負責)
程式碼編寫
DbUtil工具類
package cn.huanzi.qch.util; import java.sql.*; import java.util.ArrayList; import java.util.HashMap; /** * 原生jdbc操作資料庫工具類 */ public class DbUtil { //資料庫連線:地址、使用者名稱、密碼 private final String url; private final String username; private final String password; //Connection連線例項 private Connection connection; public DbUtil(String url, String username, String password){ this.url = url; this.username = username; this.password = password; } public DbUtil(String url, String username, String password, String driver){ this(url,username,password); //載入驅動 try { /* 同時需要引入相關驅動依賴 1、MySQL: com.mysql.cj.jdbc.Driver 2、Oracle: oracle.jdbc.driver.OracleDriver 3、pgsql: org.postgresql.Driver */ Class.forName(driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } /** * 獲取 Connection 連線 */ private Connection getConnection() { if(connection == null){ try { connection= DriverManager.getConnection(url, username, password); connection.setAutoCommit(true); } catch (SQLException e) { System.err.println("獲取Connection連線異常..."); e.printStackTrace(); } } return connection; } /** * 設定是否自動提交事務 * 當需要進行批量帶事務的操作時,關閉自動提交手動管理事務,將會大大提高效率! */ public void setAutoCommit(boolean autoCommit){ try { this.getConnection().setAutoCommit(autoCommit); } catch (SQLException e) { e.printStackTrace(); } } /** * 關閉自動提交事務時,需要手動管理事務提交、回滾 */ public void commit(){ try { this.getConnection().commit(); } catch (SQLException e) { e.printStackTrace(); } } public void rollback(){ try { this.getConnection().rollback(); } catch (SQLException e) { e.printStackTrace(); } } /** * 關閉 Connection 連線 */ public void close(){ if(connection != null){ try { connection.close(); connection = null; } catch (SQLException e) { System.err.println("關閉Connection連線異常..."); e.printStackTrace(); } } } /** * 查詢 * 查詢語句 */ public ArrayList<HashMap<String,Object>> find(String sql, Object[] params) { ArrayList<HashMap<String, Object>> list = new ArrayList<>(); //獲取連線 Connection conn = this.getConnection(); PreparedStatement ps; ResultSet rs; try { //設定SQL、以及引數 ps = conn.prepareStatement(sql); if (params != null) { for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); } } //執行查詢 rs = ps.executeQuery(); //獲取查詢結果 ResultSetMetaData rm = rs.getMetaData(); int columnCount = rm.getColumnCount(); //封裝結果集 while (rs.next()) { HashMap<String, Object> map = new HashMap<>(columnCount); for (int i = 1; i <= columnCount; i++) { String name = rm.getColumnName(i).toLowerCase(); Object value = rs.getObject(i); map.put(name,value); } list.add(map); } } catch (Exception e) { System.err.println("執行 jdbcUtil.find() 異常..."); e.printStackTrace(); } return list; } public HashMap<String,Object> findOne(String sql, Object[] params){ ArrayList<HashMap<String, Object>> list = this.find(sql, params); return list.size() > 0 ? list.get(0) : null; } public ArrayList<HashMap<String,Object>> find(String sql) { return this.find(sql,null); } public HashMap<String,Object> findOne(String sql) { return this.findOne(sql,null); } /** * 執行 * 新增/刪除/更新 等SQL語句 */ public boolean execute(String sql, Object[] params){ boolean flag = false; //獲取連線 Connection conn = this.getConnection(); PreparedStatement ps; try { //設定SQL、以及引數 ps = conn.prepareStatement(sql); if (params != null) { for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); } } //執行 flag = ps.execute(); } catch (SQLException e) { System.err.println("執行 jdbcUtil.update() 異常..."); e.printStackTrace(); } return flag; } public boolean execute(String sql){ return this.execute(sql,null); } }
SecurityUtil工具類
package cn.huanzi.qch.util; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; /** * 一套簡單的登入、鑑權工具 */ public class SecurityUtil { /** * 單例模式-餓漢 */ private static final SecurityUtil instance = new SecurityUtil(); private SecurityUtil (){} public static SecurityUtil getInstance() { return instance; } /** * 無需登入即可訪問的URL * PS:建議從配置檔案讀取 */ private static final String[] URLS = { //登入頁、登入請求、登出請求 "/loginPage", "/login", "/logout", //靜態資源,例如:js、css等 "/assets/**", //一些特殊無需許可權控制的地址、api "/portal/index", }; /** * 使用者角色資訊一般情況下是不輕易更改,可以將結果儲存到快取物件 */ private static HashMap<String,List<Role>> userRoleMap = new HashMap<>(10); //查詢資料庫操作,應交由專案ORM框架負責 private final DbUtil dbUtil = new DbUtil("jdbc:mysql://localhost/jfinal_demo","root","123456"); /** * 鑑權中心 * PS:返回值型別有待商榷 */ public String auc(HttpServletRequest request){ //請求URL地址 String requestUri = request.getRequestURI(); SecurityUtil securityUtil = SecurityUtil.getInstance(); //是否為無需登入即可訪問URL if(SecurityUtil.checkUrl(requestUri,SecurityUtil.URLS)){ //允許訪問! return "SUCCEED"; } //是否為登入使用者 SecurityUtil.User loginUser = securityUtil.getLoginUser(request); if(loginUser == null){ //未登入或登入憑證過期! return "UNAUTHORIZED"; } //該登入使用者是否有權訪問當前URL if(!SecurityUtil.checkUrl(requestUri,securityUtil.getRoleUrlByUserId(loginUser.getId()))){ //抱歉,你無許可權訪問! return "FORBIDDEN"; } //允許訪問! return "SUCCEED"; } /** * 檢查requestUri是否包含在urls中 */ public static boolean checkUrl(String requestUri,String[] urls){ //對/進行特殊處理 if("/".equals(requestUri) && !Arrays.asList(urls).contains(requestUri)){ return false; } String[] requestUris = requestUri.split("/"); for (String url : urls) { if (check(requestUris, url.split("/"))) { return true; } } return false; } private static boolean check(String[] requestUris,String[] urls){ for (int i1 = 0; i1 < requestUris.length; i1++) { //判斷長度 if (i1 >= urls.length){ return false; } //處理/*、/**情況 if("**".equals(urls[i1])){ return true; } if("*".equals(urls[i1])){ continue; } //處理帶字尾 if(requestUris[i1].contains(".") && urls[i1].contains(".")){ String[] split = requestUris[i1].split("\\."); String[] split2 = urls[i1].split("\\."); // *.字尾的情況 if("*".equals(split2[0]) && split[1].equals(split2[1])){ return true; } } //不相等 if(!requestUris[i1].equals(urls[i1])){ return false; } } return true; } /** * 從request設定、獲取當前登入使用者 * PS:登入使用者可以放在session中,也可以做做成jwt */ public void setLoginUser(HttpServletRequest request,User loginUser){ request.getSession().setAttribute("loginUser",loginUser); } public User getLoginUser(HttpServletRequest request){ return (User)request.getSession().getAttribute("loginUser"); } public List<Role> getLoginUserRole(HttpServletRequest request){ User loginUser = this.getLoginUser(request); return loginUser != null ? getRoleByUserId(loginUser.getId()) : null; } /** * 根據使用者id,獲取使用者允許訪問URL */ public String[] getRoleUrlByUserId(String userId){ StringBuilder roleUrl = new StringBuilder(); for (SecurityUtil.Role role : this.getRoleByUserId(userId)) { roleUrl.append(",").append(role.getRoleUrl()); } return roleUrl.toString().split(","); } /** * 獲取使用者、使用者角色 * PS:這些查詢資料庫操作,應交由專案ORM框架負責 */ public User getUserByUserNameAndPassword(String username,String password){ //PS:密碼應該MD5加密後密文儲存,匹配時先MD5加密後匹配,本例中儲存的是明文,就不進行MD5加密了 User user = null; HashMap<String, Object> map = dbUtil.findOne("select * from sys_user where user_name = ? and password = ?", new String[]{username, password}); if(map != null){ user = new User(map.get("id").toString(),map.get("nick_name").toString(),map.get("user_name").toString(),map.get("password").toString()); } //關閉資料庫連線 dbUtil.close(); return user; } public List<Role> getRoleByUserId(String userId){ //先從快取中獲取 List<Role> roles = userRoleMap.get(userId); if(roles != null){ return roles; } //查詢資料庫 List<Role> roleList = null; List<HashMap<String, Object>> list = dbUtil.find("select r.* from sys_role r join sys_user_role ur on r.id = ur.role_id where ur.user_id = ?", new String[]{userId}); if(list != null){ roleList = new ArrayList<>(list.size()); for (HashMap<String, Object> map : list) { roleList.add(new Role(map.get("id").toString(),map.get("role_name").toString(),map.get("role_menu").toString(),map.get("role_url").toString())); } } //關閉資料庫連線 dbUtil.close(); //放到快取中 userRoleMap.put(userId,roleList); return roleList; } /* 3張基礎表 sys_user 系統使用者表 id 表id nick_name 暱稱 user_name 賬號 password 密碼 sys_role 系統角色表 id 表id role_name 角色名稱 role_menu 角色選單可視許可權(可以不關聯選單,單獨做成選單管理直接與使用者關聯) role_url 角色URL訪問許可權 sys_user_role 系統使用者-角色關聯表 id 表id user_id 使用者id role_id 角色id */ public class User{ private String id;//表id private String nickName;//暱稱 private String userName;//賬號 private String password;//密碼 public User(String id, String nickName, String userName, String password) { this.id = id; this.nickName = nickName; this.userName = userName; this.password = password; } public String getId() { return id; } public String getNickName() { return nickName; } public String getUserName() { return userName; } public String getPassword() { return password; } } public class Role{ private String id;//表id private String RoleName;//角色名稱 private String RoleMenu;//角色選單可視許可權(可以不關聯選單,單獨做成選單管理直接與使用者關聯) private String RoleUrl;//角色URL訪問許可權 public Role(String id, String roleName, String roleMenu, String roleUrl) { this.id = id; RoleName = roleName; RoleMenu = roleMenu; RoleUrl = roleUrl; } public String getId() { return id; } public String getRoleName() { return RoleName; } public String getRoleMenu() { return RoleMenu; } public String getRoleUrl() { return RoleUrl; } } public class UserRole{ private String id;//表id private String UserId;//使用者id private String RoleId;//角色id } }
資料庫目前用的是mysql,使用時要記得新增驅動依賴
SpringBoot整合
程式碼
PS:我們自定義DbUtil工具類獲取連線操作,SpringBoot專案需要帶上時區、字符集引數
jdbc:mysql://localhost/jfinal_demo?serverTimezone=GMT%2B8&characterEncoding=utf-8
新建一個springboot專案或在我們的springBoot專案中隨便挑一個來測試
首先需要將springboot-exceptionhandler專案中自定義統一異常處理相關程式碼拷貝過來,方便捕獲我們丟擲的業務異常
然後新建一個AccessAuthorityFilter攔截器
/** * SpringBoot測試鑑權攔截器 */ @WebFilter(filterName = "AccessAuthorityFilter",urlPatterns = {"/**"}) @ServletComponentScan @Component public class AccessAuthorityFilter implements Filter { @Override public void init(FilterConfig filterConfig) { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { //請求頭 HttpServletRequest request = (HttpServletRequest) servletRequest; SecurityUtil securityUtil = SecurityUtil.getInstance(); //鑑權中心 String auc = securityUtil.auc(request); if("UNAUTHORIZED".equals(auc)){ throw new ServiceException(ErrorEnum.UNAUTHORIZED); } if("FORBIDDEN".equals(auc)){ throw new ServiceException(ErrorEnum.FORBIDDEN); } //執行 filterChain.doFilter(servletRequest, servletResponse); } @Override public void destroy() { } }
寫幾個測試介面,包括login登入、logout登出等
/** * 測試介面 */ @RestController public class TestController { /** * 簡單登入、登出、獲取登入使用者 */ @GetMapping("/login") public String login(HttpServletRequest request,String username, String password){ SecurityUtil securityUtil = SecurityUtil.getInstance(); SecurityUtil.User user = securityUtil.getUserByUserNameAndPassword(username, password); if(user != null){ securityUtil.setLoginUser(request,user); return "登入成功!"; }else{ return "賬號或密碼錯誤..."; } } @GetMapping("/logout") public String logout(HttpServletRequest request){ SecurityUtil securityUtil = SecurityUtil.getInstance(); SecurityUtil.User loginUser = securityUtil.getLoginUser(request); securityUtil.setLoginUser(request,null); return "登出成功!"; } @GetMapping("/getLoginUser") public HashMap<String, Object> getLoginUser(HttpServletRequest request){ SecurityUtil securityUtil = SecurityUtil.getInstance(); SecurityUtil.User loginUser = securityUtil.getLoginUser(request); List<SecurityUtil.Role> loginUserRole = securityUtil.getLoginUserRole(request); HashMap<String, Object> map = new HashMap<>(2); map.put("loginUser",loginUser); map.put("loginUserRole",loginUserRole); return map; } /** * 登入、鑑權測試介面 */ @GetMapping("/sys/xtgl") public String xtgl() { return "系統管理..."; } @GetMapping("/sys/yhgl") public String yhgl() { return "使用者管理..."; } @GetMapping("/portal/mhgl") public String mhgl() { return "網站門戶管理..."; } @GetMapping("/portal/index") public String portalIndex() { return "網站門戶首頁..."; } }
效果
未登入時,只有配置在無需登入即可訪問的URL才能允許訪問
登入後,除了無需許可權的URL,還可以訪問角色允許訪問的URL,登出後恢復登入前狀態
SpringBoot專案比較常規大家用的也比較多,程式碼就不上傳了
JFinal整合
程式碼
建立一個訪問許可權攔截器AccessAuthorityInterceptor
package cn.huanzi.qch.interceptor; import cn.huanzi.qch.common.model.ErrorEnum; import cn.huanzi.qch.common.model.ServiceException; import cn.huanzi.qch.util.SecurityUtil; import com.jfinal.aop.Interceptor; import com.jfinal.aop.Invocation; import com.jfinal.log.Log; import javax.servlet.http.HttpServletRequest; /** * 訪問許可權攔截器 */ public class AccessAuthorityInterceptor implements Interceptor { private static final Log log = Log.getLog(AccessAuthorityInterceptor.class); @Override public void intercept(Invocation invocation) { //請求頭 HttpServletRequest request = invocation.getController().getRequest(); SecurityUtil securityUtil = SecurityUtil.getInstance(); //鑑權中心 String auc = securityUtil.auc(request); if("UNAUTHORIZED".equals(auc)){ throw new ServiceException(ErrorEnum.UNAUTHORIZED); } if("FORBIDDEN".equals(auc)){ throw new ServiceException(ErrorEnum.FORBIDDEN); } invocation.invoke(); } }
AppConfig中註冊攔截器
/** * API 引導式配置 */ public class AppConfig extends JFinalConfig { //省略其他程式碼... /** * 配置路由 */ public void configRoute(Routes me) { //省略其他程式碼... // 此處配置 Routes 級別的攔截器,可配置多個 me.addInterceptor(new AccessAuthorityInterceptor()); } //省略其他程式碼... }
寫幾個測試介面,包括login登入、logout登出等
/** * 使用者表 Controller * * 作者:Auto Generator By 'huanzi-qch' * 生成日期:2021-07-29 17:32:50 */ @Path(value = "/user",viewPath = "/user") public class UserController extends CommonController<User,UserServiceImpl> { //省略其他程式碼... /** * 簡單登入、登出、獲取登入使用者 */ @ActionKey("/login") public void login() { String username = get("username"); String password = get("password"); SecurityUtil securityUtil = SecurityUtil.getInstance(); SecurityUtil.User user = securityUtil.getUserByUserNameAndPassword(username, password); if(user != null){ securityUtil.setLoginUser(this.getRequest(),user); renderText("登入成功!"); }else{ renderText("賬號或密碼錯誤..."); } } @ActionKey("/logout") public void logout() { SecurityUtil securityUtil = SecurityUtil.getInstance(); SecurityUtil.User loginUser = securityUtil.getLoginUser(this.getRequest()); securityUtil.setLoginUser(this.getRequest(),null); renderText("登出成功!"); } @ActionKey("/getLoginUser") public void getLoginUser() { SecurityUtil securityUtil = SecurityUtil.getInstance(); SecurityUtil.User loginUser = securityUtil.getLoginUser(this.getRequest()); List<SecurityUtil.Role> loginUserRole = securityUtil.getLoginUserRole(this.getRequest()); HashMap<String, Object> map = new HashMap<>(2); map.put("loginUser",loginUser); map.put("loginUserRole",loginUserRole); renderJson(map); } /** * 登入、鑑權測試介面 */ @ActionKey("/sys/xtgl") public void xtgl() { renderText("系統管理..."); } @ActionKey("/sys/yhgl") public void yhgl() { renderText("使用者管理..."); } @ActionKey("/portal/mhgl") public void mhgl() { renderText("網站門戶管理..."); } @ActionKey("/portal/index") public void portalIndex() { renderText("網站門戶首頁..."); } }
效果
未登入時,只有配置在無需登入即可訪問的URL才能允許訪問
登入後,除了無需許可權的URL,還可以訪問角色允許訪問的URL,登出後恢復登入前狀態
JFinal專案的整合程式碼在我的jfinal-demo專案中:不想用Spring全家桶?試試這個國產JFinal框架
後記
一套簡單的登入、鑑權工具暫時先記錄到這,後續再進行補充
版權宣告
作者:huanzi-qch 出處:https://www.cnblogs.com/huanzi-qch 若標題中有“轉載”字樣,則本文版權歸原作者所有。若無轉載字樣,本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結,否則保留追究法律責任的權利.AD廣告位(長期招租,如有需要請私信)
【基塔後臺】免費後臺管理系統,低程式碼快速搭建管理後臺【騰訊雲】雲產品限時秒殺,爆款1核2G雲伺服器,首年74元!
【騰訊雲】境外1核2G伺服器低至2折,半價續費券限量免費領取!
【騰訊雲】星星海SA2雲伺服器,1核2G首年99元起,高性價比首選!
【騰訊雲】中小企業福利專場,多款剛需產品,滿足企業通用場景需求,雲伺服器2.5折起!
【阿里雲】新老使用者同享,上雲優化聚集地!
【阿里雲】最新活動頁,上新必買搶先知,勁爆優惠不錯過!
【阿里雲】輕量應用伺服器2核2G 低至60元/年起!香港與海外伺服器最低24元/月起!
【阿里雲】ECS例項升級、續費,享低至 6.3折 限時折扣!
捐獻、打賞
請注意:作者五行缺錢,如果喜歡這篇文章,請隨意打賞!支付寶
微信
QQ群交流群
QQ群交流群有事請加群,有問題進群大家一起交流!