Spring Boot 集成Spring Security权限控制

592 阅读3分钟

前言

上一章讲解了Spring Boot集成Spring Security实现了登录认证,本文将讲解Spring Security实现权限控制

实现权限控制有如下几种方式:

  • 表达式控制URL权限
  • 注解方式控制权限
  • 动态控制权限

表达式控制URL权限

Spring Security支持在URL和方法权限控制时使用SpEL表达式,如果表达式返回值为true则表示需要对应的权限,否则表示不需要对应的权限。Spring Secuity提供了如下的SPEL表达式

图片.png

权限配置

protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/app/sys/user/roleTest").hasRole("user")
               //未授权
               .exceptionHandling().accessDeniedPage( "/403" );
  }
 

说明:访问/app/sys/user/roleTest请求需要user角色,否则会跳转到403

测试结果

用户登录成功后,访问/app/sys/usr/roleTest,没有权限则跳转到403页面

图片.png

采用注解表达式权限控制

Spring Security也提供了如下注解,可以采用注解表达式的方式进行权限控制

  • @PreAuthorize:方法执行前进行权限检查
  • @PostAuthorize:方法执行后进行权限检查
  • @Secured:类似于 @PreAuthorize

权限配置

通过注解的方式,首先需要开启注解,即设置securedEnabled和prePostEnabled指为true

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
}

示例代码

//用户id为admin
@PreAuthorize("principal.username.equals('admin')")
    public String hello() {
        return "hello";
    }

//用户角色为admin的才能访问
@PreAuthorize("hasRole('ROLE_admin')")
    @RequestMapping("/roleTest")
    public String roleTest()
    {
        return "成功";
    }

注意:Spring Security中的角色默认是以Role_角色id

测试结果

用户登录成功后,访问/app/sys/usr/roleTest,没有权限则跳转到403页面

图片.png

动态控制权限

动态权限主要通过重写拦截器和决策器来实现。

权限验证流程

图片.png

实现步骤

自定义元数据加载器

@Component
public class CustomInvocationSecurityMetadataSourceService implements
        FilterInvocationSecurityMetadataSource
{
    private Logger logger =LoggerFactory.getLogger(getClass());
    
    @Autowired
    private UserMapper userMapper;
    
    public Collection<ConfigAttribute> getAllConfigAttributes()
    {
        return null;
    }

    private List<String> allowedRequest(){
        return Arrays.asList("/login","/css/**","/fonts/**","/js/**","/scss/**","/img/**");
    }
    
    /**
     * 判断当前请求是否在允许请求的范围内
     * @param fi 当前请求
     * @return 是否在范围中
     */
    private boolean isMatcherAllowedRequest(FilterInvocation fi){
        return allowedRequest().stream().map(AntPathRequestMatcher::new)
                .filter(requestMatcher -> requestMatcher.matches(fi.getHttpRequest()))
                .toArray().length > 0;
    }

    /**
     * 此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中则返回给 decide方法。
     */
    public Collection<ConfigAttribute> getAttributes(Object o)
            throws IllegalArgumentException
    {
        // 请求url
        FilterInvocation filterInvocation = (FilterInvocation) o;
        String requestUrl = filterInvocation.getRequestUrl();
        logger.info("request Url:{}",requestUrl);
        
        //过滤特殊的url
        if (isMatcherAllowedRequest(filterInvocation)) 
        {
            return null; 
        }
        
        List<String> roles = getRolesByRes(requestUrl);
        logger.info("{} 对应的角色列表{}",requestUrl,roles);
        if (CollectionUtils.isEmpty(roles)) 
        {
            return null;
        }
        String[] attributes = roles.toArray(new String[0]);
        return SecurityConfig.createList(attributes);
    }

    public boolean supports(Class<?> arg0)
    {
        return true;
    }
    
    public List<String> getRolesByRes(String url) 
    {
        //加载所有权限数据
        List<Func> funcs = userMapper.getRolesByRes(url);

        if(funcs==null || funcs.isEmpty())
        {
            return null;
        }
        
        List<String> roles =new ArrayList<>();
        for (Func func : funcs) 
        {
            String roleId ="ROLE_"+func.getRoleId();
            roles.add(roleId);
        }
        return roles;
    }
}

说明:查询当前访问的URL所对应的角色信息,如果存在则返回给AccessDecisionManager处理,如果为空则不处理。

自定义决策管理器

/**
 * @Description 自定义权限决策管理器
 */

@Component
public class CustomAccessDecisionManager implements AccessDecisionManager
{
    private final static Logger logger = LoggerFactory.getLogger(CustomAccessDecisionManager.class);
    
    @Override
    public void decide(Authentication authentication, Object object,
            Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
            InsufficientAuthenticationException
    {
        if (null == configAttributes || configAttributes.isEmpty()) 
        {
           logger.error("configAttributes is emtpy");
           return;
        } 
        else 
        {
            //判断是否有权限
            for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) 
            {
                String needRole = iter.next().getAttribute();
                for(GrantedAuthority ga : authentication.getAuthorities()) {
                    if(needRole.trim().equals(ga.getAuthority().trim()))
                    {
                        return;
                    }
                }
            }
            //没有权限报错
            throw new AccessDeniedException("当前访问没有权限");
        }
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute)
    {
        return true;
    }

    @Override
    public boolean supports(Class<?> clz)
    {
        return true;
    }

}

说明:AccessDecisionManager 有三个默认实现:

AffirmativeBased 基于肯定的决策器。 用户持有一个同意访问的角色就能通过。 ConsensusBased 基于共识的决策器。 用户持有同意的角色数量多于禁止的角色数。 UnanimousBased 基于一致的决策器。 用户持有的所有角色都同意访问才能放行。

默认采用的是第一种投票决策器

未登录权限处理

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint
{
    private Logger logger =LoggerFactory.getLogger(CustomAuthenticationEntryPoint.class);
    @Override
    public void commence(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException
    {
        logger.info("user not login AuthenticationEntryPoint");
        response.setCharacterEncoding("utf-8");
        response.setContentType("text/javascript;charset=utf-8");
        Map<String,String> map = new HashMap<>();
        map.put("code", "403");
        map.put("msg","用户未登录,无访问权限");
        response.getWriter().print(map);
        
    }
}

无权限访问处理

public class CustomAccessDeineHandler implements AccessDeniedHandler
{
    private Logger logger = LoggerFactory.getLogger(CustomAccessDeineHandler.class);
    
    public void handle(HttpServletRequest request, HttpServletResponse response,
            AccessDeniedException ex) throws IOException, ServletException 
    {
        logger.info("user have not permisson CustomAccessDeineHandler");
        response.setCharacterEncoding("utf-8");
        response.setContentType("text/javascript;charset=utf-8");
        Map<String,String> map = new HashMap<>();
        map.put("code", "403");
        map.put("msg","用户无访问权限");
        response.getWriter().print(JSONObject.toJSONString(map));
    }
}

说明:用户未登录,且请求的URL不是的通用的,则会进行拦截。

修改Spring Security配置

 protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login","/register","/login-error","/register-save","/error","/css/**","/js/**").permitAll()
                //任何请求需要认证
                .anyRequest().authenticated()
                // 登录页面
                .and().formLogin().loginPage("/login")
                .failureUrl("/login-error").defaultSuccessUrl("/index")
                //登出
                .and().logout().logoutUrl("/logout")
                //关闭跨域访问
                .and().csrf().disable()
                //未授权页面访问
                .exceptionHandling().accessDeniedPage( "/403" )
                //设置验证
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setSecurityMetadataSource(customInvocationSecurityMetadataSourceService);
                        o.setAccessDecisionManager(customAccessDecisionManager);
                        return o;
                    }
                });
                //未登录验证、鉴权异常
                http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                .accessDeniedHandler(new CustomAccessDeineHandler());
    }

说明:用户登录,无权限访问的请求,则会进行拦截。

测试

系统配置了User和admin角色以及user和admin用户

用户未登录

用户未登录,访问/app/sys/user/list,显示如下信息

图片.png

用户已登录,user用户访问

用户已登录,无权限访问/app/sys/user/list,

图片.png

用户已登录,admin用户访问权限

用户已登录,有权限访问/app/sys/user/list

图片.png

总结

本文介绍了Spring Security实现权限控制的几种方法,如有疑问,可以随时反馈交流。