最近做了一个后台的权限管理项目,没有用到springSecurity,也没有用到shiro,纯粹是用原生态的写法做的,有点粗糙,但是思想没变,这里记录一下自己的心得。
首先是设计数据库的表,一共5张表,分别为用户表,角色表,权限表,用户角色关系表,角色权限关系表。这里遇到的最大的坑就一个,导致后台程序被大改,这里给自己警醒,场景是这样的,当我手动从数据库中删除一条数据的时候,代码debug调试的时候竟然查到我删掉的数据,这是一件很诡异的事情,后来上网查了一些这方面的资料,虽然没查到我这个问题的原因,但是解决方案算是找到了,即为:一张表除了有一个数据库自带的自增主键,还要设定一个逻辑主键,可以用UUID作为主键。第二个问题是项目中的删除很少用到delete语句,一般都是软删除,即为添加一个字段isDelete(0表示删除,1表示存在),所以删除即为更新一下这个字段即可。
然后是后台程序方面:第一个肯定是登录操作,判断逻辑还是有点绕的,首先根据用户名查该用户是否存在,如果不存在,即返回给前端错误码和错误信息;如果查到的话,再将查到的用户的密码和前台传过来的密码进行比对,如果不等,那么进行三次机会重新输入,如果都输入错误,则将该用户的状态更新为锁定,表示该用户暂时不能登录;如果该用户密码比对正确或者三次机会中有一次机会输入正确,则表示该用户登陆成功,更新该用户数据库密码错误次数为初始值。然后用该用户主键根据角色用户关系表查询该用户的角色,为一个集合,再用该角色的主键根据角色权限关系表查询该角色的权限信息,为一个list,然后用该用户的唯一标识,比如该用户的编号,拼接一个随机数,生成一个token,然后将该用户信息和权限的list集合以及这个token封装到一个对象中,返给前台,还将这个信息存在redis缓存中,当然还有个自定义的本地缓存,怎么创建,这样来:创建一个工具类,例如LocalCache类,然后在这个类中定义一个static的集合,例如:
private static List<缓存存储的对象> myList = new ArrayList<>();
然后是用户类和角色类的增删改查,删除数据改isDelete状态,修改数据后返回修改后的数据信息,这些没什么好说的,新增数据的时候要注意先查一下,如果数据库中有这条数据的话,那么不让增加。代码需要try{}catch(){},这样出现错误也能够捕捉的到。区别权限的菜单级和按钮级,这个很好区别,根据Controller的@RequestMapping("")中的path很好判断。例如:
然后还有根据日期区间进行查询,例如查询当天的数据,例如查询2018-12-12 00:00:00到2018-12-12 23:59:59这个日期区间的数据,但是前台传给你的只有2018-12-12日这个格式的数据,因此需要你进行转换为你所需要的日期形式,如下:
package com.Jevin.controller;
import org.junit.Test;
import java.util.Calendar;
import java.util.Date;
public class DateUtil {
/**
* 获取日期的00:00:00
* @param startDate
* @return
*/
public static Date getStartTime(Date startDate){
Calendar calendar = Calendar.getInstance();
calendar.setTime(startDate);
calendar.set(Calendar.HOUR_OF_DAY,0);
calendar.set(Calendar.MINUTE,0);
calendar.set(Calendar.SECOND,0);
return calendar.getTime();
}
/**
* 获取日期的23:59:59
* @param endDate
* @return
*/
public static Date getEndTime(Date endDate){
Calendar calendar = Calendar.getInstance();
calendar.setTime(endDate);
calendar.set(Calendar.HOUR_OF_DAY,23);
calendar.set(Calendar.MINUTE,59);
calendar.set(Calendar.SECOND,59);
return calendar.getTime();
}
@Test
public void test(){
System.out.println(getEndTime(new Date()));
}
}
然后就是对象封装的问题,即为同一个类,我们需要将其拆分为三个对象,一个用来接收前台的参数,一个对象用来和数据库进行交互,一个对象用来返回给前端。
然后是过滤器的问题,到目前为止我已经试过4中过滤器了:
第一种是springAOP过滤器:
package com.Jevin.controller;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class PermissionInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("进入过滤器");
return methodInvocation.proceed();
}
}
配置文件
<!-- 配置拦截器的bean -->
<bean id="permissionInterceptor" class="com.Jevin.controller.PermissionInterceptor"/>
<!-- springAOP方法拦截器配置 -->
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames">
<list>
<value>userController</value>
</list>
</property>
<property name="interceptorNames">
<list>
<value>permissionInterceptor</value>
</list>
</property>
</bean>
<!-- 任意一个类的bean -->
<bean id="userController" class="com.Jevin.controller.UserController"/>
第二种是jboss.resteasy中的CorsFilter
导入一下依赖
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<version>3.0.12.Final</version>
</dependency>
package com.Jevin.controller;
import org.jboss.resteasy.plugins.interceptors.CorsFilter;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
@Provider
@PreMatching
public class PermissionFilter extends CorsFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
System.out.println("requestContext");
boolean flag = false;
if(!flag){
Result result = new Result();
result.setResponseCode("000");
result.setReserveMsg("error message");
Response.ResponseBuilder builder = Response.status(200).entity(result);
builder.type(MediaType.APPLICATION_JSON);
requestContext.abortWith(builder.build());
}
}
}
其中Result是一个结果集对象,主要用来返回给前端响应结果的。
第三种是javax.servlet.Filter中的doFilter()方法过滤器,
第四种是org.springframework.web.servlet.HandlerInterceptor,
这两种比较常见,在网上随处可以查到一大把,这里就不说了。
然后是用户状态,比方说0表示正常,1表示销户,2表示锁定,0,1,2是插入数据库中的数据;正常,销户,锁定是返回给前端的用户状态信息,这里最好用enum枚举类型来做,如下:
package com.Jevin.controller;
public enum UserStateEnum {
NORMAL("0","正常"),
CANCEL("1","注销"),
LOCK("2","锁定");
private String code;
private String status;
UserStateEnum(String code,String status){
this.code=code;
this.status=status;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}
大致自己所走过的弯路,所总结的也就这些了,希望给道友一些友好的提示。。。
后来这里做过一套shiro的权限系统,这里也做一下总结:
实体类三个:
package cn.coralglobal.model.po.admin;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.Size;
import java.io.Serializable;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.Data;
@Data
public class AdminAccount implements Serializable {
private String id;
@NotEmpty(message = "用户名不能为空")
private String name;
@NotEmpty(message = "密码不能为空")
@Size(min = 3, max = 20, message = "密码长度为3到20位")
private String password;
private Date createTime;
private List<AdminRole> roleList; //一个用户对应多个角色
}
package cn.coralglobal.model.po.admin;
import lombok.Data;
import java.util.Date;
import java.util.List;
@Data
public class AdminRole implements java.io.Serializable {
private String id;
private String name;
private String description;
private Date createTime;
private List<AdminPermission> permissionList;// 一个角色对应多个权限
}
package cn.coralglobal.model.po.admin;
import lombok.Data;
import java.util.Date;
@Data
public class AdminPermission implements java.io.Serializable {
private String id;
private String name;
private String description;
private Date createTime;
}
数据库设计如下:
用户表:
CREATE TABLE `admin_account` (
`id` varchar(30) NOT NULL,
`name` varchar(50) NOT NULL,
`password` varchar(50) NOT NULL,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '用户表';
用户角色表
CREATE TABLE `admin_account_role` (
`account_id` varchar(30) NOT NULL COMMENT '用户id',
`role_id` varchar(30) NOT NULL COMMENT '角色id'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '用户角色对应表';
角色表
CREATE TABLE `admin_role` (
`id` varchar(30) NOT NULL,
`name` varchar(50) NOT NULL,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`description` varchar(250) DEFAULT NULL COMMENT '描述',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '角色表';
角色权限表
CREATE TABLE `admin_role_permission` (
`role_id` varchar(30) NOT NULL COMMENT '角色id',
`permission_id` varchar(30) NOT NULL COMMENT '权限id'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '角色权限对应表';
权限表
CREATE TABLE `admin_permission` (
`id` varchar(30) NOT NULL,
`name` varchar(50) NOT NULL,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`description` varchar(250) DEFAULT NULL COMMENT '描述',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '权限表';
mapper文件如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.coralglobal.dal.mapper.AdminAccountMapper">
<resultMap id="BaseResultMap" type="cn.coralglobal.model.po.admin.AdminAccount">
<id column="id" property="id" jdbcType="VARCHAR"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="password" property="password" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<collection property="roleList" javaType="java.util.List" ofType="cn.coralglobal.model.po.admin.AdminRole">
<id column="role_id" property="id" jdbcType="VARCHAR"/>
<result column="role_name" property="name" jdbcType="VARCHAR"/>
<result column="role_create_time" property="createTime" jdbcType="TIMESTAMP"/>
</collection>
</resultMap>
<sql id="Base_Column_List">
a.id,
a.name,
a.password,
a.create_time,
r.id AS role_id,
r.name AS role_name,
r.create_time as role_create_time
</sql>
<!-- 根据用户名称查询该用户的角色 -->
<select id="selectByName" resultMap="BaseResultMap" parameterType="java.lang.String">
SELECT
<include refid="Base_Column_List"/>
FROM admin_account a
LEFT JOIN admin_account_role ar ON a.id = ar.account_id
RIGHT JOIN admin_role r ON r.id = ar.role_id
WHERE a.name = #{name}
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.coralglobal.dal.mapper.AdminRoleMapper">
<resultMap id="BaseResultMap" type="cn.coralglobal.model.po.admin.AdminRole">
<id column="id" property="id" jdbcType="VARCHAR"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<collection property="permissionList" javaType="java.util.List" ofType="cn.coralglobal.model.po.admin.AdminPermission">
<id column="permission_id" property="id" jdbcType="VARCHAR"/>
<result column="permission_name" property="name" jdbcType="VARCHAR"/>
<result column="permission_create_time" property="createTime" jdbcType="TIMESTAMP"/>
</collection>
</resultMap>
<sql id="Base_Column_List">
r.id,
r.name,
r.create_time,
p.id AS permission_id,
p.name AS permission_name,
p.create_time AS permission_create_time
</sql>
<!-- 根据角色名称查询该角色权限 -->
<select id="selectByName" resultMap="BaseResultMap" parameterType="java.lang.String">
SELECT
<include refid="Base_Column_List"/>
FROM admin_role r
LEFT JOIN admin_role_permission rp ON r.id = rp.role_id
RIGHT JOIN admin_permission p ON p.id = rp.permission_id
WHERE r.name = #{name,jdbcType=VARCHAR}
</select>
</mapper>
认证和授权如下
package cn.coralglobal.admin.config;
import cn.coralglobal.dal.mapper.AdminAccountMapper;
import cn.coralglobal.dal.mapper.AdminRoleMapper;
import cn.coralglobal.model.po.admin.AdminAccount;
import cn.coralglobal.model.po.admin.AdminPermission;
import cn.coralglobal.model.po.admin.AdminRole;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.List;
public class UserAuthRealm extends AuthorizingRealm {
private static final Logger logger = LoggerFactory.getLogger(UserAuthRealm.class);
@Autowired
private AdminAccountMapper adminAccountMapper;
@Autowired
private AdminRoleMapper adminRoleMapper;
/**
* 授权
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// Object primaryPrincipal = principalCollection.getPrimaryPrincipal();
// AdminAccount adminAccount = (AdminAccount) primaryPrincipal;
// System.out.println("adminAccount:" + adminAccount);
String userName = (String) principalCollection.fromRealm(this.getClass().getName()).iterator().next();
AdminAccount account = adminAccountMapper.selectByName(userName);
List<AdminRole> roleList = account.getRoleList();
AdminRole adminRole = roleList.get(0);
adminRole = adminRoleMapper.selectByName(adminRole.getName());
// AdminRole adminRole = adminRoleMapper.selectByName(userName);
List<AdminPermission> permissionList = adminRole.getPermissionList();
List<String> permissions = new ArrayList<>();
if (permissionList.size() > 0) {
for (AdminPermission permission : permissionList) {
permissions.add(permission.getName());
}
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRole(adminRole.getName());
info.addStringPermissions(permissions);
return info;
}
/**
* 认证登录
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String loginName = usernamePasswordToken.getUsername();
AdminAccount adminAccount = adminAccountMapper.selectByName(loginName);
if (adminAccount == null) {
throw new UnknownAccountException();
}
List<AdminRole> roleList = adminAccount.getRoleList(); //获取用户角色
List<AdminPermission> permissionList = new ArrayList<>();
for (AdminRole roleList1 : roleList) {
List<AdminPermission> permissionList1 = adminRoleMapper.selectByPrimaryKey(roleList1.getId()).getPermissionList(); //获取用户权限
permissionList.addAll(permissionList1);
roleList1.setPermissionList(permissionList1);
}
adminAccount.setRoleList(roleList);
System.out.println("============" + adminAccount.getRoleList());
return new SimpleAuthenticationInfo(adminAccount.getName(), adminAccount.getPassword(), this.getClass().getName());
}
}
登陆核心代码也就几行:
退出的话,代码也很简单,如下
前端页面权限控制可用如下标签:
当然,也可用<@shiro.hasRole name=""></@shiro.hasRole>