授权系统的作用

例如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书相关的功能,不可能让他看到并且去使用添加书籍信息,删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了,应该就能看到并使用添加书籍信息,删除书籍信息等功能。

​ 总结起来就是不同的用户可以使用不同的功能。这就是权限系统要去实现的效果。

​ 我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果有人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作。

​ 所以我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须具有所需权限才能进行相应的操作。

授权的基本流程

image-20211214144425527

SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。

所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication

然后设置我们的资源所需要的权限即可。

在讲解登录认证详解时,我们有一些遗留代码没有写,都是关于权限的。如下:

  • UserDetailsService接口的实现时,遗留了查询用户的权限,并封装到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
    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService service;

    @Autowired
    private MenuMapper menuMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    //根据传入了Passward查询用户
    LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(User::getUserName,username);
    User user = service.getOne(queryWrapper);
    //如果没有查询到用户就抛出异常
    if(Objects.isNull(user)){
    System.out.println("1111");
    throw new UsernameNotFoundException("用户名不存在");
    }
    // TODO 查询对应的权限

    return new LoginUser(user);
    }
    }
  • 在实现认证过滤器时,遗留了获取权限信息封装到Authentication中。

    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
    @Component
    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
    //首先需要获取token
    String token = httpServletRequest.getHeader("token");
    //判断token是否为Null
    if(!StringUtils.hasText(token)) {
    filterChain.doFilter(httpServletRequest, httpServletResponse);
    return;
    }
    //如何不为空,解析token,获得了UserId
    String userId;
    try {
    Claims claims = JwtUtil.parseJWT(token);
    userId = claims.getSubject();
    } catch (Exception e) {
    e.printStackTrace();
    throw new RuntimeException("toen非法");
    }
    //根据UserId查redis获取用户数据
    String key = "login:"+userId;
    LoginUser LoginUser = redisCache.getCacheObject(key);
    if(Objects.isNull(LoginUser)){
    throw new RuntimeException("用户未登录");
    }
    //然后封装Authentication对象存入SecurityContextHolder
    // TODO 获取权限信息封装到Authentication中
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(LoginUser,null,null);
    SecurityContextHolder.getContext().setAuthentication(authenticationToken);

    filterChain.doFilter(httpServletRequest,httpServletResponse);
    }
    }

SpringSecurity中,用什么表示权限?

权限其实就是 带有特殊意义的字符串

RBAC权限模型

RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。

image-20220821202618630

基本表:

  • sys_user:用户表,记录用户的基本信息。
  • sys_role:角色表,记录一些角色信息,比如 项目经理、程序员,项目组组长….
  • sys_menu:权限表,记录一些权限信息,比如,删除权限,更新权限等等…

关系表:

  • sys_user_role:用户和角色是多对多的关系,一个用户可以有多个角色,一个角色可以被多个用户所拥有【这个表描述,一个用户有哪些角色】
  • sys_role_menu:角色和权限也是多对多关系【这个表描述,一个角色有哪些权限】

这样就可以描述了 一个用户有哪些权限了。

当我们要查询一个用户有哪些权限时,SQL语句如下:

1
2
3
4
5
6
7
8
9
10
11
SELECT 
DISTINCT m.`perms`
FROM
sys_user_role ur
LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
WHERE
user_id = 2
AND r.`status` = 0
AND m.`status` = 0

授权过程

准备工作

建表语句

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`id` bigint NOT NULL AUTO_INCREMENT,
`menu_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NULL' COMMENT '菜单名',
`path` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '路由地址',
`component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '组件路径',
`visible` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
`status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
`perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '#' COMMENT '菜单图标',
`create_by` bigint NULL DEFAULT NULL,
`create_time` datetime NULL DEFAULT NULL,
`update_by` bigint NULL DEFAULT NULL,
`update_time` datetime NULL DEFAULT NULL,
`del_flag` int NULL DEFAULT 0 COMMENT '是否删除(0未删除 1已删除)',
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '菜单表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_menu
-- ----------------------------
INSERT INTO `sys_menu` VALUES (2, '部门管理', 'dept', 'system/dept/index', '0', '0', 'system:dept:index', '#', NULL, NULL, NULL, NULL, 0, NULL);
INSERT INTO `sys_menu` VALUES (3, '测试', 'test', 'system/test/index', '0', '0', 'system:test:index', '#', NULL, NULL, NULL, NULL, 0, NULL);

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`role_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色权限字符串',
`status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '角色状态(0正常 1停用)',
`del_flag` int NULL DEFAULT 0 COMMENT 'del_flag',
`create_by` bigint NULL DEFAULT NULL,
`create_time` datetime NULL DEFAULT NULL,
`update_by` bigint NULL DEFAULT NULL,
`update_time` datetime NULL DEFAULT NULL,
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (3, 'CEO', 'ceo', '0', 0, NULL, NULL, NULL, NULL, NULL);
INSERT INTO `sys_role` VALUES (4, 'Coder', 'coder', '0', 0, NULL, NULL, NULL, NULL, NULL);

-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`role_id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`menu_id` bigint NOT NULL DEFAULT 0 COMMENT '菜单id',
PRIMARY KEY (`role_id`, `menu_id`) USING BTREE,
INDEX `menu_id`(`menu_id`) USING BTREE,
CONSTRAINT `menu_id` FOREIGN KEY (`menu_id`) REFERENCES `sys_menu` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `role_id` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role_menu
-- ----------------------------
INSERT INTO `sys_role_menu` VALUES (3, 2);
INSERT INTO `sys_role_menu` VALUES (3, 3);
INSERT INTO `sys_role_menu` VALUES (4, 3);

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NULL' COMMENT '用户名',
`nick_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NULL' COMMENT '昵称',
`password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NULL' COMMENT '密码',
`status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
`email` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱',
`phonenumber` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '手机号',
`sex` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
`avatar` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '头像',
`user_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
`create_by` bigint NULL DEFAULT NULL COMMENT '创建人的用户id',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`update_by` bigint NULL DEFAULT NULL COMMENT '更新人',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
`del_flag` int NULL DEFAULT 0 COMMENT '删除标志(0代表未删除,1代表已删除)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (3, 'TJS', '111', '$2a$10$1s.BtZ6Ay/nU7VB/cgaTv.PiYezHYWOLntRsUqFgter/hsMDViZ0K', '0', NULL, NULL, NULL, NULL, '1', NULL, NULL, NULL, NULL, 0);

-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`user_id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户id',
`role_id` bigint NOT NULL DEFAULT 0 COMMENT '角色id',
PRIMARY KEY (`user_id`, `role_id`) USING BTREE,
INDEX `rold_id`(`role_id`) USING BTREE,
CONSTRAINT `rold_id` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `user_id` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (3, 3);

SET FOREIGN_KEY_CHECKS = 1;

创建权限实体类

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
@TableName(value="sys_menu")
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Menu implements Serializable {
private static final long serialVersionUID = -54979041104113736L;

@TableId
private Long id;

private String menuName;//菜单名

private String path;//路由地址

private String component;//组件路径

private String visible;//菜单状态(0显示 1隐藏)

private String status;//菜单状态(0正常 1停用)

private String perms;//权限标识

private String icon; //菜单图标

private Long createBy;

private Date createTime;

private Long updateBy;

private Date updateTime;

private Integer delFlag;//是否删除(0未删除 1已删除)

private String remark;//备注
}

从数据库查询权限信息

我们只需要根据用户id去查询到其所对应的权限信息即可。

定义一个MenuMapper,在Mapper包下

1
2
3
4
@Mapper
public interface MenuMapper extends BaseMapper<Menu> {
List<String> selectPermsByUserId(Long id);
}

配置文件在 resources/Mapper下创建MenuMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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.example.Mapper.MenuMapper"><!--注意这个命名空间别出错-->
<select id="selectPermsByUserId" resultType="java.lang.String">
SELECT
DISTINCT m.`perms`
FROM
sys_user_role ur
LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
WHERE
user_id = #{userid} <!--user_id需要使用占位符-->
AND r.`status` = 0
AND m.`status` = 0
</select>
</mapper>

在配置文件中,配置如下

1
2
3
mybatis-plus:
mapper-locations: classpath*:/Mapper/**/*.xml
#这表明 classpath下,Mapper包中,任意层级包,任意以xml为结尾的文件。

然后我们要修改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
@Data
//@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {


private User user;
private List<String> permissions;

//存储SpringSecurity所需要的权限信息的集合
@JSONField(serialize = false)
private List<SimpleGrantedAuthority> authorities;

public LoginUser(User user, List<String> permissions) {
this.user = user;
this.permissions = permissions;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(!Objects.isNull(authorities))return authorities;
if(Objects.isNull(permissions))return null;
authorities = new ArrayList<>();
for (String permission : permissions) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
authorities.add(authority);
}
return authorities;
}

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

@Override
public String getUsername() {
return user.getUserName() ;
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}

需要说明的是:

  • private List<String> permissions;代表权限字符串集合。

  • private List<SimpleGrantedAuthority> authorities;代表权限集合,我们要把LoginUser存入到Redis当中,默认情况下不会将SimpleGrantedAuthority进行序列化的,到时候会出问题报异常,因此需要使用@JSONField(serialize = false)禁止让他序列化。

  • getAuthorities():这个方法是用来获取权限信息,需要返回权限信息的集合。

    方法体内就是List<String>转换为List<SimpleGrantedAuthority>

  • SimpleGrantedAuthority:这个类是GrantedAuthority接口的实现类,构造方法是传入一个字符串

  • 还新增了一个构造方法。public LoginUser(User user, List<String> permissions)

UserDetailsService接口的实现时,查询用户的权限,并封装到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
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

@Autowired
private UserService service;

@Autowired
private MenuMapper menuMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据传入了Passward查询用户
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username);
User user = service.getOne(queryWrapper);
//如果没有查询到用户就抛出异常
if(Objects.isNull(user)){
System.out.println("1111");
throw new UsernameNotFoundException("用户名不存在");
}
// TODO 查询对应的权限
List<String> list = menuMapper.selectPermsByUserId(user.getId());

return new LoginUser(user,list);
}
}

封装权限信息到Authentication中。

此时,UserDetails【实现类是LoginUser】中已经有了全新信息。因此只需要从中获取即可。LoginUser.getAuthorities()

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
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

@Autowired
private RedisCache redisCache;

@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//首先需要获取token
String token = httpServletRequest.getHeader("token");
//判断token是否为Null
if(!StringUtils.hasText(token)) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
//如何不为空,解析token,获得了UserId
String userId;
try {
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("toen非法");
}
//根据UserId查redis获取用户数据
String key = "login:"+userId;
LoginUser LoginUser = redisCache.getCacheObject(key);
if(Objects.isNull(LoginUser)){
throw new RuntimeException("用户未登录");
}
//然后封装Authentication对象存入SecurityContextHolder
// TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(LoginUser,null,LoginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);

filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}

限制访问资源所需权限

SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。

但是要使用它我们需要先开启相关配置。

1
@EnableGlobalMethodSecurity(prePostEnabled = true)

然后就可以使用对应的注解。@PreAuthorize

1
2
3
4
5
6
7
8
9
@RestController
public class HelloController {

@RequestMapping("/hello")
@PreAuthorize("hasAuthority('system:test:index')")
public String hello(){
return "hello";
}
}

权限表

image-20220821210526692

权限控制方案

基于注解的权限控制方案

参考:

Spring Security默认是关闭方法注解的,开启它只需要通过引入@EnableGlobalMethodSecurity注解即可:

1
2
3
4
5
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
//...
}

@EnableGlobalMethodSecurity 提供了以下三种方式:

  • prePostEnabled:基于表达式的注解
  • securedEnabled:开启基于角色的注解
  • jsr250Enabled:开启对JSR250的注解。

可以根据需要选择使用这三种的一种或者其中几种。

下面我们大概了解一下

prePostEnabled = true

开启后支持Spring EL表达式,如果没有访问方法的权限,会抛出AccessDeniedException,启用了如下注解:

  • @PreAuthorize:进入方法之前验证授权

    可以使用SPEL表达式或者调用自带的方法,例如:

    1
    2
    @PreAuthorize("#userId == authentication.principal.userId or hasAuthority(‘ADMIN’)")
    void changePassword(long userId ){}

    表示在changePassword方法执行之前,判断方法参数userId的值是否等于principal中保存的当前用户的userId,或者当前用户是否具有ROLE_ADMIN权限,两种符合其一,就可以访问该 方法。

    使用方法进行校验:

    • hasAuthority:只能传入一个权限,只有用户有这个权限才可以访问资源。

    • hasAnyAuthority:可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。

      例如:

      1
      2
      3
      4
      @PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')")
      public String hello(){
      return "hello";
      }
    • hasRole:要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。

      1
      2
      3
      4
      @PreAuthorize("hasRole('system:dept:list')")
      public String hello(){
      return "hello";
      }

      用户有system:dept:list权限是无法访问的,得有ROLE_system:dept:list权限才可以。

    • hasAnyRole:有任意的角色就可以访问。

  • @PostAuthorize:检查授权方法之后才被执行并且可以影响执行方法的返回值

    1
    2
    3
    4
    @PostAuthorize("returnObject.username == authentication.principal.nickName")
    public CustomUser loadUserDetail(String username) {
    return userRoleRepository.loadUserByUserName(username);
    }
  • @PostFilter:在方法执行之后执行,而且这里可以调用方法的返回值,然后对返回值进行过滤或处理或修改并返回

  • @PreFilter:在方法执行之前执行,而且这里可以调用方法的参数,然后对参数值进行过滤或处理或修改

securedEnabled=true

主要开启了@Secured

注解规定了访问访方法的角色列表,在列表中最少指定一种角色

@Secured在方法上指定安全性,要求 角色/权限等 只有对应 角色/权限 的用户才可以调用这些方法。 如果有人试图调用一个方法,但是不拥有所需的 角色/权限,那会将会拒绝访问将引发异常

1
2
3
4
5
6
7
8
9
10
@Secured("ROLE_VIEWER")
public String getUsername() {
SecurityContext securityContext = SecurityContextHolder.getContext();
return securityContext.getAuthentication().getName();
}

@Secured({ "ROLE_DBA", "ROLE_ADMIN" })
public String getUsername2() {
//...
}

@Secured("ROLE_VIEWER") 表示只有拥有ROLE_VIEWER角色的用户,才能够访问getUsername()方法。

@Secured({ "ROLE_DBA", "ROLE_ADMIN" }) 表示用户拥有”ROLE_DBA", "ROLE_ADMIN" 两个角色中的任意一个角色,均可访问 getUsername2 方法。

@Secured,不支持Spring EL表达式

jsr250Enabled = true

  1. @DenyAll:拒绝所有权限
  2. @RolesAllowed:在功能及使用方法上与 @Secured 完全相同
  3. @PermitAll:接受所有权限

基于配置文件的权限控制方案

使用配置文件进行权限控制方案,适合对静态资源进行配置。

在权限可以在配置类中进行配置。

例如:

1
2
3
4
5
6
7
8
9
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter{

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/hello").hasAuthority("system:test:index");
}
}

权限配置

参考链接:

https://blog.csdn.net/qq_41865652/article/details/123685248

https://blog.csdn.net/Shair911/article/details/104181917/

http.authorizeRequests()主要是对url进行访问权限控制,通过这个方法来实现url授权操作。

  • anyRequest(),表示匹配所有的url请求

    1
    2
    3
    http.authorizeRequests()
    // 匹配所有的请求,并且所有请求都需要登录认证
    .anyRequest().authenticated();
  • antMatcher(String regx),传递一个ant表达式参数,表示匹配所有满足ant表达式的请求

    • ant表达式中特殊字符解释

      规则 解释说明
      匹配一个字符
      * 匹配0个或多个字符
      ** 匹配0个或多个目录

      配置类代码示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      http.authorizeRequests()
      // 允许登录页面匿名访问
      .antMatchers("/showLogin", "/errPage").anonymous()
      // 所有的静态资源允许匿名访问
      .antMatchers(
      "/css/**",
      "/js/**",
      "/images/**",
      "/fonts/**",
      "/favicon.ico"
      ).anonymous()
      // 其他所有的请求都需要登录认证
      .anyRequest().authenticated();
  • antMatcher(HttpMethod.*, String regx),传递一个请求方法类型参数加ant表达式参数,表示匹配所有满足ant表达式的指定请求方式的url

    请求方式的枚举类如下:

    image-20220822175402125

    配置类代码示例:

    1
    2
    3
    http.authorizeRequests()
    // 允许GET请求登录页面匿名访问
    .antMatchers(HttpMethod.GET, "/showLogin", "/errPage").anonymous();

访问控制方法

方法名称 方法作用
permitAll() 表示所匹配的URL任何人都允许访问
anonymous() 表示可以匿名访问匹配的URL。和permitAll()效果类似,只是设置为anonymous()的url会执行filterChain中的filter
denyAll() 表示所匹配的URL都不允许被访问。
authenticated() 表示所匹配的URL都需要被认证才能访问
rememberMe() 允许通过remember-me登录的用户访问
access() SpringEl表达式结果为true时可以访问
fullyAuthenticated() 用户完全认证可以访问(非remember-me下自动登录)
hasRole() 如果有参数,参数表示角色,则其角色可以访问
hasAnyRole() 如果有参数,参数表示角色,则其中任何一个角色可以访问
hasAuthority() 如果有参数,参数表示权限,则其权限可以访问
hasAnyAuthority() 如果有参数,参数表示权限,则其中任何一个权限可以访问
hasIpAddress() 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问

配置案例示例:

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
//任何用户都可以访问
http.authorizeRequests().antMatchers("/index").permitAll();
http.authorizeRequests().antMatchers("/index").access("permitAll");]

//任何用户都不能访问
http.authorizeRequests().antMatchers("/home").denyAll();
http.authorizeRequests().antMatchers("/home").access("denyAll");

//认证用户可以访问(除了匿名认证)
http.authorizeRequests().antMatchers("/admin").authenticated();
http.authorizeRequests().antMatchers("/admin").access("authenticated");

//认证用户可以访问(除了匿名认证,记住我)
http.authorizeRequests().antMatchers("/admin").fullyAuthenticated();
http.authorizeRequests().antMatchers("/admin").access("fullyAuthenticated");

//记住我的认证可以访问
http.authorizeRequests().antMatchers("/admin").rememberMe();
http.authorizeRequests().antMatchers("/admin").access("rememberMe");

//匿名用户可以访问
http.authorizeRequests().antMatchers("/admin").anonymous();
http.authorizeRequests().antMatchers("/admin").access("anonymous");

//是否有权限
http.authorizeRequests().antMatchers("/index").hasAuthority("user");
http.authorizeRequests().antMatchers("/index").access("hasAuthority('user')");

//是否有任意一个权限
http.authorizeRequests().antMatchers("/home").hasAnyAuthority("update", "delete", "insert");
http.authorizeRequests().antMatchers("/home").access("hasAnyAuthority('update','delete','insert')");

//spring security中的role并非是一个或多个权限的集合,而是权限的一种,通常以ROLE_开头
//role就是ROLE_开头的权限
//注意:hasRole、hasAnyRole里面的role不需要以ROLE_开头,否则会报异常
//注意:如果在access里面使用hasRole、hasAnyRole则ROLE_前缀可加,可不加
http.authorizeRequests().antMatchers("/index").hasRole("GUEST");
http.authorizeRequests().antMatchers("/index").access("hasRole('GUEST')");
http.authorizeRequests().antMatchers("/admin").hasAuthority("ROLE_GUEST");
http.authorizeRequests().antMatchers("/home").hasAnyRole("GUEST", "USER", "ADMIN");
http.authorizeRequests().antMatchers("/home").access("hasAnyRole('ROLE_GUEST','ROLE_USER','ROLE_ADMIN')");

hasAuthority 源码解读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class SecurityExpressionRoot implements SecurityExpressionOperations {

public SecurityExpressionRoot(Authentication authentication) {
if (authentication == null) {
throw new IllegalArgumentException("Authentication object cannot be null");
} else {
this.authentication = authentication;
}
}

public final boolean hasAuthority(String authority) {
return this.hasAnyAuthority(authority);
}

public final boolean hasAnyAuthority(String... authorities) {
return this.hasAnyAuthorityName((String)null, authorities);
}
//...
}

我们可以看到 hasAuthority() 方法存在于SecurityExpressionRoot类中,返回的是布尔类型。

它调用了hasAnyAuthority()方法。而它又进一步调用了this.hasAnyAuthorityName((String)null, authorities)

hasAnyAuthorityName源码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private boolean hasAnyAuthorityName(String prefix, String... roles) {
Set<String> roleSet = this.getAuthoritySet();
String[] var4 = roles;
int var5 = roles.length;

for(int var6 = 0; var6 < var5; ++var6) {
String role = var4[var6];
String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
if (roleSet.contains(defaultedRole)) {
return true;
}
}

return false;
}

传入了两个参数:前缀字符串 和 **权限字符串(可变参数)**。

  1. Set<String> roleSet = this.getAuthoritySet();获取权限集合,也就是访问该接口,需要哪些权限。
  2. String[] var4 = roles;,这是我们传入的权限,代表用户具有哪些权限。
  3. 然后遍历该权限数组
    1. String role = var4[var6]获取权限。
    2. String defaultedRole = getRoleWithDefaultPrefix(prefix, role);将前缀和权限名拼接,由于传进来的前缀字符串为null,所以拼接之后不变。
    3. if (roleSet.contains(defaultedRole))return true,如果所需权限集合包含该权限返回true。
  4. return false;

由此可见,它内部其实是调用authenticationgetAuthorities方法获取用户的权限列表。然后判断我们存入的方法参数数据在权限列表中。

自定义权限校验方法

如果要自定义权限校验方法

只需要定义一个类,该类使用注解注入到Spring容器中

然后在类中定义一个方法,让它的返回值为布尔类型即可。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
@Component("ex")
public class SGExpressionRoot {

public boolean hasAuthority(String authority){
//获取当前用户的权限
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
List<String> permissions = loginUser.getPermissions();
//判断用户权限集合中是否存在authority
return permissions.contains(authority);
}
}

当我们使用时,需要使用SPEL表达式,

@Bean的名字就可以获取容器中的Bean,然后就可以使用当中的方法。

@Component("ex")可以指定Bean的名字。

1
2
3
4
5
@RequestMapping("/hello")
@PreAuthorize("@ex.hasAuthority('system:dept:list')")//@ex相当于获取容器中bean的名字为ex的对象
public String hello(){
return "hello";
}

__END__