Spring security 动态权限管理(基于数据库)
一、简介
如果对该篇文章不了解,请移步上一篇文章:spring security 中的授权使用-CSDN博客
当我们配置的 URL 拦截规则请求 URL 所需要的权限都是通过代码来配置的,这样就比较死板,如果想要调整访问某一个 URL 所需要的权限,就需要修改代码。动态管理权限规则就是我们将UR 拦截规则和访问 URI 所需要的权限都保存在数据库中,这样,在不修改源代码的情况下,只需要修改数据库中的数据,就可以对权限进行调整。
二、库表设计
里面涉及到 用户 ,角色 ,权限 ,用户角色关系表,角色菜单表共计五张表,用户是用来认证使用的;其中是一些建表语句,如果角色复杂可以将用户这会用内存实现;
2.1 权限表
-- 菜单表
CREATE TABLE `t_authority_menu`(
id int(11) NOT NULL AUTO_INCREMENT primary key ,
pattern_url varchar(128) DEFAULT ''
)ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 添加数据
BEGIN;
insert into t_authority_menu values (1,'/admin/**');
insert into t_authority_menu values (2,'/user/**');
insert into t_authority_menu values (3,'/guest/**');
commit ;
2.2 角色表
-- 角色表
CREATE TABLE `t_authority_role`(
id int(11) NOT NULL AUTO_INCREMENT primary key ,
role_name varchar(128) DEFAULT '' comment '角色标识',
role_desc varchar(128) DEFAULT '' comment '角色描述'
)ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 添加数据
BEGIN;
insert into t_authority_role values (1,'ROLE_ADMIN','系统管理员');
insert into t_authority_role values (2,'ROLE_USER','普通用户');
insert into t_authority_role values (3,'ROLE_GUEST','游客');
commit ;
2.3 角色权限关联表
-- 角色菜单关系表 primary key (m_id,r_id)
CREATE TABLE `t_authority_role_menu`(
id int(11) NOT NULL AUTO_INCREMENT primary key,
m_id int(11) DEFAULT NULL comment '菜单id',
r_id int(11) DEFAULT NULL comment '角色id'
)ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 创建唯一索引
CREATE UNIQUE INDEX i_authority_role_menu1 ON t_authority_role_menu(m_id, r_id);
-- 添加数据
BEGIN;
insert into t_authority_role_menu(m_id, r_id) values (1,1);
insert into t_authority_role_menu(m_id, r_id) values (2,1);
insert into t_authority_role_menu(m_id, r_id) values (2,2);
insert into t_authority_role_menu(m_id, r_id) values (3,3);
insert into t_authority_role_menu(m_id, r_id) values (3,2);
commit ;
2.4 用户表
-- 用户表
CREATE TABLE t_authority_user (
id int(11) NOT NULL AUTO_INCREMENT primary key,
user_name varchar(32) DEFAULT '' comment '用户名',
password varchar(32) DEFAULT '' comment '密码',
enabled tinyint(1) DEFAULT 1 comment '是否启用 1启用',
locked tinyint(1) DEFAULT 1 comment '是否锁定 1 未锁定'
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
BEGIN;
insert into t_authority_user values (1,'admin','{noop}123456','1','1');
insert into t_authority_user values (2,'user','{noop}123456','1','1');
insert into t_authority_user values (3,'guest','{noop}123456','1','1');
commit ;
2.5 用户角色关联表
-- 角色用户关系表
CREATE TABLE `t_authority_role_user`(
id int(11) NOT NULL AUTO_INCREMENT primary key ,
u_id int(11) DEFAULT 0 comment '用户id',
r_id int(11) DEFAULT 0 comment '角色id'
)ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- 创建唯一索引
CREATE UNIQUE INDEX i_authority_role_user1 ON t_authority_role_user(u_id, r_id);
-- 添加数据
BEGIN;
insert into t_authority_role_user(u_id, r_id) values (1,1);
insert into t_authority_role_user(u_id, r_id) values (1,2);
insert into t_authority_role_user(u_id, r_id) values (2,2);
insert into t_authority_role_user(u_id, r_id) values (3,3);
commit ;
三、用户角色权限相关
我们设计 用户 《=》角色 《=》 权限,一个用户拥有多个角色,一个角色对应多个权限
3.1 相关实体类
// 用户
@Data
public class AuthorityUser implements Serializable {
private Integer id;
/**
* 用户名
*/
private String userName;
/**
* 密码
*/
private String password;
/**
* 是否启用 1启用
*/
private Boolean enabled;
/**
* 是否锁定 1 未锁定
*/
private Boolean locked;
private static final long serialVersionUID = 1L;
}
// 角色
@Data
public class AuthorityRole implements Serializable {
private Integer id;
/**
* 角色标识
*/
private String roleName;
/**
* 角色描述
*/
private String roleDesc;
private static final long serialVersionUID = 1L;
}
// 菜单
@Data
public class AuthorityMenu implements Serializable {
private Integer id;
private String patternUrl;
/**
* 一个菜单对应多个角色
*/
private List<AuthorityRole> roles;
private static final long serialVersionUID = 1L;
}
3.2 相关dao 接口
// 用户dao
public interface AuthorityUserDao {
/**
* 根据用户名获取用户信息 认证使用
* @param id
* @return
*/
AuthorityUser selectByusername(String id);
int deleteByPrimaryKey(Integer id);
int insert(AuthorityUser record);
int insertSelective(AuthorityUser record);
AuthorityUser selectByPrimaryKey(Integer id);
int updateByPrimaryKeySelective(AuthorityUser record);
int updateByPrimaryKey(AuthorityUser record);
}
// 角色
public interface AuthorityRoleDao {
/**
* 根据用户编码获取角色列表
* @param userId
* @return
*/
List<AuthorityRole> selectRoleListByUserId(int userId);
int deleteByPrimaryKey(Integer id);
int insert(AuthorityRole record);
int insertSelective(AuthorityRole record);
AuthorityRole selectByPrimaryKey(Integer id);
int updateByPrimaryKeySelective(AuthorityRole record);
int updateByPrimaryKey(AuthorityRole record);
}
// 权限
public interface AuthorityMenuDao {
AuthorityMenu selectByPrimaryKey(Integer id);
/**
* 获取所有的菜单权限
* @return
*/
List<AuthorityMenu> getAllMenu();
}
3.3 dao对应的xml mybatis
// 用户
<?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="com.fashion.mapper.mysql.AuthorityUserDao">
<resultMap id="BaseResultMap" type="com.fashion.model.AuthorityUser">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="user_name" jdbcType="VARCHAR" property="userName" />
<result column="password" jdbcType="VARCHAR" property="password" />
<result column="enabled" jdbcType="BOOLEAN" property="enabled" />
<result column="locked" jdbcType="BOOLEAN" property="locked" />
</resultMap>
<sql id="Base_Column_List">
id, user_name, `password`, enabled, locked
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_authority_user
where id = #{id,jdbcType=INTEGER}
</select>
<select id="selectByusername" parameterType="java.lang.String" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_authority_user
where user_name = #{userName}
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">
delete from t_authority_user
where id = #{id,jdbcType=INTEGER}
</delete>
<insert id="insert" keyColumn="id" keyProperty="id" parameterType="com.fashion.model.AuthorityUser" useGeneratedKeys="true">
insert into t_authority_user (user_name, `password`, enabled,
locked)
values (#{userName,jdbcType=VARCHAR}, #{password,jdbcType=VARCHAR}, #{enabled,jdbcType=BOOLEAN},
#{locked,jdbcType=BOOLEAN})
</insert>
<insert id="insertSelective" keyColumn="id" keyProperty="id" parameterType="com.fashion.model.AuthorityUser" useGeneratedKeys="true">
insert into t_authority_user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="userName != null">
user_name,
</if>
<if test="password != null">
`password`,
</if>
<if test="enabled != null">
enabled,
</if>
<if test="locked != null">
locked,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="userName != null">
#{userName,jdbcType=VARCHAR},
</if>
<if test="password != null">
#{password,jdbcType=VARCHAR},
</if>
<if test="enabled != null">
#{enabled,jdbcType=BOOLEAN},
</if>
<if test="locked != null">
#{locked,jdbcType=BOOLEAN},
</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="com.fashion.model.AuthorityUser">
update t_authority_user
<set>
<if test="userName != null">
user_name = #{userName,jdbcType=VARCHAR},
</if>
<if test="password != null">
`password` = #{password,jdbcType=VARCHAR},
</if>
<if test="enabled != null">
enabled = #{enabled,jdbcType=BOOLEAN},
</if>
<if test="locked != null">
locked = #{locked,jdbcType=BOOLEAN},
</if>
</set>
where id = #{id,jdbcType=INTEGER}
</update>
<update id="updateByPrimaryKey" parameterType="com.fashion.model.AuthorityUser">
update t_authority_user
set user_name = #{userName,jdbcType=VARCHAR},
`password` = #{password,jdbcType=VARCHAR},
enabled = #{enabled,jdbcType=BOOLEAN},
locked = #{locked,jdbcType=BOOLEAN}
where id = #{id,jdbcType=INTEGER}
</update>
</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="com.fashion.mapper.mysql.AuthorityRoleDao">
<resultMap id="BaseResultMap" type="com.fashion.model.AuthorityRole">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="role_name" jdbcType="VARCHAR" property="roleName" />
<result column="role_desc" jdbcType="VARCHAR" property="roleDesc" />
</resultMap>
<sql id="Base_Column_List">
id, role_name, role_desc
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_authority_role
where id = #{id,jdbcType=INTEGER}
</select>
<select id="selectRoleListByUserId" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select a.role_name,a.role_desc from
t_authority_role a
left join t_authority_role_user b on a.id = b.r_id
where b.u_id = #{userId}
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Integer">
delete from t_authority_role
where id = #{id,jdbcType=INTEGER}
</delete>
<insert id="insert" keyColumn="id" keyProperty="id" parameterType="com.fashion.model.AuthorityRole" useGeneratedKeys="true">
insert into t_authority_role (role_name, role_desc)
values (#{roleName,jdbcType=VARCHAR}, #{roleDesc,jdbcType=VARCHAR})
</insert>
<insert id="insertSelective" keyColumn="id" keyProperty="id" parameterType="com.fashion.model.AuthorityRole" useGeneratedKeys="true">
insert into t_authority_role
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="roleName != null">
role_name,
</if>
<if test="roleDesc != null">
role_desc,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="roleName != null">
#{roleName,jdbcType=VARCHAR},
</if>
<if test="roleDesc != null">
#{roleDesc,jdbcType=VARCHAR},
</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="com.fashion.model.AuthorityRole">
update t_authority_role
<set>
<if test="roleName != null">
role_name = #{roleName,jdbcType=VARCHAR},
</if>
<if test="roleDesc != null">
role_desc = #{roleDesc,jdbcType=VARCHAR},
</if>
</set>
where id = #{id,jdbcType=INTEGER}
</update>
<update id="updateByPrimaryKey" parameterType="com.fashion.model.AuthorityRole">
update t_authority_role
set role_name = #{roleName,jdbcType=VARCHAR},
role_desc = #{roleDesc,jdbcType=VARCHAR}
where id = #{id,jdbcType=INTEGER}
</update>
</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="com.fashion.mapper.mysql.AuthorityMenuDao">
<resultMap id="BaseResultMap" type="com.fashion.model.AuthorityMenu">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="pattern_url" jdbcType="VARCHAR" property="patternUrl" />
</resultMap>
<sql id="Base_Column_List">
id, pattern_url
</sql>
<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_authority_menu
where id = #{id,jdbcType=INTEGER}
</select>
<resultMap id="MenuResultMap" type="com.fashion.model.AuthorityMenu">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="pattern_url" jdbcType="VARCHAR" property="patternUrl" />
<collection property="roles" ofType="com.fashion.model.AuthorityRole">
<id column="rId" property="id"/>
<result column="role_name" jdbcType="VARCHAR" property="roleName" />
<result column="role_desc" jdbcType="VARCHAR" property="roleDesc" />
</collection>
</resultMap>
<select id="getAllMenu" resultMap="MenuResultMap">
select a.*,c.id rId,c.role_name,c.role_desc
from t_authority_menu a
left join t_authority_role_menu b on a.id = b.m_id
left join t_authority_role c on c.id = b.r_id
</select>
</mapper>
四、自定义UserDetailsService
用来实现自定义登录逻辑,查询数据库用户,如果不懂请看前面系列教程
@Service
public class UserServiceDetailImpl implements UserDetailsService {
@Autowired
private AuthorityUserDao authorityUserDao;
@Autowired
private AuthorityRoleDao authorityRoleDao;
/**
* 认证
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1、 根据用户名获取信息
AuthorityUser authorityUser = authorityUserDao.selectByusername(username);
if (null == authorityUser) {
throw new UsernameNotFoundException("用户不存在!");
}
// 2、获取用户对应的角色
List<AuthorityRole> roles = authorityRoleDao.selectRoleListByUserId(authorityUser.getId());
UserDetailInf userDetailInf = new UserDetailInf(authorityUser, roles);
return userDetailInf;
}
}
4.1 UserDetails 自定义
用来保存用户登录成功后,SpringSecurityHolder中的认证信息,里面有用户权限信息
public class UserDetailInf implements UserDetails {
private AuthorityUser user;
private List<AuthorityRole> roles;
public UserDetailInf(AuthorityUser user, List<AuthorityRole> roles) {
this.user = user;
this.roles = roles;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(r -> new SimpleGrantedAuthority(r.getRoleName()))
.collect(ArrayList::new, List::add,List::addAll);
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getPassword();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
4.2 自定义数据源扫码mapper
@Configuration
@MapperScan(basePackages = MysqlDsConfiguration.PACKAGE, sqlSessionFactoryRef = "mysqlSqlSessionFactory" )
@Slf4j
public class MysqlDsConfiguration {
static final String PACKAGE = "com.fashion.mapper.mysql";
static final String MAPPER_LOCATION = "classpath:mybatis/mapper/mysql/*.xml";
/**
* 配置数据源
* @return
*/
@Primary
@Bean
public DataSource mysqlDataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/test");
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUsername("root");
dataSource.setPassword("12345");
return dataSource;
}
@Primary
@Bean
public SqlSessionFactory mysqlSqlSessionFactory(@Autowired DataSource mysqlDataSource){
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
try {
sessionFactory.setDataSource(mysqlDataSource);
sessionFactory.setConfigLocation(new ClassPathResource("/mybatis/mybatis-config.xml"));
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources(MysqlDsConfiguration.MAPPER_LOCATION));
return sessionFactory.getObject();
} catch (Exception e) {
log.error("mysql数据源初始化失败",e);
}
return null;
}
}
五、定义 MetadataSource 权限信息
实现 FilterInvocationSecurity ,用来将权限对应的角色信息加载进去,该类用于我们重写getAttributes ,将我们菜单对应的角色权限查询出来,实现动态授权
@Component
public class UrlAntPatchMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private AuthorityMenuDao authorityMenuDao;
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 当前obj 实际上是一个url
String requestURI = ((FilterInvocation) object).getRequest().getRequestURI();
// 获取所有的url 对应的角色集合
List<AuthorityMenu> allAuthorityMenus = authorityMenuDao.getAllMenu();
String[] roles = allAuthorityMenus.stream()
.filter(menu -> antPathMatcher.match(menu.getPatternUrl(),requestURI))
.flatMap(authorityMenu -> authorityMenu.getRoles().stream())
.map(AuthorityRole::getRoleName)
.toArray(String[]::new);
if (null != roles && roles.length > 0) {
return SecurityConfig.createList(roles);
}
// for (AuthorityMenu menu : allAuthorityMenus) {
// if (antPathMatcher.match(menu.getPatternUrl(),requestURI)) {
// String[] roles = menu.getRoles().stream().map(r -> r.getRoleName()).toArray(String[]::new);
// return SecurityConfig.createList(roles);
// }
// }
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
六、SecurityConfiguration 配置
1、设置全局的自定义数据源
2、设置权限过滤规则,将自定义的FilterInvocationSecurityMetadataSource注入
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private UrlAntPatchMetadataSource urlAntPatchMetadataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 1、获取工厂对象
ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
// 2、设置自定义url 匹配规则
http.apply(new UrlAuthorizationConfigurer<>(applicationContext))
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setSecurityMetadataSource(urlAntPatchMetadataSource);
// 如果没有权限是否拒绝
object.setRejectPublicInvocations(true);
return object;
}
});
http.authorizeRequests()
.anyRequest().authenticated()
.and().formLogin()//开启表单登录
.and().csrf().disable();
}
}
七、测试controller
用户对应的角色分析
admin 用户拥有 ROLE_ADMIN/ROLE_USER 的角色
user 用户拥有 ROLE_USER 的角色
guest 用户拥有 ROLE_GUEST 的角色
角色对应的权限分析
ROLE_ADMIN角色拥有 /admin/** 以及 /user/** 以下权限
ROLE_USER 角色拥有 /user/** 以及/guest/** 以下权限
ROLE_GUEST用户拥有 /guest/** 路径以下权限
@RestController
public class HelloController {
/**
*
* @return
*/
@RequestMapping("/admin/hello")
public String admin() {
return "hello admin";
}
@RequestMapping("/user/hello")
public String user() {
return "hello user";
}
@RequestMapping("/guest/hello")
public String guest() {
return "hello guest";
}
@RequestMapping("/hello")
public String hello() {
return "hello";
}
}
7.1 admin 登录测试效果
使用 admin 用户登录,我们访问接口测试权限,经分析,admin 拥有下面所有的权限,对应controller中的三个方法是都可以访问的;
7.2 user登录测试效果
使用 user用户登录,分析得到;user用户只能访问 /user/hello 或者 /guest/hello 接口
7.3 guest 用户登录测试效果
使用 guest用户登录,分析得到;guest用户只能访问 /guest/hello 接口
八、问题总结
一、AntPathMatcher中的mather 方法,里面千万不能写反了,第一个是我们通配库里面定义的,第二个参数为请求的url,两个的顺序不能对调;
antPathMatcher.match(menu.getPatternUrl(),requestURI)二、我们每一次授权都需要走一次数据库,性能问题;
解决方案:1、设置一个hutool中的timecache 定时清除里面对应权限信息,设置10分钟,这样我们10分钟才跟数据源有一次交互;问题就是集群,可能每一台里面都需要放一次,优点是效率更高
2、我们将权限信息存到redis中,这种方案更好,如果是集群也不影响;缺点就是需要一个redis的依赖;
源码跟文档我都上传了,有需求的小伙伴自行下载,下载链接:
https://download.csdn.net/download/qq_36260963/89733771