关于权限

在一个系统中有很多功能例如:查看用户信息、修改用户信息、删除用户信息,而其中有些功能并不适宜让所有人都能够访问,例如修改用户信息和删除用户信息,这些功能需要被合理的使用,只能由一些人使用。

那么怎么识别谁能够使用这些功能,就是给每个人加上一个标识也就是角色

  • 如果你是管理员,就可以进行修改和删除的操作

  • 如果你是普通用户,就只能进行查看操作

有了对权限的认知,下面会讲解在数据库中如何实现权限

数据库表的建立

用户表

用户表就是存放用户的各种信息

大概为:

  • id:作为方便查找用户信息的一种标识
  • name:名字
  • username:名字可能会与其他人重复,我们需要username来确认是否为要找的人
  • password:其他人不能操作自己的空间,需要登录判断是否为本人
  • phone:手机号 也是作为是否为本人的标识
  • gender:性别
  • enabled:是否为启用 如果进行了敏感操作,封禁账号
  • last_login_time:上一次登录时间

角色表

我们首先思考角色需要什么,首先是名字用来区分,之后为备注对于角色的说明

字段:

  • id:方便查找的标识
  • name:角色的名称
  • remark:备注

权限表

权限需要什么:

  • id
  • name: 名字用来区分哪个权限
  • url:实际的权限:每个功能由url请求获取,所以需要存储url
  • method:请求的方式
  • service:服务名
  • parent_id:父id,在一个菜单中可能拥有多个功能。在权限管理界面 查看权限时需要以树形结构显示

现在用户、角色、权限表的创建都已完成,那么这样就可以实现权限了吗?答案是还需要让它们关联起来

用户对应着角色而角色则对应着权限

首先创建用户角色表

用户角色表

用户对应的角色

字段:

  • id
  • role_id:角色的id
  • user_id:用户的id

其次是角色权限表

角色权限表

角色拥有的权限

字段:

  • id
  • role_id:角色的id
  • permission_id:权限的id

这样用户关联角色,角色关联权限,也让用户间接拥有了权限

如何实现

我们可以使用Spring整合SpringSecurity来实现权限控制

首先看流程

在SpringSecurity中它通过UserDetailsService来获取用户信息,之后角色信息也被加载封装为GrantedAuthority 代表赋予给用户的权限的接口。

因为我们用的是数据库中的数据

所以我们需要先实现UserDetails提供用户详细信息。

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class AccountUser implements UserDetails {
private static final long serialVersionUID = -1L;
private Integer userId;
private String password;
private String username;
//权限
private Collection<? extends GrantedAuthority> authorities;
//账户是否过期
private boolean accountNonExpired;
//账户是否锁定
private boolean accountNonLocked;
//证书是否过期
private boolean credentialsNonExpired;
private boolean enabled;

public AccountUser(Integer userId, String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(userId, username, password, true, true, true, true, authorities);
}

public AccountUser(Integer userId, String username, String password,
boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired,
boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null,
"Cannot pass null or empty values to constructor");
this.userId = userId;
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = authorities;
}

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

public Integer getUserId() {
return this.userId;
}

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

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

@Override
public boolean isAccountNonExpired() {
return this.accountNonExpired;
}

@Override
public boolean isAccountNonLocked() {
return this.accountNonLocked;
}

@Override
public boolean isCredentialsNonExpired() {
return this.credentialsNonExpired;
}

@Override
public boolean isEnabled() {
return this.enabled;
}

}

再实现UserDetailsService来获取AccountUser对象,从而实现用户的认证和授权

  1. 创建AccountUserDetailsService实现UserDetailsService接口
1
2
public class AccountUserDetailsService implements UserDetailsService {
}
  1. 注入UserService,因为我们的数据是从数据库中获得。至于数据库的实体类根据上面的表自己创建
1
2
3
4
5
private final UserService userService;

public AccountUserDetailsService(UserService userService) {
this.userService = userService;
}

UserService

此 Service 中创建了获取用户对应权限的方法

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
@Service
public class UserService extends ServiceImpl<UserMapper, User> {
//用户角色
private final UserRoleService userRoleService;
//角色权限
private final RolePermissionService rolePermissionService;
//权限
private final PermissionService permissionService;

public UserService(UserRoleService userRoleService, RolePermissionService rolePermissionService, PermissionService permissionService) {
this.userRoleService = userRoleService;
this.rolePermissionService = rolePermissionService;
this.permissionService = permissionService;
}

//通过用户名获取权限
public List<Permission> getPermissionByUsername(String username) {
User user = super.getOne(Wrappers.<User>lambdaQuery().eq(User::getUsername, username), true);
return this.getPermissionByUser(user);
}

//通过id获取权限
public List<Permission> getPermissionByUserId(Integer userId) {
User user = super.getById(userId);
return this.getPermissionByUser(user);
}

public List<Permission> getPermissionByUser(User user) {
//最终返回的权限集合
List<Permission> permissions = new ArrayList<>();
if (null != user) {
//获取用户对应的全部角色
List<UserRole> userRoles = userRoleService.list(Wrappers.<UserRole>lambdaQuery().eq(UserRole::getUserId, user.getId()));
if (CollectionUtils.isNotEmpty(userRoles)) {
List<Integer> roleIds = new ArrayList<>();
//将角色id收集到集合中
userRoles.forEach(userRole -> roleIds.add(userRole.getRoleId()));
//查找角色对应的所有权限id
List<RolePermission> rolePermissions = rolePermissionService.list(Wrappers.<RolePermission>lambdaQuery().in(RolePermission::getRoleId, roleIds));
if (CollectionUtils.isNotEmpty(rolePermissions)) {
//将权限id都添加到集合中
List<Integer> permissionIds = new ArrayList<>();
rolePermissions.forEach(rolePermission -> permissionIds.add(rolePermission.getPermissionId()));
//通过权限id查找id
permissions = permissionService.list(Wrappers.<Permission>lambdaQuery().in(Permission::getId, permissionIds));
}
}
}
return permissions;
}
}
  1. 重写 loadUserByUsername(String username) 方法
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
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//通过username查出用户的信息
User user = userService.getOne(Wrappers.<User>lambdaQuery().eq(User::getUsername, username), true);
if (user == null) {
throw new UsernameNotFoundException("用户名或密码错误");
}
return new AccountUser(user.getId(), user.getUsername(), user.getPassword(), getUserAuthority(user.getUsername()));
}

/**
* 获取用户权限信息(角色、菜单权限)
* @param username
* @return
*/
public List<GrantedAuthority> getUserAuthority(String username) {
// 角色(比如ROLE_admin),菜单操作权限(比如sys:user:list)
// 角色必须以ROLE_开头,security在判断角色时会自动截取ROLE_
List<Permission> permissions = userService.getPermissionByUsername(username);
// 比如ROLE_admin,ROLE_normal,sys:user:list,...
String authority = "";
if (CollectionUtils.isNotEmpty(permissions)) {
List<String> urls = permissions.stream().map(Permission::getUrl).collect(Collectors.toList());
authority = StrUtil.join(",", urls);
}
//commaSeparatedStringToAuthorityList: 为user账户添加一个或多个权限,用逗号分隔
return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
}

创建SecurityConfig在其中注册AuthenticationProvider使用我们的来获取用户数据

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@Configuration
//开启权限控制 prePostEnabled:在请求前验证
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig{
private final AccountUserDetailsService accountUserDetailsService;

//进行JWT Token验证的过滤器。
private final JwtAuthenticationFilter jwtAuthenticationFilter;
//处理登出成功事件。
private final JwtLogoutSuccessHandler jwtLogoutSuccessHandler;
//处理访问被拒绝(Access Denied)情况。
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
//处理身份验证成功事件。
private final LoginSuccessHandler loginSuccessHandler;
//处理身份验证失败事件。
private final LoginFailureHandler loginFailureHandler;
//处理身份验证失败(未经授权)情况。
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

public SecurityConfig(AccountUserDetailsService accountUserDetailsService, JwtAuthenticationFilter jwtAuthenticationFilter, JwtLogoutSuccessHandler jwtLogoutSuccessHandler, JwtAccessDeniedHandler jwtAccessDeniedHandler, LoginSuccessHandler loginSuccessHandler, LoginFailureHandler loginFailureHandler, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint) {
this.accountUserDetailsService = accountUserDetailsService;
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
this.jwtLogoutSuccessHandler = jwtLogoutSuccessHandler;
this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
this.loginSuccessHandler = loginSuccessHandler;
this.loginFailureHandler = loginFailureHandler;
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
}

//密码加密
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

/**
* 配置身份认证提供者,用于对用户进行身份验证
*
* @return DaoAuthenticationProvider实例
*/
@Bean
public AuthenticationProvider authenticationProvider() {
// 创建一个用户认证提供者
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
// 设置用户相关信息,可以从数据库中读取、或者缓存、或者配置文件
authProvider.setUserDetailsService(accountUserDetailsService);
// 设置加密机制,用于对用户进行身份验证
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
/**
* 配置身份验证管理器,用于处理身份验证请求
*
* @param config AuthenticationConfiguration实例
* @return AuthenticationManager实例
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}

/**
* 配置Spring Security过滤器链,定义请求的安全配置
*
* @param http HttpSecurity实例
* @return SecurityFilterChain实例
* @throws Exception
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 禁用csrf(防止跨站请求伪造攻击)
.csrf(csrf -> csrf.disable())
// 配置登录操作
.formLogin(form -> form
.successHandler(loginSuccessHandler).failureHandler(loginFailureHandler))
// 配置登出操作
.logout(logout -> logout.logoutSuccessHandler(jwtLogoutSuccessHandler))
// 使用无状态session,即不使用session缓存数据
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 设置白名单,允许特定路径的请求不进行身份验证
.authorizeHttpRequests(auth -> auth
.requestMatchers("/user/login").permitAll()
//其他请求都需要认证后才能访问
.anyRequest().authenticated())
// 添加JWT身份验证过滤器
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}

在需要进行权限控制的controller方法上添加权限控制注解 @PreAuthorize

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
@RestController
@RequestMapping(path = "/user", produces = "application/json;charset=utf-8")
public class UserController {
@Resource
private JwtUtil jwtUtil;

private final AuthenticationProvider authenticationProvider;

public UserController(AuthenticationProvider authenticationProvider) {
this.authenticationProvider = authenticationProvider;
}

@PostMapping("/login")
public ResultData login(@RequestBody @Validated UserLoginDTO userLoginDTO) {
Authentication authenticate = authenticationProvider
.authenticate(new UsernamePasswordAuthenticationToken(userLoginDTO.getUsername(), userLoginDTO.getPassword()));
// 认证成功
String token = jwtUtil.generateToken(userLoginDTO.getUsername());
Map<String, String> map = new HashMap<>();
map.put("token", token);
return ResultData.success(map);
}

//@PreAuthorize配合@EnableGlobalMethodSecurity(prePostEnabled = true)使用
@PreAuthorize("hasAuthority('/user/logout')")
@GetMapping("/logout")
public ResultData logout(HttpServletRequest request, HttpServletResponse response) {
// 退出登录
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
//清除认证
new SecurityContextLogoutHandler().logout(request, response, auth);
}
return ResultData.success();
}
}

关于**@PreAuthorize:value** (可省略)属性是 Spring-EL 表达式类型的字符串。其值可参考类 SecurityExpressionRoot,这是Spring Security 框架封装的专门针对 Spring Security 框架本身的 Spring—EL表达式解析类。

分别介绍一下其中值选项:

  • hasRole,对应 public final boolean hasRole(String role) 方法,含义为必须含有某角色(ROLE_开头),如有多个的话,必须同时具有这些角色,才可访问对应资源。
  • hasAnyRole,对应 public final boolean hasAnyRole(String… roles) 方法,含义为只要具有某一角色(多个角色的话,具有任意一个即可),即可访问对应资源。
  • hasAuthority,对应 public final boolean hasAuthority(String authority) 方法,含义同 hasRole,不同点在于这是权限,而不是角色,区别就在于角色往往带有前缀(如默认的ROLE_)而权限只有标识
  • hasAnyAuthority,对应 public final boolean hasAnyAuthority(String… authorities) 方法,含义同 hasAnyRole,不同点在于这是权限,而不是角色,区别就在于角色往往带有前缀(如默认的ROLE_)而权限只有标识
  • permitAll,对应 public final boolean permitAll() 方法,含义为允许所有人(可无任何权限)访问
  • denyAll,对应 public final boolean denyAll() 方法,含义为不允许任何(即使有最大权限)访问
  • isAnonymous,对应 public final boolean isAnonymous() 方法,含义为可匿名(不登录)访问
  • isAuthenticated,对应 public final boolean isAuthenticated() 方法,含义为身份证认证后访问
  • isRememberMe,对应 public final boolean isRememberMe() 方法,含义为记住我用户操作访问
  • isFullyAuthenticated,对应 public final boolean isFullyAuthenticated() 方法,含义为非匿名且非记住我用户允许访问

最终效果:

admin用户拥有退出权限

可以看到admin有登出权限,成功登出

normal用户没有 登出权限

可以看到normal没有权限登出,操作失败返回不允许访问

动态权限管理

我们第一个实现权限管理的方法是直接在接口/Service层方法上添加注解,这样做的好处是实现简单,但是有一个问题就是权限硬编码,每一个方法需要什么权限都是在代码中配置好的,后期如果想通过管理页面修改是不可能的,要修改某一个方法所需要的权限只能改代码

所以我们使用动态权限管理解决

将请求和权限的关系通过数据库来描述,每一个请求需要什么权限都在数据库中配置好,当请求到达的时候,动态查询,然后判断权限是否满足,这样做的好处是比较灵活,将来需要修改接口和权限之间的关系时,可以通过管理页面点几下,问题就解决了,不用修改代码。

一个url可以被多个role访问,只要user中有一个role即可访问该url

  1. 首先创建一个类存储url和role对应的关系
1
2
3
4
5
6
7
@Data
@AllArgsConstructor
public class PermissionWithRoleVO {
private String url;

private List<Role> roles;
}
  1. 创建Service
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
@Service
public class PermissionWithRoleService {
@Resource
private PermissionService permissionService;
@Resource
private RolePermissionService rolePermissionService;
@Resource
private RoleService roleService;

public List<PermissionWithRoleVO> getPermissionWithRole(){
//最终返回的结果
List<PermissionWithRoleVO> permissionWithRoleVOS = new ArrayList<>();
//获取所有权限
List<Permission> permissions = permissionService.list();
permissions.forEach(permission -> {
//获取权限对应的角色id
List<Integer> roleIds = rolePermissionService
.list(Wrappers.<RolePermission>lambdaQuery().eq(RolePermission::getPermissionId, permission.getId()))
.stream().map(RolePermission::getRoleId).toList();
//获取角色
List<Role> roles = roleService.list(Wrappers.<Role>lambdaQuery().in(Role::getId, roleIds));
//将url和角色放进集合里
permissionWithRoleVOS.add(new PermissionWithRoleVO(permission.getUrl(),roles));
});
return permissionWithRoleVOS;
}
}
  1. SecurityConfig中的**securityFilterChain()**方法配置动态权限管理
    动态权限管理在.anyRequest().access()中配置。
    此配置进行了如下操作:

    • 将**.authenticated()**的功能在这里实现

    • 动态权限管理

      • 首先获取此次访问请求的路径
      • 对比看数据库中是否有此路径
      • 如果有获取可以访问此路径的角色集
      • 获取当前用户拥有的角色
      • 判断角色集中是否有用户拥有的角色
      • 如果有,请求访问通过
      • 如果没有,不允许访问
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
.authorizeHttpRequests(auth ->
auth
.requestMatchers(URL_WHITELIST).permitAll()
.anyRequest()
.access((authentication, object) -> {
// 验证是否认证 替换.authenticated()
if (authentication.get() instanceof AnonymousAuthenticationToken) {
return new AuthorizationDecision(false);
}

// 动态权限管理
String requestURI = object.getRequest().getRequestURI();
for (PermissionWithRoleVO p : permissionWithRoleService.getPermissionWithRole()) {
if (new AntPathMatcher().match(p.getUrl(), requestURI)) {
//获取此url可以进行访问的角色
List<Role> roles = p.getRoles();
//获取当前用户拥有的角色,只要有一个可以访问该url的角色即可访问
for (GrantedAuthority authority : authentication.get().getAuthorities()) {
for (Role role : roles) {
if (authority.getAuthority().equals(role.getName())) {
return new AuthorizationDecision(true);
}
}
}
}
}
// 如果没有匹配上,则默认不允许访问
return new AuthorizationDecision(false);
})
)
  1. 因为我们前面实现用户获取的权限信息是urls,所以我们需要将其改为获取角色

    UserService 获取用户拥有的权限方法改为获取用户拥有的角色,之前的方法名为getPermissionByUser()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public List<Role> getRoleByUser(User user) {
List<Role> roles = new ArrayList<>();
if (null != user) {
//获取用户对应的全部角色
List<UserRole> userRoles = userRoleService.list(Wrappers.<UserRole>lambdaQuery().eq(UserRole::getUserId, user.getId()));
if (CollectionUtils.isNotEmpty(userRoles)) {
List<Integer> roleIds = new ArrayList<>();
//将角色id收集到集合中
userRoles.forEach(userRole -> roleIds.add(userRole.getRoleId()));
roles = roleService.list(Wrappers.<Role>lambdaQuery().in(Role::getId, roleIds));
}
}
return roles;
}

AccountUserDetailsService

1
2
3
4
5
6
7
8
9
10
11
12
public List<GrantedAuthority> getUserAuthority(String username) {
// 角色(比如ROLE_admin),菜单操作权限(比如sys:user:list)
// 角色必须以ROLE_开头,security在判断角色时会自动截取ROLE_
List<Role> roles = userService.getPermissionByUsername(username);
// 比如ROLE_admin,ROLE_normal,sys:user:list,...
String authority = "";
if (CollectionUtils.isNotEmpty(roles)) {
List<String> names = roles.stream().map(Role::getName).collect(Collectors.toList());
authority = StrUtil.join(",", names);
}
return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
}

这样我们的动态权限管理就完成了,效果跟用注解一样。