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权限

权限控制操作

  1. 拥有 sys:user 权限的用户,可以访问任意请求方式的 /user
antMatchers("/user").hasAuthority("sys:user")
  1. 拥有 sys:role 权限的用户,可以访问 get 请求方式的 /role
antMatchers(HttpMethod.GET, "/role").hasAuthority("sys:role")
  1. 如果想同时满足多个表达式权限,要使用 access 方法来指定表达式
    如:拥有 sys:permission 权限或 ADMIN 角色,可以访问 get 请求方式的 /permission

注意:指定角色标识后,底层会自动加上前缀 ROLE_ ,所以在给用户授权角色时需要加上前缀

antMatchers(HttpMethod.GET, "/permission").access("hasAuthority('sys:permission') or  hasAnyRole('ADMIN')") //角色会加上前缀 ROLE_,即真实是 ROLE_ADMIN

基于注解控制方法级权限

概述

在Spring Security中实现方法级的安全性,最常见办法是使用特定的注解,将这些注解应用到需要保护的方法上。这些注解不仅可以直接加 controller 方法上, 也可以注解 ServiceDAO 类中的方法.

注解 描述
@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;
    }

登录成功后我们可以获取到他发主体信息,下面是一开始登录进行校验的时候信息

SpringSecurity权限控制_spring

然后最终到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,没有权限访问。

SpringSecurity权限控制_当前用户_02

SpringSecurity权限控制_当前用户_03

授权配置分层管理

概述

当前所有认证请求配置和应用权限配置在安全模块中,而很多权限配置是在应用模块提供的对应服务处理逻辑,这样只有应用模块才知道的这些配置,所以要将它们抽取到应用中进行配置。

实现流程

  1. 创建 AuthorizeConfigurerProvider 授权配置统一接口。
  2. 针对每个功能模块都创建一个 AuthorizeConfigurerProvider 接口的模块权限配置实现类,
    如:用户模块权限配置实现类、角色模块权限配置实现类
  3. 将对应权限配置抽取到对应 AuthorizeConfigurerProvider 的实现类中
  4. 创建一个授权配置管理者接口 AuthorizeConfigurerManager 管理所有的授权配置
  5. 通过 AuthorizeConfigurerManager 接口实现类,将 AuthorizeConfigureProvider 所有的授权配置实现类全部加载到容器中。

创建授权配置提供者

  1. 方法参数是 HttpSecurity 中 authorizeRequests() 返回值 ,因为都是基于它进行授权配置的。
/**
 * @author WGR
 * @create 2021/8/7 -- 15:26
 */
public interface AuthorizeConfigurerProvider {

    /**
     * 参数为 authorizeRequests() 的返回值
     * @param config
     */
    void configure(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config);
}
  1. 实现 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')");
    }
}
  1. 创建关于系统管理模块相关的授权配置提供者实现 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();
    }
}

创建授权配置管理者

  1. 方法参数是 HttpSecurity 中 authorizeRequests() 返回值 ,它用于接收 SpringSecurityConfig 传递过来 的。

/**
 * @author WGR
 * @create 2021/8/7 -- 15:31
 */
public interface AuthorizeConfigurerManager {

    void configure(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry
                           config);
}
  1. 加载所有的授权配置类,也就是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();
    }
}
  1. 将 SpringSecurityConfig#configure(HttpSecurity http) 方法中请求放行和授权代码删除,使用 AuthorizeConfigurerManager 处理授权配置。

SpringSecurity权限控制_方法调用_04