无论是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,使用时要记得添加驱动依赖
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项目比较常规大家用的也比较多,代码就不上传了
创建一个访问权限拦截器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
若标题中有“转载”字样,则本文版权归原作者所有。若无转载字样,本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利.