前言
上篇文章介绍了Spring Boot Security基于Redis的Spring Session管理
本篇文章,可以说比较核心、实用的功能,动态用户角色资源管理(RBAC),可能篇幅会比较长,废话不多说,马上进入正题
简单介绍
相信每个正规的系统,都会对系统安全和访问权限有严格的控制。简单的一句话总结,就是对的人访问对的资源,这里可能会比较抽象,博主给大家举个例子就懂了:
现在假设有个系统,里面有菜单A、菜单B和菜单C 客户有这么个需求,就是对于管理员来说,可以访问所有资源菜单,对于普通用户来说,只能访问菜单A和菜单B,如图:
相信这个也是广大系统都有的最基础的需求,那么在系统中的表现,就是用户登录了系统后,如果是普通用户的话,前端只显示菜单A和菜单B,其他途径访问(直接输入URL)菜单C会被提示无权限,而管理员则显示所有菜单
那么怎么实现呢,小编这里就是基于RBAC模型去实现的,简单来说就是:
举个例子:
- 用户就是登录系统的用户,像张三、李四、小王这样的具体登陆用户
- 角色就是假如张三是教师、李四是学生,那么教师和学生角色,也可能可以分得更细,这个根据需求来定义
- 资源就是访问系统的资源,如查询学生信息、编辑学生信息等等之类
用户和资源是没有直接关联的,用户是通过关联角色,角色再关联资源这种间接的方式去判断自己的资源权限。这样做的好处就是可以更简单直观的去管理用户资源间的关联,不需要说每创建一个用户,就去再重新分配资源这么繁琐,减少数据库冗余设计
数据库设计
数据库表的设计如图:
这里有几点要说明下:
- 一般 用户 与 角色 是一对一或者一对多的关系,我这里为了方便所以选择一对一的关系
- 角色 与 资源 是多对多的关系,所以需要中间表 sys_role_resource 存储中间的联系
实体代码如下:
Role.java
/**
* Copyright 2020. javaymw.com Studio All Right Reserved
* <p>
* Create on 2020-06-07 11:38
* Created by zhaoxinguo
* Version 2.0.0
*/
package com.javaymw.demo.sys.entity;
import org.springframework.security.core.GrantedAuthority;
import javax.annotation.Resource;
import javax.persistence.*;
import java.util.Set;
/**
* @description: 角色实体类
* @author zhaoxinguo
* @date 2020/6/7 11:38
*/
@Entity
@Table(name = "sys_role")
public class Role implements GrantedAuthority {
//id
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column
protected Integer id;
//角色标识
@Column
private String roleKey;
//角色名称
@Column
private String roleName;
//角色拥有的资源(多对多)
@ManyToMany(targetEntity = Resource.class, fetch = FetchType.EAGER)
@JoinTable(
name = "sys_role_resource",
joinColumns = {
@JoinColumn(name = "role_id", referencedColumnName = "id", nullable = false)
},
inverseJoinColumns = {
@JoinColumn(name = "resource_id", referencedColumnName = "id", nullable = false)
})
private Set<Resource> resources;
@Override
public String getAuthority() {
return roleKey;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getRoleKey() {
return roleKey;
}
public void setRoleKey(String roleKey) {
this.roleKey = roleKey;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public Set<Resource> getResources() {
return resources;
}
public void setResources(Set<Resource> resources) {
this.resources = resources;
}
}
这里要说明下,GrantedAuthority 接口中的getAuthorities()方法返回的当前用户对象拥有的权限,简单的说就是该用户的角色信息,所以这里我角色标识roleKey表示
Resource.java
/**
* Copyright 2020. javaymw.com Studio All Right Reserved
* <p>
* Create on 2020-06-07 11:42
* Created by zhaoxinguo
* Version 2.0.0
*/
package com.javaymw.demo.sys.entity;
import javax.persistence.*;
import java.io.Serializable;
/**
* @description: 资源实体类
* @author zhaoxinguo
* @date 2020/6/7 11:42
*/
@Entity
@Table(name = "sys_resource")
public class Resource implements Serializable {
//id
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column
protected Integer id;
//资源名称
@Column(nullable = false)
private String resourceName;
//资源标识
@Column(nullable = false)
private String resourceKey;
//资源url
@Column(nullable = false)
private String url;
/**
* 资源类型
* 0:菜单
* 1:按钮
*/
@Column(nullable = false)
private Integer type;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getResourceName() {
return resourceName;
}
public void setResourceName(String resourceName) {
this.resourceName = resourceName;
}
public String getResourceKey() {
return resourceKey;
}
public void setResourceKey(String resourceKey) {
this.resourceKey = resourceKey;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Integer getType() {
return type;
}
public void setType(Integer type) {
this.type = type;
}
}
相信这些代码大家都看得明白,下面开始进入核心部分
实现
在这里,博主介绍下怎么在Spring Security中实现资源管理功能,也就是针对不同的用户角色,动态的判断是否能访问相应的资源菜单
先看看项目结构图:
首先,我们需要在自定义登录认证那里,设置权限信息:
LoginValidateAuthenticationProvider.java
/**
* Copyright 2020. javaymw.com Studio All Right Reserved
* <p>
* Create on 2020-06-05 21:59
* Created by zhaoxinguo
* Version 2.0.0
*/
package com.javaymw.demo.core;
import com.javaymw.demo.sys.entity.User;
import com.javaymw.demo.sys.service.UserService;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.HashSet;
import java.util.Set;
/**
* @description: 自定义认证核心类
* @author zhaoxinguo
* @date 2020/6/5 21:59
*/
@Component
public class LoginValidateAuthenticationProvider implements AuthenticationProvider {
@Resource
private UserService userService;
/**
* 解密用的
*/
@Resource
private PasswordEncoder passwordEncoder;
/**
* 进行身份验证
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//获取输入的用户名
String username = authentication.getName();
//获取输入的明文
String rawPassword = (String) authentication.getCredentials();
//查询用户是否存在
User user = (User) userService.loadUserByUsername(username);
if (!user.isEnabled()) {
throw new DisabledException("该账户已被禁用,请联系管理员");
} else if (!user.isAccountNonLocked()) {
throw new LockedException("该账号已被锁定");
} else if (!user.isAccountNonExpired()) {
throw new AccountExpiredException("该账号已过期,请联系管理员");
} else if (!user.isCredentialsNonExpired()) {
throw new CredentialsExpiredException("该账户的登录凭证已过期,请重新登录");
}
//验证密码
if (!passwordEncoder.matches(rawPassword, user.getPassword())) {
throw new BadCredentialsException("输入密码错误!");
}
// TODO Spring Boot2 + Spring Security5 动态用户角色资源的权限管理(6) 添加代码
// 设置权限信息
Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
for (com.javaymw.demo.sys.entity.Resource resource : user.getRole().getResources()) {
//资源key作为权限标识
grantedAuthorities.add(new SimpleGrantedAuthority(resource.getResourceKey()));
user.setAuthorities(grantedAuthorities);
}
return new UsernamePasswordAuthenticationToken(user, rawPassword, user.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
//确保authentication能转成该类
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
这里要注意的是,我们把resource实体的resourceKey作为资源的权限标识,设置进grantedAuthorities集合里面,以便spring security根据注解@PreAuthorize自动权限判断
由于我们设计的用户与角色是一对一关联,所以我们这里GrantedAuthority集合就只有一条角色信息数据
然后就是自定义权限不足handler
PerAccessDeniedHandler.java
/**
* Copyright 2020. javaymw.com Studio All Right Reserved
* <p>
* Create on 2020-06-07 11:50
* Created by zhaoxinguo
* Version 2.0.0
*/
package com.javaymw.demo.core.handler;
import com.alibaba.fastjson.JSONObject;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
/**
* @description: 自定义权限不足handler
* @author zhaoxinguo
* @date 2020/6/7 11:49
*/
@Component
public class PerAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//登录成功返回
Map<String, Object> paramMap = new HashMap<>(2);
paramMap.put("code", "503");
paramMap.put("message", accessDeniedException.getMessage());
//设置返回请求头
response.setContentType("application/json;charset=utf-8");
//写出流
PrintWriter out = response.getWriter();
out.write(JSONObject.toJSONString(paramMap));
out.flush();
out.close();
}
}
最后我们看看Spring Security配置类的变化:
SecurityConfig.java
/**
* Copyright 2020. javaymw.com Studio All Right Reserved
* <p>
* Create on 2020-06-05 21:58
* Created by zhaoxinguo
* Version 2.0.0
*/
package com.javaymw.demo.config;
import com.javaymw.demo.core.LoginValidateAuthenticationProvider;
import com.javaymw.demo.core.handler.LoginFailureHandler;
import com.javaymw.demo.core.handler.LoginSuccessHandler;
import com.javaymw.demo.core.handler.PerAccessDeniedHandler;
import com.javaymw.demo.sys.service.UserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.annotation.Resource;
import javax.sql.DataSource;
/**
* @description: Spring Security 核心配置类
* @author zhaoxinguo
* @date 2020/6/5 21:57
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//数据源
@Resource
private DataSource dataSource;
//用户业务层
@Resource
private UserService userService;
//自定义认证
@Resource
private LoginValidateAuthenticationProvider loginValidateAuthenticationProvider;
//登录成功handler
@Resource
private LoginSuccessHandler loginSuccessHandler;
//登录失败handler
@Resource
private LoginFailureHandler loginFailureHandler;
//权限不足handler
@Resource
private PerAccessDeniedHandler perAccessDeniedHandler;
/**
* 权限核心配置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//基础设置
http.httpBasic()//配置HTTP基本身份验证
.and()
.authorizeRequests()
.anyRequest().authenticated()//所有请求都需要认证
.and()
.formLogin() //登录表单
.loginPage("/login")//登录页面url
.loginProcessingUrl("/login")//登录验证url
.defaultSuccessUrl("/index")//成功登录跳转
.successHandler(loginSuccessHandler)//成功登录处理器
.failureHandler(loginFailureHandler)//失败登录处理器
.permitAll()//登录成功后有权限访问所有页面
.and()
.exceptionHandling().accessDeniedHandler(perAccessDeniedHandler)//设置权限不足handler
.and()
.rememberMe()//记住我功能
.userDetailsService(userService)//设置用户业务
.tokenRepository(persistentTokenRepository())//设置持久化token
.tokenValiditySeconds(24 * 60 * 60);//记住登录1天(24小时 *60分钟 * 60秒)
//关闭csrf跨域攻击防御
http.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//这里要设置自定义认证
auth.authenticationProvider(loginValidateAuthenticationProvider);
}
/**
* BCrypt加密
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 记住我功能,持久化的token服务
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
//数据源设置
tokenRepository.setDataSource(dataSource);
//启动的时候创建表,这里只执行一次,第二次就注释掉,否则每次启动都重新创建表
/*tokenRepository.setCreateTableOnStartup(true);*/
return tokenRepository;
}
}
在Spring Security配置文件中,我们只需要设置PerAccessDeniedHandler就可以了,还要记得在头部添加**@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled=true)注解,以启动spring security注解生效**
接下来就是前端页面和控制层:
UserContorller.java
/**
* Copyright 2020. javaymw.com Studio All Right Reserved
* <p>
* Create on 2020-06-05 22:01
* Created by zhaoxinguo
* Version 2.0.0
*/
package com.javaymw.demo.sys.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @description: TODO
* @author zhaoxinguo
* @date 2020/6/5 22:01
*/
@Controller
@RequestMapping("/")
public class UserController {
/**
* 登录页面跳转
* @return
*/
@GetMapping(value = "/login")
public String login() {
return "login.html";
}
/**
* index页跳转
* @return
*/
@GetMapping("index")
public String index() {
return "index.html";
}
/**
* menu1
* @return
*/
@PreAuthorize("hasAuthority('menu1')")
@GetMapping("menu1")
@ResponseBody
public String menu1() {
return "menu1";
}
/**
* menu2
* @return
*/
@PreAuthorize("hasAuthority('menu2')")
@GetMapping("menu2")
@ResponseBody
public String menu2() {
return "menu2";
}
/**
* menu3
* @return
*/
@PreAuthorize("hasAuthority('menu3')")
@GetMapping("menu3")
@ResponseBody
public String menu3() {
return "menu3";
}
}
这里要注意的是,每个需要权限判断的方法中,都需要增加@PreAuthorize(“hasAuthority(‘key’)”)注解,否则权限判断不生效,key对应数据库资源表中的资源标识字段
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index页</title>
</head>
<body>
index页<br/><br/>
<button id="menu1Btn" type="button" onclick="sendAjax('/menu1')">菜单1</button>
<button id="menu2Btn" type="button" onclick="sendAjax('/menu2')">菜单2</button>
<button id="menu3Btn" type="button" onclick="sendAjax('/menu3')">菜单3</button>
function sendAjax(url) {
$.ajax({
type: "GET",
url: url,
dataType: "text",
success: function (data) {
console.log(data);
}
});
}
</body>
</html>
这里简单的说说数据库的数据
用户表:admin、teacher和student
角色表:管理员、教师和学生
资源表:menu1、menu2、menu3
对应权限: 管理员:menu1、menu2、menu3
教师:menu1、menu2
学生:meun1
下面我们看看效果,登录页:
index页:
这里我们先用admin管理员角色登录,然后点击所有菜单:
可以看到数据正常,并且已经访问到了所有资源菜单
然后我们用 teacher教师角色 登录,也是点击所有菜单:
会发现,在点击第三个菜单的时候,会返回没有权限访问
我们再用 student学生角色 登录,也是点击所有菜单:
这里说明我们的动态权限资源管理都生效了
那么文章就介绍到这里,在这里留了个坑,一般系统是不会让用户去点击了菜单才发现没有权限访问,而是针对不同的用户,动态显示不同的菜单,这个内容小编下篇文章就会讲解
结束语
那么基本代码和效果也演示完毕了
源码获取方式加加入QQ交流群(660164226),进群找群主要源码,如果有问题,可以提出疑问,群主会尽量帮助解决~
希望能帮助到大家,如果有不好或者错误的地方希望能多多提出,谢谢大家~
注意:本文归作者所有,未经作者允许,不得转载