Spring Security 作为安全框架包含类两大核心的模块:认证与鉴权。在前两篇文章中展现了 Spring Security 中的认证模块一些内容,紧接着这篇文章会将目光放到 Spring Security 的鉴权模块中。介绍一下在 Spring Security 中如何使用鉴权功能。

那接下来就看一看在 Spring Security 中如何实现基于角色的访问控制吧!

本文配套的示例源码: https://github.com/lxiaocode/spring-security-examples

本系列的其他文章,推荐按顺序阅读:

  1. Spring Security 使用 JSON 格式登陆
  2. Spring Security 接入数据库中的数据

你将会学到什么

  1. Spring Security 是如何获取用户角色信息的。
  2. Spring Security 的角色是以什么形式表示的。
  3. 在登录时如何获取用户角色并返回给 Spring Security 进行认证授权。
  4. 如何配置角色的访问权限。

1. 准备工作

1.1 创建 Spring Security 项目

在实现功能之前当然要先创建好项目啦。如果学习过我前两篇文章的话,创建一个 Spring Security 项目可以说是非常轻松了。因为这篇文章的示例是基于 Spring Security 接入数据库中的数据 进行扩展的,所以推荐先学习一下这篇文章。

首先,这篇文章会在 Spring Security 接入数据库中的数据 的基础上实现 Spring Security 基于角色的访问控制。所以需要先搭建一个已接入数据库用户的 Spring Security 项目。因为这涉及到了上一篇文章的内容,所以接下来会简单快速的创建项目,不再进行说明了。

1.1.1 导入依赖项
spring-boot-starter-web
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
spring-boot-starter-security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
数据库相关依赖
<!-- 本文使用 MyBatis-Plus 连接数据库 -->
<!-- 使用什么的方法都可以,只要从数据库查询到数据就行 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
1.1.2 创建用户类以及相关类
User.class 实现 UserDetails
@TableName("user")
public class User implements UserDetails {
private String id;
private String username;
private String password;

/**
* 表示该属性不为数据库表字段,但又是必须使用的。
*/
@TableField(exist = false)
private Set<? extends GrantedAuthority> authorities;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}

@Override
public String getPassword() {
return this.password;
}

@Override
public String getUsername() {
return this.username;
}

public void setAuthorities(Set<? extends GrantedAuthority> authorities) {
this.authorities = authorities;
}

//账号是否过期
@Override
public boolean isAccountNonExpired() {
return true;
}

//账号是否锁定
@Override
public boolean isAccountNonLocked() {
return true;
}

//账号凭证是否未过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
// 省略 getter setter ...
}
UserMapper.class
public interface UserMapper extends BaseMapper<User> {
}
UserService.class 实现 UserDetailsService
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
User user = userMapper.selectOne(wrapper);
if (user == null){
throw new UsernameNotFoundException("用户不存在");
}
return user;
}
}
SecurityConfig.class Security 配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;

@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
}

好了,现在就完成 Spring Security 项目的创建并且接入了数据库中的数据了(当然,其中省略了数据库、扫描 Mapper 的相关配置 )。

1.2 数据库表

在基于角色的访问控制中,我们需要有与用户相绑定的角色信息。所以下面会给出本文示例中所用到的数据库表结构,以便参考。

用户表
-- spring_security.`user` definition

CREATE TABLE `user` (
`id` varchar(32) CHARACTER SET utf8mb4 NOT NULL COMMENT '主键,ID',
`username` varchar(100) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '用户名',
`password` varchar(100) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '用户密码',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';

INSERT INTO spring_security.`user` (id,username,password) VALUES
('a4ff5fa9eb9593d335a7e5455de98031','admin','$2a$10$4KL3EiD.TGgcer2l6hSQ1uzuz6kyj6FeqpRR5hhagyNE8f75/FL1S')
,('a4ff5fa9eb9593d335a7e5455de98032','superadmin','$2a$10$lsrIhZQv9HKRFePBBKXFruIM5Yc/YHHNIQL9yy83YBMX9zUf4amCm')
,('a4ff5fa9eb9593d335a7e5455de9803d','user','$2a$10$XZXK3uQw6m0mYAvH5TyyuOxbbEaXICIu1DFv/WJD4v718duRdKGqm')
;
角色表
-- spring_security.`role` definition

CREATE TABLE `role` (
`id` int(11) NOT NULL COMMENT '主键,ID',
`role` varchar(100) CHARACTER SET utf8mb4 NOT NULL COMMENT '角色的字符串表示',
`role_name` varchar(100) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '角色的中文名',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表';

INSERT INTO spring_security.`role` (id,`role`,role_name) VALUES
(1,'ROLE_USER','普通用户')
,(2,'ROLE_ADMIN','普通管理员')
,(3,'ROLE_SUPERADMIN','超级管理员')
;
用户角色关联表
-- spring_security.user_role definition

CREATE TABLE `user_role` (
`user_id` varchar(32) CHARACTER SET utf8mb4 NOT NULL COMMENT '用户ID ',
`role_id` int(11) NOT NULL COMMENT '角色ID '
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户角色关联表';

INSERT INTO spring_security.user_role (user_id,role_id) VALUES
('a4ff5fa9eb9593d335a7e5455de9803d',1)
,('a4ff5fa9eb9593d335a7e5455de98031',2)
,('a4ff5fa9eb9593d335a7e5455de98032',3)
;

将以上数据导入数据库中后,所有的准备工作都做好了。接下来开始实现基于角色的访问控制。

2. 创建角色类以及相关类

我们根据给出的数据库表结构可以创建出所需要的角色类。

Role.class
@TableName("role")
public class Role {
// 可以看出来这个角色类十分的简单,除了 id 只有角色和角色名
// 在 Spring Security 中,默认使用以简单的字符串形式来表示角色,
// 所以我们只需要提供一个 String 类型的字段用来表示角色即可
// 需要注意的是在 Spring Security 中角色字符串表示需要以 “ROLE_” 前缀开头(例如:ROLE_ADMIN)

private Integer id;
// 角色的字符串表示
private String role;
// 角色的中文名
private String roleName;
// 省略 getter setter ...
}
RoleMapper.class
public interface RoleMapper extends BaseMapper<Role> {
}
UserRole.class
@TableName("user_role")
public class UserRole {
// 在数据库中还有名为 user_role 的表,这是用来描述用户和角色之间关联的
// 简单的说,这里存储了哪个用户具有哪个角色的信息

private String userId;
private Integer roleId;
}
UserRoleMapper.class
public interface UserRoleMapper extends BaseMapper<UserRole> {
}

3. 在登录时获取用户角色信息

从之前的文章中我们知道,用户在登录时 Spring Security 会通过 UserDetailsService.loadUserByUsername() 方法获取登录的用户的详细信息,然后会将用户的数据封装进 UserDetails 对象中。

在接入数据库时,我们自定义的用户类实现了 UserDetails 接口,所以我们可以在 loadUserByUsername() 方法中直接返回用户对象。在编写 loadUserByUsername() 时我们重新分析一下 UserDetails

3.1 再次分析 UserDetails

UserDetails 源码分析
// 因为在每个需要认证授权的应用程序中都有自己的用户类来保存用户的信息,但是这些用户类非常相似又截然不同
// Spring Security 为了能对这些各种各样的用户类进行认证授权,就提供了 UserDetails 接口
// 将认证授权所需要的信息都封装进了 UserDetails 中
// 而我们同样也需要实现 UserDetails,并在对应的方法中返回正确的值,否则 Spring Security 无法帮助我们认证授权
public interface UserDetails extends Serializable {

// 其中 getAuthorities() 就是我们本文所关注的方法
// 这个方法需要我们返回一个保存了 GrantedAuthority 子类的集合
// 而这个 GrantedAuthority 就是 Spring Security 中用来表示角色的类
// Spring Security 就可以通过这个方法获取这个用户所拥有的所有角色信息
Collection<? extends GrantedAuthority> getAuthorities();

String getPassword();

String getUsername();

boolean isAccountNonExpired();

boolean isAccountNonLocked();

boolean isCredentialsNonExpired();

boolean isEnabled();
}
GrantedAuthority 源码分析
public interface GrantedAuthority extends Serializable {

// 在 GrantedAuthority 接口中只定义了这个一个方法
// 这个方法就是用于获取角色的字符串表示形式的
String getAuthority();
}

通过对 UserDetails 的再次分析,我们知道了 Spring Security 使用 GrantedAuthority 类来表示角色,并且需要在 getAuthorities() 方法中返回用户的角色信息。

3.2 分析自定义 User

User.class
// 在我们的自定义用户类中同样也实现了 UserDetails 接口,但是我们的用户表中并没有保存与用户角色相关的信息
// 我们所有的角色信息都保存在了角色表和用户角色关联表中
// 所以我们需要一个 setter 方法将外部的查询到的角色信息设置到用户对象中
@TableName("user")
public class User implements UserDetails {
private String id;
private String username;
private String password;

// 在数据库表之外,我们设置了一个字段用于保存角色的信息
// 使用 setter 方法将用户的角色信息保存到这个字段中
@TableField(exist = false)
private Set<? extends GrantedAuthority> authorities;

// 在这个方法中,我们会获取字段中保存的角色信息
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}

// 省略其他无关方法 ...
}

通过分析 User 类,我们知道我们会将外部查询到的用户角色信息保存在字段中。然后在 getAuthorities() 方法中获取其字段的值,这样 Spring Security 就可以获取我们用户的角色信息了。

3.3 在登录时获取用户信息

在接入数据库用户数据时,我们需要实现 UserDetailsService 接口来加载数据库中的用户信息。当时我们只是简单的将用户查询出来返回给 Spring Security 进行认证,并没有将用户的角色信息存入用户对象。接下来就基于原来的 UserService 进行改造添加用户角色信息。

UserService.class
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserMapper userMapper;

// 查询角色以及用户与角色之间的关联的 Mapper
@Autowired
private UserRoleMapper userRoleMapper;
@Autowired
private RoleMapper roleMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

// 查询用户信息
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
User user = userMapper.selectOne(wrapper);
if (user == null){
throw new UsernameNotFoundException("用户不存在");
}

// 这里可以使用联表查询直接查询出用户的角色信息,这里为了简单和易于理解将查询分为两条进行

// 根据用户的 id 查询用户拥有的角色信息
QueryWrapper<UserRole> userRoleWrapper = new QueryWrapper<>();
userRoleWrapper.eq("user_id", user.getId());
List<UserRole> userRoles = userRoleMapper.selectList(userRoleWrapper);

// 用于保存 GrantedAuthority 形式的角色 Set 集合
Set<GrantedAuthority> roles = new HashSet<>();
// 遍历用户的所有角色
QueryWrapper<Role> roleWrapper = new QueryWrapper<>();
for (UserRole ur : userRoles){
// 根据角色 id 查询角色信息
roleWrapper.eq("id", ur.getRoleId());
Role role = roleMapper.selectOne(roleWrapper);
// 将角色的字符串表示形式封装进 SimpleGrantedAuthority 对象中
// SimpleGrantedAuthority 是 GrantedAuthority 的子类,一种以字符串形式表示角色的类型
roles.add(new SimpleGrantedAuthority(role.getRole()));
}
// 将保存用户角色的 Set 集合设置进用户对象中
user.setAuthorities(roles);
// 将存有用户角色信息的用户对象返回给 Spring Security
return user;
}
}
SimpleGrantedAuthority 源码分析
public final class SimpleGrantedAuthority implements GrantedAuthority {

private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

private final String role;

// 在构造方法中,SimpleGrantedAuthority 将角色的字符串表示保存进了 role 字段中
// 作为一种简单的角色表示形式
public SimpleGrantedAuthority(String role) {
Assert.hasText(role, "A granted authority textual representation is required");
this.role = role;
}

// 在 getAuthority() 方法中,SimpleGrantedAuthority 不对 role 进行处理直接将 role 作为角色返回
@Override
public String getAuthority() {
return role;
}
// 省略其他无关方法 ...
}

以上就完成了对 UserService 的改造,实现了在登录时获取用户角色信息并返回给 Spring Security 进行认证授权。

4. 配置角色的权限

前面讲了这么多关于用户角色的内容,那么角色是如何在访问控制中发挥作用呢?关于角色所具有的权限,可以在 SecurityConfig 中进行设置。

SecurityConfig.class
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserService userService;

@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
// 在这里我们对进行配置,Spring Security 从上到下对用户角色进行以此匹配
// 判断用户是否具有权限进行访问
http.authorizeRequests()
.antMatchers("/user/**").hasRole("USER") // 表示具有 USER 角色的用户可以访问 “/user/**” 相关接口
.antMatchers("/admin/**").hasRole("ADMIN") // 表示具有 ADMIN 角色的用户可以访问 “/admin/**” 相关接口
.antMatchers("/super-admin/**").hasRole("SUPERADMIN")// 表示具有 SUPERADMIN 角色的用户可以访问 “/super-admin/**” 相关接口
.anyRequest().authenticated() // 表示其他接口需要登录才能访问
.and()
.formLogin()
.and()
.csrf().disable();
}
}

在上面的 SecurityConfig 中就对角色的接口访问权限进行了配置,这里仅进行对角色权限的简单设置,在以后的文章中会对角色权限设置进行全面的讲解(大概)。

编写对应的接口就可以对用户角色的访问权限进行测试了。

RoleApplication.class
@SpringBootApplication
@MapperScan("com.lxiaocode.role.mapper")
@RestController
@RequestMapping("")
public class RoleApplication {
public static void main(String[] args) {
SpringApplication.run(RoleApplication.class, args);
}

@GetMapping("/user")
public String user(){
return "hello user";
}

@GetMapping("/admin")
public String admin(){
return "hello admin";
}

@GetMapping("/super-admin")
public String superAdmin(){
return "hello super admin";
}
}

5. 总结

以上就是 Spring Security 实现基于角色的访问控制的所有内容。

  • 准备用户、角色以及用户角色的数据库表。
  • 在 loadUserByUsername(String) 方法中返回带有角色信息的 UserDetails 对象。
  • 在 configure(HttpSecurity) 方法中对角色的权限进行设置。

本文配套的示例源码: https://github.com/lxiaocode/spring-security-examples

评论