前言
無論是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 NULL DEFAULT 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
若標題中有“轉載”字樣,則本文版權歸原作者所有。若無轉載字樣,本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,否則保留追究法律責任的權利.