介绍

使用 springboot+mybatis+SpringSecurity 实现数据库动态的管理用户、角色、权限管理。

细分角色和权限,并将用户、角色、权限和资源均采用数据库存储,并且自定义滤器,代替原有的FilterSecurityInterceptor过滤器,并分别实现 AccessDecisionManager、InvocationSecurityMetadataSourceService 和 serDetailsService,并在配置文件中进行相应配置。

数据库表设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#创建用户表
create table user
(
id int not null primary key comment'主键id',
username varchar(50) not null comment'用户名',
password varchar(50) not null comment'密码'
)

#角色表
create table role
(
id int not null primary key comment'主键id',
name varchar(50) not null comment'角色名'
)

#用户角色表
create table user_role
(
user_id varchar(50) not null comment'用户id',
role_id varchar(50) not null comment'角色id'
)

#权限表
create table permission (
id int unsigned not null auto_increment comment'主键id',
name varchar (200) not null comment'角色名',
description varchar (200) default null comment'描述',
url varchar (200) not null comment'路径',
pid int default null comment'上级id',
primary key (id)
)

#角色权限中间表
create table permission_role (
id int unsigned not null auto_increment comment'主键id',
role_id int unsigned not null comment'角色id',
permission_id int unsigned not null comment'权限id',
primary key (id)
)

#插入用户
INSERT INTO user(id,username,password)VALUES('1',admin','admin');
INSERT INTO user(id,username,password)VALUES('2',user','user');
INSERT INTO user(id,username,password)VALUES('3',test','test');

#插入角色
INSERT INTO role VALUES ('1', 'ROLE_ADMIN');
INSERT INTO role VALUES ('2', 'ROLE_USER');

INSERT INTO user_role VALUES ('1', '1');
INSERT INTO user_role VALUES ('1', '2');
INSERT INTO user_role VALUES ('2', '2');

INSERT INTO permission VALUES ('1', 'ROLE_USER', 'user', '/admin/home', null);
INSERT INTO permission_role VALUES ('1', '2', '1'));

项目结构

项目结构

引入 maven 依赖

pom 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<!--mysql版本根据自身情况调整-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!--thymeleaf页面展示控制-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
<version>3.0.2.RELEASE</version>
</dependency>
</dependencies>

配置数据库信息

1
2
3
4
5
6
7
8
9
spring.datasource.url=jdbc:mysql://localhost:3306/world
spring.datasource.username=root
spring.datasource.password=123456
spring.jpa.database=mysql
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

server.port=8088

mybatis.mapper-locations=classpath:UserMapper.xml

创建 pojo/dao/mapper/controller 文件

PS: 功能比较简单,所以省略 service 文件,直接调用 dao 层文件;并将权限表的相关查询操作写在同一 mapper 文件

User.java

1
2
3
4
5
6
7
8
public class User {
private int id;
private String username;
private String password;
private List<Role> roles;

/* Getter 和 Setter 自行补充 */
}

Role.java

1
2
3
4
5
6
public class Role {
private int id;
private String name;

/* Getter 和 Setter 自行补充 */
}

Permission.java

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Role {
private int id;
//权限名称
private String name;
//权限描述
private String descritpion;
//授权链接
private String url;
//父节点id
private int pid;

/* Getter 和 Setter 自行补充 */
}

UserMapper.java

1
2
3
4
5
6
7
8
9
10
@Mapper
@Repository
public interface UserMapper {
//根据用户名查找用户所有权限信息
User findByUserName(String username);
//获取所有权限信息
List<Permission> findAllPermission();
//
List<Permission> findByAdminUserId(int userId);
}

UserMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?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.xeh.security.dao.UserMapper">
<resultMap id="userMap" type="com.xeh.security.model.User">
<id property="id" column="ID"/>
<result property="username" column="username"/>
<result property="password" column="PASSWORD"/>
<collection property="roles" ofType="com.xeh.security.model.Role">
<result column="name" property="name"/>
</collection>
</resultMap>
<select id="findByUserName" parameterType="String" resultMap="userMap">
select u.*
,r.name
from user u
LEFT JOIN user_role sru on u.id = sru.user_id
LEFT JOIN role r on sru.role_id = r.id
where username = #{username}
</select>
<select id="findAllPermission" resultType="com.xeh.security.model.Permission">
select * from permission
</select>
<select id="findByAdminUserId" parameterType="int" resultType="com.xeh.security.model.Permission">
select p.*
from user u
left join user_role sru on u.id = sru.user_id
left join role r on sru.role_id = r.id
left join permission_role spr on spr.role_id = r.id
left join permission p on p.id = spr.permission_id
where u.id = #{userId}
</select>
</mapper>

HelloController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
public class HelloController {
@RequestMapping("/admin/hello")
public String index(){
return "hello";
}

@RequestMapping(value ={"","/","/home"})
public String home(){
return "home";
}

@RequestMapping("/login")
public String login(){
return "login";
}
}

页面文件

home.html

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Home</title>
</head>
<body>
<h1>欢迎!</h1>
<p>点击 <a th:href="@{/admin/hello}">这里</a>进入hello页面.</p>
</body>
</html>

hello.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

<div sec:authorize="isAuthenticated()">
<!-- 用户认证通过才能才显示 -->
<p>用户名:<span sec:authentication="name"></span></p>
<p>权限:<span sec:authentication="principal.authorities"></span></p>
</div>
<div sec:authorize="hasRole('ADMIN')">
<!-- 用户角色为“ADMIN”才显示 -->
<p>【管理员】才能看见的内容</p>
</div>
<div sec:authorize="hasRole('USER')">
<!-- 用户角色具有“USER”权限才显示 -->
<p>【普通用户】才能看到的内容</p>
</div>
<form th:action="@{/logout}" method="post">
<input type="submit" value="注销"/>
</form>
</body>
</html>

login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>login</title>
</head>
<body>
<div th:if="${param.error}">
用户名或密码错误
</div>
<div th:if="${param.logout}">
账户已退出登录
</div>
<form th:action="@{/login}" method="post" action="/login">
<div><label> 用户名: <input type="text" name="username"/> </label></div>
<div><label> 密 码: <input type="password" name="password"/> </label></div>
<div><input type="submit" value="登录"/></div>
</form>
</body>
</html>

这样,我们就搭好了连接数据库的 springboot 项目,接下来就是添加权限了

流程图

SpringSecurity 登录认证流程图

SpringSecurity 权限管理流程图

添加 SpringSecurity,实现登录及权限验证

创建 Spring Security 的配置类 WebSecurityConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@Configuration
@EnableWebSecurity //使得Spring Security提供并且支持了Spring MVC的集成
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

//用户无权限拦截处理类
@Autowired
private MyAccessDeniedHandler accessDeniedHandler;

//授权管理
@Autowired
private MyFilterSecurityInterceptor myFilterSecurityInterceptor;

//注册UserDetailsService 的bean,通过用户名加载与该用户的用户名、密码以及权限相关的信息
@Bean
UserDetailsService customUserService(){ //注册UserDetailsService 的bean
return new CustomUserDetailService();
}

/*定义认证规则*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserService()).passwordEncoder(new MyPasswordEncoder()); //user Details Service验证
}

/**
* 对URL进行权限配置
* 该方法定义url的访问权限,登录路径,注销
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll() //任何人(包括没有经过验证的)都可以访问"/"和"/home"
//.antMatchers("/admin/**").access("hasRole('USER')")
.anyRequest().authenticated() //所有其他的URL都需要用户进行验证
.and()
// 配置被拦截时的处理
.exceptionHandling()
//添加无权限时的处理
.accessDeniedHandler(accessDeniedHandler)
.and()
.formLogin() //使用Java配置默认值设置了基于表单的验证。使用POST提交到"/login"时,需要用"username"和"password"进行验证
.loginPage("/login") //指定在需要登录时将用户发送到的URL
.permitAll() //用户可以访问formLogin()相关的任何URL
.and()
.logout() //注销
.permitAll(); //用户可以访问logout()相关的任何URL

//权限控制 Filter
http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class);
}

/*忽略静态资源*/
/*@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/resources/static/**");
}*/
}

实现 UserDetailsService(认证管理器)

自定义UserDetailsService 接口(认证管理器),储存用户所有角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Service
public class CustomUserDetailService implements UserDetailsService {

@Autowired
private UserMapper usersMapper;

/**
* 通过用户名加载与该用户的用户名、密码以及权限相关的信息
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws DisabledException {
User user = usersMapper.findByUserName(username);
if (user != null) {
List<Permission> permissions = usersMapper.findByAdminUserId(user.getId());
List<GrantedAuthority> grantedAuthorities = new ArrayList <>();
for (Permission permission : permissions) {
if (permission != null && permission.getName()!=null) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getName());
//1:此处将权限信息添加到 GrantedAuthority 对象中,在后面进行全权限验证时会使用GrantedAuthority 对象。
grantedAuthorities.add(grantedAuthority);
}
}
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorities);
} else {
//throw new UsernameNotFoundException("用户名不存在");
throw new DisabledException("---->UserName :" + username + " not found!");
}
}
}

实现 PasswordEncoder(加密类)

spring security 版本在 5.0 后要添加 PasswordEncoder 验证

1
2
3
4
5
6
7
8
9
10
11
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}

@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
}

实现 AccessDeniedHandler (用户无权限处理器)

用户无权限时处理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
//返回json形式的错误信息       
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println("{\"code\":403,\"message\":\"你没有权限访问!\",\"data\":\"\"}");
response.getWriter().flush();
/*//无权限时跳转
response.sendRedirect("/home");
*/
request.getSession().invalidate(); //会话结束
}
}

继承 AbstractSecurityInterceptor(资源管理拦截器)

spring security 版本在 5.0 后要添加 PasswordEncoder 验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@Service
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {

//获取被拦截url所需的权限
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;

//获取权限管理器
@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}

public void invoke(FilterInvocation fi) throws IOException, ServletException {
//fi里面有一个被拦截的url
//里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
//再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//执行下一个拦截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}

@Override
public void destroy() {

}

@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}

@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}

}

实现 FilterInvocationSecurityMetadataSource (读取url资源)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@Service
public class MySecurityMetadataSource implements
FilterInvocationSecurityMetadataSource {
@Autowired
private UserMapper usersMapper;

// 资源权限集合
private HashMap<String, Collection<ConfigAttribute>> map =null;

/**
* 获取权限表中所有权限
*/
public void loadResourceDefine(){
map = new HashMap<>();
Collection<ConfigAttribute> array;
ConfigAttribute cfg;
List<Permission> permissions = usersMapper.findAllPermission();
for(Permission permission : permissions) {
array = new ArrayList<>();
cfg = new SecurityConfig(permission.getName());
//此处只添加了用户的名字,其实还可以添加更多权限的信息,例如请求方法到ConfigAttribute的集合中去。此处添加的信息将会作为MyAccessDecisionManager类的decide的第三个参数。
array.add(cfg);
//用权限的getUrl() 作为map的key,用ConfigAttribute的集合作为 value,
map.put(permission.getUrl(), array);
}

}

//此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
if(map ==null) loadResourceDefine();
//object 中包含用户请求的 url 信息
String url = ((FilterInvocation) object).getRequestUrl();
String resUrl;
for(Iterator<String> iter = map.keySet().iterator(); iter.hasNext(); ) {
resUrl = iter.next();
if(resUrl .matches(url)) {
return map.get(resUrl);
}
}
return null;
}

@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}

@Override
public boolean supports(Class<?> clazz) {
return true;
}
}

实现 AccessDecisionManager (授权管理器)

判断用户请求的资源是否能通过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {
// decide 方法是判定是否拥有权限的决策方法,
//authentication 是CustomUserService中循环添加到 GrantedAuthority 对象中的权限信息集合.
//object 包含客户端发起的请求的requset信息,可转换为 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
//configAttributes 为MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法返回的结果,此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {

if(null== configAttributes || configAttributes.size() <=0) {
return;
}
ConfigAttribute c;
String needRole;
for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
c = iter.next();
needRole = c.getAttribute();
//authentication 为 CustomUserDetailService 中循环添加到 GrantedAuthority 对象中的权限信息集合
for(GrantedAuthority ga : authentication.getAuthorities()) {
if(needRole.trim().equals(ga.getAuthority())) {
return;
}
}
}
throw new AccessDeniedException("no right");
}

@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}

@Override
public boolean supports(Class<?> clazz) {
return true;
}
}

测试

首页
登录页面
管理员权限的 hello 页面
普通用户权限的 hello 页面
无权限用户登录后页面

踩坑

加密方式

WebSecurityConfig 配置文件认证规则方法”configure(AuthenticationManagerBuilder auth)” 报错
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

原因: spring security 版本在5.0后,之前版本中的 NoOpPasswordEncoder 被 DelegatingPasswordEncoder 取代了,而你保存在数据库中的密码没有指定加密方式,就要加个 PasswordEncoder 验证

认证管理器异常抛出

CustomUserDetailService 类 loadUserByUsername 默认抛出 UsernameNotFoundException,用 DisabledException 替换 UsernameNotFoundException

这里我们不抛出 UsernameNotFoundException 因为 Security 会把我们抛出的该异常捕捉并换掉,导致抛出的异常无法被 ControllerAdvice 捕捉到,无法进行统一异常处理;所以我们只需要打印正确的异常消息即可,Security 自动把异常添加到 HttpServletRequest 或 HttpSession 中

权限管理设置

1.使用权限表设置,如上
权限管理流程图所示,通过实现 MyFilterSecurityInterceptor(资源管理拦截器)、MyAccessDecisionManager(授权管理器)和MySecurityMetadataSource(拦截器)进行权限拦截。

2.不通过权限表设置
2.1 使用注解方式在 controller 和 WebSecurityConfig 上进行设置


2.2 直接在 WebSecurityConfig 中 configure(HttpSecurity http) 方法进行设置

PS:hasRole() 方法默认含有 ‘ROLE_’ 前缀,书写方式:‘ROLE_ADMIN’ –> hasRole(‘ADMIN’)

源码

项目 github 源码:https://github.com/xeh1430/xehProject/tree/master/security