授权系统的作用
例如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书相关的功能,不可能让他看到并且去使用添加书籍信息,删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了,应该就能看到并使用添加书籍信息,删除书籍信息等功能。
总结起来就是不同的用户可以使用不同的功能。这就是权限系统要去实现的效果。
我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果有人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作。
所以我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须具有所需权限才能进行相应的操作。
授权的基本流程
在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
public class UserDetailsServiceImpl implements UserDetailsService {
private UserService service;
private MenuMapper menuMapper;
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
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private RedisCache redisCache;
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)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。
基本表:
- sys_user:用户表,记录用户的基本信息。
- sys_role:角色表,记录一些角色信息,比如 项目经理、程序员,项目组组长….
- sys_menu:权限表,记录一些权限信息,比如,删除权限,更新权限等等…
关系表:
- sys_user_role:用户和角色是多对多的关系,一个用户可以有多个角色,一个角色可以被多个用户所拥有【这个表描述,一个用户有哪些角色】
- sys_role_menu:角色和权限也是多对多关系【这个表描述,一个角色有哪些权限】
这样就可以描述了 一个用户有哪些权限了。
当我们要查询一个用户有哪些权限时,SQL
语句如下:
1 | SELECT |
授权过程
准备工作
建表语句
1 | SET NAMES utf8mb4; |
创建权限实体类
1 |
|
从数据库查询权限信息
我们只需要根据用户id去查询到其所对应的权限信息即可。
定义一个MenuMapper
,在Mapper包下
1 |
|
配置文件在 resources/Mapper下创建MenuMapper.xml
1 |
|
在配置文件中,配置如下
1 | mybatis-plus: |
然后我们要修改UserDetails的实现类
因为我们要把权限信息封装进去。
1 |
|
需要说明的是:
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 |
|
封装权限信息到Authentication
中。
此时,UserDetails
【实现类是LoginUser
】中已经有了全新信息。因此只需要从中获取即可。LoginUser.getAuthorities()
1 |
|
限制访问资源所需权限
SpringSecurity
为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。
但是要使用它我们需要先开启相关配置。
1 |
然后就可以使用对应的注解。@PreAuthorize
1 |
|
权限表
权限控制方案
基于注解的权限控制方案
参考:
Spring Security默认是关闭方法注解的,开启它只需要通过引入@EnableGlobalMethodSecurity
注解即可:
1 |
|
@EnableGlobalMethodSecurity
提供了以下三种方式:
prePostEnabled
:基于表达式的注解securedEnabled
:开启基于角色的注解jsr250Enabled
:开启对JSR250的注解。
可以根据需要选择使用这三种的一种或者其中几种。
下面我们大概了解一下
prePostEnabled = true
开启后支持Spring EL表达式,如果没有访问方法的权限,会抛出AccessDeniedException
,启用了如下注解:
@PreAuthorize:进入方法之前验证授权
可以使用SPEL表达式或者调用自带的方法,例如:
1
2
void changePassword(long userId ){}表示在
changePassword
方法执行之前,判断方法参数userId
的值是否等于principal
中保存的当前用户的userId
,或者当前用户是否具有ROLE_ADMIN
权限,两种符合其一,就可以访问该 方法。使用方法进行校验:
hasAuthority
:只能传入一个权限,只有用户有这个权限才可以访问资源。hasAnyAuthority
:可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。例如:
1
2
3
4
public String hello(){
return "hello";
}hasRole
:要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。1
2
3
4
public String hello(){
return "hello";
}用户有
system:dept:list
权限是无法访问的,得有ROLE_system:dept:list
权限才可以。hasAnyRole
:有任意的角色就可以访问。
@PostAuthorize:检查授权方法之后才被执行并且可以影响执行方法的返回值
1
2
3
4
public CustomUser loadUserDetail(String username) {
return userRoleRepository.loadUserByUserName(username);
}@PostFilter:在方法执行之后执行,而且这里可以调用方法的返回值,然后对返回值进行过滤或处理或修改并返回
@PreFilter:在方法执行之前执行,而且这里可以调用方法的参数,然后对参数值进行过滤或处理或修改
securedEnabled=true
主要开启了@Secured
注解规定了访问访方法的角色列表,在列表中最少指定一种角色
@Secured
在方法上指定安全性,要求 角色/权限等 只有对应 角色/权限 的用户才可以调用这些方法。 如果有人试图调用一个方法,但是不拥有所需的 角色/权限,那会将会拒绝访问将引发异常
1 |
|
@Secured("ROLE_VIEWER")
表示只有拥有ROLE_VIEWER
角色的用户,才能够访问getUsername()
方法。
@Secured({ "ROLE_DBA", "ROLE_ADMIN" })
表示用户拥有”ROLE_DBA", "ROLE_ADMIN"
两个角色中的任意一个角色,均可访问 getUsername2
方法。
@Secured,不支持Spring EL表达式
jsr250Enabled = true
- @DenyAll:拒绝所有权限
- @RolesAllowed:在功能及使用方法上与
@Secured
完全相同 - @PermitAll:接受所有权限
基于配置文件的权限控制方案
使用配置文件进行权限控制方案,适合对静态资源进行配置。
在权限可以在配置类中进行配置。
例如:
1 |
|
权限配置
参考链接:
http.authorizeRequests()
主要是对url
进行访问权限控制,通过这个方法来实现url
授权操作。
anyRequest()
,表示匹配所有的url
请求1
2
3http.authorizeRequests()
// 匹配所有的请求,并且所有请求都需要登录认证
.anyRequest().authenticated();antMatcher(String regx)
,传递一个ant
表达式参数,表示匹配所有满足ant
表达式的请求ant表达式中特殊字符解释
规则 解释说明 ? 匹配一个字符 * 匹配0个或多个字符 ** 匹配0个或多个目录 配置类代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13http.authorizeRequests()
// 允许登录页面匿名访问
.antMatchers("/showLogin", "/errPage").anonymous()
// 所有的静态资源允许匿名访问
.antMatchers(
"/css/**",
"/js/**",
"/images/**",
"/fonts/**",
"/favicon.ico"
).anonymous()
// 其他所有的请求都需要登录认证
.anyRequest().authenticated();
antMatcher(HttpMethod.*, String regx)
,传递一个请求方法类型参数加ant表达式参数,表示匹配所有满足ant表达式的指定请求方式的url
请求方式的枚举类如下:
配置类代码示例:
1
2
3http.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 | //任何用户都可以访问 |
hasAuthority 源码解读
1 | public abstract class SecurityExpressionRoot implements SecurityExpressionOperations { |
我们可以看到 hasAuthority()
方法存在于SecurityExpressionRoot
类中,返回的是布尔类型。
它调用了hasAnyAuthority()
方法。而它又进一步调用了this.hasAnyAuthorityName((String)null, authorities)
。
hasAnyAuthorityName
源码如下。
1 | private boolean hasAnyAuthorityName(String prefix, String... roles) { |
传入了两个参数:前缀字符串 和 **权限字符串(可变参数)**。
Set<String> roleSet = this.getAuthoritySet();
获取权限集合,也就是访问该接口,需要哪些权限。String[] var4 = roles;
,这是我们传入的权限,代表用户具有哪些权限。- 然后遍历该权限数组
String role = var4[var6]
获取权限。String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
将前缀和权限名拼接,由于传进来的前缀字符串为null,所以拼接之后不变。if (roleSet.contains(defaultedRole))return true
,如果所需权限集合包含该权限返回true。
return false;
由此可见,它内部其实是调用
authentication
的getAuthorities
方法获取用户的权限列表。然后判断我们存入的方法参数数据在权限列表中。
自定义权限校验方法
如果要自定义权限校验方法
只需要定义一个类,该类使用注解注入到Spring容器中。
然后在类中定义一个方法,让它的返回值为布尔类型即可。
例如:
1 |
|
当我们使用时,需要使用SPEL
表达式,
@Bean的名字
就可以获取容器中的Bean,然后就可以使用当中的方法。
@Component("ex")
可以指定Bean的名字。
1 |
|
__END__