Spring EL 权限表达式
Spring Security 允许我们使用 Spring EL 表达式,来进行用户权限的控制,如果对应的表达式结果返回true,则表示拥有对应的权限,反之则无。
Spring Security 可用表达式对象的基类是 SecurityExpressionRoot
注意: 表达式以下面为准, 黄色标注的 is 在使用时是要加上的
表达式 | 描述 |
---|---|
permitAll() | 总是返回true,表示允许所有访问(认证不认证都可访问 URL或方法 ) |
denyAll() | 总是返回false,表示拒绝所有访问(永远访问不到指定的 URL或方法 ) |
isAnonymous() | 当前用户是一个匿名用户(未登录用户)允许访问,返回true |
isRememberMe() | 当前用户是通过Remember-Me自动登录的允许访问,返回true |
isAuthenticated() | 当前用户是已经登录认证成功的允许访问(包含了rememberMe自动 登录的),返回true |
isFullyAuthenticated() | 如果当前用户既不是一个匿名用户,同时也不是通过Remember-Me自 动登录的,则允许访问(可以理解为通过页面输入帐户信息认证的)。 |
hasRole(String role) | 当前用户拥有指定角色权限的允许访问,返回true。注意: 指定的角色 名(如: ADMIN ) SpringSecurity 底层会在前面拼接 ROLE_ 字符串,所 以在UserDetailsService实现类,数据库返回的角色名要有 ROLE_ADMIN |
hasAnyRole([role1, role2]) | 多个角色以逗号分隔的字符串。如果当前用户拥有指定角色中的任意一 个则允许访问,返回true。 |
hasAuthority(String authority) | 当前用户拥有指定权限标识的允许访问,返回true。注意:和 hasRole 区别是, hasAuthority 不会在前面拼接 ROLE_ 字符串, 。 |
hasAnyAuthority([auth1,auth2]) | 多个权限标识是以逗号分隔的字符串。如果当前用户拥有指定权限标识 中的任意一个则允许访问,返回true |
hasIpAddress("192.168.1.1/29") | 限制指定IP或指定范围内的IP才可以访问 |
表达式控制URL权限
权限控制操作
- 拥有 sys:user 权限的用户,可以访问任意请求方式的 /user
antMatchers("/user").hasAuthority("sys:user")
- 拥有 sys:role 权限的用户,可以访问 get 请求方式的 /role
antMatchers(HttpMethod.GET, "/role").hasAuthority("sys:role")
- 如果想同时满足多个表达式权限,要使用 access 方法来指定表达式
如:拥有 sys:permission 权限或 ADMIN 角色,可以访问 get 请求方式的 /permission
注意:指定角色标识后,底层会自动加上前缀 ROLE_ ,所以在给用户授权角色时需要加上前缀
antMatchers(HttpMethod.GET, "/permission").access("hasAuthority('sys:permission') or hasAnyRole('ADMIN')") //角色会加上前缀 ROLE_,即真实是 ROLE_ADMIN
基于注解控制方法级权限
概述
在Spring Security中实现方法级的安全性,最常见办法是使用特定的注解,将这些注解应用到需要保护的方法上。这些注解不仅可以直接加 controller 方法上, 也可以注解 Service 或 DAO 类中的方法.
注解 | 描述 |
---|---|
@PreAuthorize(表 达式) | 在方法调用前进行权限检查,表达式为 true 允许调用, 反之则无权限调用 |
@PostAuthorize(表 达式) | 在方法调用后进行权限检查,如果表达式计算结果为false,抛出异常403不允许访问. returnObject 代表方法的返回值, 可以使用 returnObject 对方法返回值进行验证 |
@PreFilter(表达式) | 允许方法调用,但必须在进入方法之前过滤方法参数值。 |
@PostFilter(表达 式) | 允许方法调用,但必须按照表达式来过滤方法的返回值。 returnObject 代表方法的返回值, 可以使用 returnObject 对方法返回值进行验证 |
表达式中可以使用 # 号 ,比如:
在表达式中, 可以使用 #user 来代表方法中的参数 user
@PreAuthorize ("#user.name == authentication.principal.username")
public void deleteUser(User user){}
默认方法级权限控制默认是关闭的,要手动开启 @EnableGlobalMethodSecurity(prePostEnabled = true)
控制方法访问权限
@PreAuthorize(权限表达式) 和 @PostAuthorize(权限表达式)
// 有'sys:user:add' 或 'sys:user:edit'权限的可以访问
@PreAuthorize("hasAnyAuthority('sys:user:add','sys:user:edit')")
@GetMapping(value = {"/form"})
public String form() {
return HTML_PREFIX + "user-form";
}
// 返回值的code等于200,则调用成功,不然抛出403不允许访问
@PostAuthorize("returnObject.code == 200")
@RequestMapping("/{id}")
@ResponseBody
public Result deleteById(@PathVariable Long id) {
if(id < 0) {
return Result.build(500, "参数不能小于0");
}
return Result.ok();
}
过滤方法的参数和返回值
使用 @PreFilter 和 @PostFilter 可以对参数或返回值进行过滤。Spring Security 将移除使对应表达式的结果为false的元素。
// 测试:http://localhost/user/batch/-1,0,1,2 ,响应 {"code":200,"message":"OK","data":[1,2]}
// filterTarget 过滤集合属性名, filterObject集合中的元素,只接收id>0的数据
@PreFilter(filterTarget = "ids", value = "filterObject > 0")
@RequestMapping("/batch/{ids}")
@ResponseBody
public Result deleteByIds(@PathVariable List<Long> ids) {
return Result.ok(ids);
}
@PostFilter("filterObject != authentication.principal.username")
@RequestMapping("/list")
@ResponseBody
public List<String> page() {
List<String> userList = Lists.newArrayList("dalianpai", "test", "admin");
return userList;
}
登录成功后我们可以获取到他发主体信息,下面是一开始登录进行校验的时候信息
然后最终到DaoAuthenticationProvider的retrieveUser,拿到我们自定义的UserDetails实现类,来拿到它的用户权限信息
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
如果登录成功,则会改写principal信息,当权限控制到每个接口的时候,就会从主体信息中判断,是否有改权限,有的话则通过,没有则报错403,没有权限访问。
授权配置分层管理
概述
当前所有认证请求配置和应用权限配置在安全模块中,而很多权限配置是在应用模块提供的对应服务处理逻辑,这样只有应用模块才知道的这些配置,所以要将它们抽取到应用中进行配置。
实现流程
- 创建 AuthorizeConfigurerProvider 授权配置统一接口。
- 针对每个功能模块都创建一个 AuthorizeConfigurerProvider 接口的模块权限配置实现类,
如:用户模块权限配置实现类、角色模块权限配置实现类 - 将对应权限配置抽取到对应 AuthorizeConfigurerProvider 的实现类中
- 创建一个授权配置管理者接口 AuthorizeConfigurerManager 管理所有的授权配置
- 通过 AuthorizeConfigurerManager 接口实现类,将 AuthorizeConfigureProvider 所有的授权配置实现类全部加载到容器中。
创建授权配置提供者
- 方法参数是 HttpSecurity 中 authorizeRequests() 返回值 ,因为都是基于它进行授权配置的。
/**
* @author WGR
* @create 2021/8/7 -- 15:26
*/
public interface AuthorizeConfigurerProvider {
/**
* 参数为 authorizeRequests() 的返回值
* @param config
*/
void configure(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config);
}
- 实现 AuthorizeConfigurerProvider 接口将 SpringSecurityConfig#configure 方法中关于身份认证相关请求配置抽取到这个 config 中
/**
* @author WGR
* @create 2021/8/7 -- 15:29
*/
@Component
public class SystemAuthorizeConfigurerProvider implements AuthorizeConfigurerProvider {
@Override
public void configure(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) {
config.antMatchers("/user").hasAuthority("sys:user")
.antMatchers(HttpMethod.GET, "/role").hasAuthority("sys:role")
.antMatchers(HttpMethod.GET, "/permission")
//角色会加上前缀 ROLE_,即真实是 ROLE_ADMIN
.access("hasAuthority('sys:permission') or hasAnyRole('ADMIN')");
}
}
- 创建关于系统管理模块相关的授权配置提供者实现 AuthorizeConfigurerProvider 接口将 SpringSecurityConfig#configure 方法中关于用户、角色、菜单模块授权配置抽取到这个 config 中
/**
* @author WGR
* @create 2021/8/7 -- 15:27
*/
@Component
public class CustomAuthorizeConfigurationProvider implements AuthorizeConfigurerProvider{
@Autowired
private SecurityProperties securityProperties;
@Override
public void configure(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) {
// 放行/login/page不需要认证可访问
config.antMatchers(securityProperties.getAuthentication().getLoginPage(),
securityProperties.getAuthentication().getImageCodeUrl(),
securityProperties.getAuthentication().getMobilePage(),
securityProperties.getAuthentication().getMobileCodeUrl()
).permitAll();
}
}
创建授权配置管理者
- 方法参数是 HttpSecurity 中 authorizeRequests() 返回值 ,它用于接收 SpringSecurityConfig 传递过来 的。
/**
* @author WGR
* @create 2021/8/7 -- 15:31
*/
public interface AuthorizeConfigurerManager {
void configure(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry
config);
}
- 加载所有的授权配置类,也就是AuthorizeConfigurerProvider接口所有子类实例实现 AuthorizeConfigurerManager 接口
/**
* @author WGR
* @create 2021/8/7 -- 15:31
*/
@Component
public class CustomAuthorizeConfigurationManager implements AuthorizeConfigurerManager{
@Autowired
List<AuthorizeConfigurerProvider> authorizeConfigurerProviders;
@Override
public void configure(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) {
for(AuthorizeConfigurerProvider provider : authorizeConfigurerProviders) {
provider.configure(config);
}
// 除了 AuthorizeConfigurerProvider 实现类中配置的,其他请求都需要身份认证
config.anyRequest().authenticated();
}
}
- 将 SpringSecurityConfig#configure(HttpSecurity http) 方法中请求放行和授权代码删除,使用 AuthorizeConfigurerManager 处理授权配置。