SpringSecurity - 简单前后端分离 - 自定义授权篇

SpringSecurity 学习指南大全
SpringSecurity - 前后端分离简单实战 - 环境准备
SpringSecurity - 前后端分离简单实战 - 自定义认证篇
开源 Spring Security 前后端分离 - 后端示例

SpringSecurity - 简单前后端分离 - 自定义授权篇

隔了那么久,终于来补坑了,(/▽\)。

我们接着介绍自定义授权是如何做的。环境准备和前面一样,接着认证篇,继续完成我们的授权处理。

简单理论

授权:授权其实就是确认当前登录系统的用户能够干些什么事情。前面的认证篇,我们可以确定哪些用户是我们系统的用户,而授权则是在认证完成的基础上来确定当前的用户能在我们系统中干些什么事情,也就是有些什么功能。

当前权限的控制也分很多种,权限的级别,一般就是指权限的粒度。比如只控制用户登录,登录后就拥有所有的权限,这种就是粗粒度的权限。

我们可以继续细分权限,比如权限粒度控制到菜单级别,这样用户登录后,拥有自己的菜单,不同的用户拥有不同的菜单权限,这也是比较简单的基于菜单的权限控制。

我们接着细化,再控制深一点,就是接口权限,不仅包含菜单权限的控制,并且精确到用户调用接口的权限,也就是功能权限,比如一个数据列表,有的用户可以修改数据,而有的用户只有查看的权限。这种权限控制比较常见,很多管理系统就是这种权限。

再细化就是数据权限了,不仅控制菜单和功能,还控制用户可以查看的数据,这种权限控制有的是在数据库添加权限字段,有的是封装查询的条件,通过用户的数据权限配置来查询符合规定的数据。比如一个用户只能查看一个功能数据的前100条等等。基于数据库的权限控制。

常见的权限级别:登录权限、菜单权限、功能权限、数据权限

当然除了前面的权限,我们还可以根据自己的业务常见定制化其它的权限控制。我这里演示的授权是基于请求路径的权限拦截。也相当于接口权限。

对于权限的数据库设计,大家可以参考网上的其它文章,比如经典的 RBAC 权限模型。

Security 的权限架构

在我之前写的文章中已经详细介绍了授权和认证架构。
SpringSecurity - 基于 Servlet 的应用程序

基本上就是,Security 已经帮我们做了一些默认的权限认证了,默认的权限控制只是认证成功的就行,

Security 在做权限认证时使用的是 AuthorizationFilter,它会拦截所有的请求,并调用 AuthorizationManagers 来处理权限认证,AuthorizationManager 是个接口,它包含两个方法。

AuthorizationDecision check(Supplier<Authentication> authentication, Object secureObject);

default AuthorizationDecision verify(Supplier<Authentication> authentication, Object secureObject)
        throws AccessDeniedException {
    AuthorizationDecision decision = check(authentication, object);
    if (decision != null && !decision.isGranted()) {
        throw new AccessDeniedException("Access Denied");
    }
}

check() 方法就是我们需要实现的方法,verify() 方法就是校验当前认证对象的权限,它会调用 check() 方法来进行授权。

AuthorizationDecision 对象就是授权决策对象,如果允许访问,则返回一个通过的 AuthorizationDecision 授权决策,如果访问拒绝,则返回拒绝的 AuthorizationDecision 授权决策,如果放弃做出决策,则返回一个 NULL 的 AuthorizationDecision。

我们可以看到,当认证对象授权不通过时就会抛出 AccessDeniedException 异常。来提示用户拒绝访问。

我们可以自定义授权管理器,当然也可以使用 Security 提供的授权处理器。新版我们只需要提供实现 AuthorizationManager 的权限控制即可,而老版则需要使用 AccessDecisionVoter 来实现权限控制。

Security 提供的默认实现如下图:
在这里插入图片描述
具体介绍可查看我之前的文档或官方文档。

所有要想自定义授权,第一步则是先自定义授权管理器。

自定义授权管理器

@Slf4j
@Component
public class RequestAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    @Override
    public void verify(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
        AuthorizationManager.super.verify(authentication, object);
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext requestAuthorizationContext) {
        System.err.println("授权处理----------------");
        boolean granted = isGranted(authentication.get(),requestAuthorizationContext.getRequest());
		// 返回授权决策对象,根据权限结果
        return new AuthorizationDecision(granted);
    }

    private boolean isGranted(Authentication authentication, HttpServletRequest request) {
        return authentication != null && isAuthorized(authentication,request);
    }

    private boolean isAuthorized(Authentication authentication,HttpServletRequest request) {
		// 自定义的权限控制,request 可以获取到当前的请求信息。
		// authentication 就是我们的认证对象,我们可以直接拿到认证用户的权限
		
        //TODO 查询缓存中用户的权限
        // anonymousUser 匿名用户 ROLE_ANONYMOUS 默认权限
        String principal = (String) authentication.getPrincipal();
        if(principal.equals("anonymousUser")){
            return false;
        }

        return true;
    }
}

授权决策对象很简单,直接上源码

public class AuthorizationDecision {

	private final boolean granted;

	public AuthorizationDecision(boolean granted) {
		this.granted = granted;
	}

	public boolean isGranted() {
		return this.granted;
	}

	@Override
	public String toString() {
		return getClass().getSimpleName() + " [granted=" + this.granted + "]";
	}

}

里面就一个 boolean 的 granted 属性,来判断认证对象是否拥有权限。

AuthorizationManager 的泛型可以参加它默认的实现类。需要注意的一点是,未登录的用户是匿名用户,如果你想对匿名用户放开哪些权限可以直接在这里设置,我是全部改为false。不允许匿名用户访问。

匿名用户的用户名可以在 Security 的配置类中指定,默认为 anonymousUser。

这样,我们的一个自定义授权管理器就完成了,比老版要简单很多。

除了认证管理器,我们还需要处理授权失败的异常。Security 也提供了自定义的入口。

自定义授权异常处理

只需要实现 AccessDeniedHandler 处理器即可。

@Component
public class JsonAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        //响应状态
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        //返回Json格式
        response.setHeader("Content-Type","application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.print(JsonUtil.objectToJson(JsonResult.result(false, ResultCode.SC_FORBIDDEN,accessDeniedException.getMessage())));
        writer.flush();
        writer.close();
    }
}

返回权限不足的json。当授权管理器抛出 AccessDeniedException 异常时,就会执行 AccessDeniedHandler。

有了自定义的授权管理器,和授权异常,接下来就是配置了。

配置自定义授权

还是我们认证篇使用的配置类。

@EnableWebSecurity
public class WebSecurityConfig {

    @Autowired
    JsonAuthenticationFilter jsonAuthenticationFilter;
    @Autowired
    JsonTokenAuthenticationFilter jsonTokenAuthenticationFilter;
    @Autowired
    SecurityAuthenticationEntryPoint authenticationEntryPoint;
    @Autowired
    RequestAuthorizationManager requestAuthorizationManager;
    @Autowired
    JsonAccessDeniedHandler jsonAccessDeniedHandler;
    @Autowired
    JsonLogoutHandler jsonLogoutHandler;
    @Autowired
    JsonLogoutSuccessHandler jsonLogoutSuccessHandler;

    // 核心配置
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http.csrf().disable();
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        //TODO token加密签名验证
        //token简单验证
        http.addFilterBefore(jsonTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        //token登录
        http.addFilterBefore(jsonAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

		// 配置授权处理,授权管理器可以配置多个
        http.authorizeHttpRequests()
                .antMatchers(HttpMethod.POST,"/login").permitAll()
                .anyRequest().access(requestAuthorizationManager);

        // 退出处理,后面介绍
        http.logout().addLogoutHandler(jsonLogoutHandler).logoutSuccessHandler(jsonLogoutSuccessHandler);

        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(jsonAccessDeniedHandler);// 授权异常配置

        return http.build();
    }
}

我们只需要修改基于路径的请求,为它添加自定义的授权管理器即可。

可以看到,处理 /login 的请求全部放行,其它的请求都会经过我们自定义的授权管理器 requestAuthorizationManager。可以配置多个授权管理器来进行处理。

到这里我们简单的自定义授权已经添加完成了。剩下的就是细节的优化。

查看结果,由于我这里的权限是全部放开的,所以测试效果不是很好。当未登录的请求直接被 AuthenticationEntryPoint 处理了。

在这里插入图片描述
要想测试出效果,则需要对当前用户进行权限处理,我这里是全部放开的。
在这里插入图片描述
具体的权限处理,则是获取用户的权限集合,判断当前的请求路径是否符合,符合则返回true,否则返回false。verify() 方法就会抛出授权异常。

一个简单的自定义授权就完成了。

自定义退出

我们如果要自定义退出处理,Security 也提供了相应的处理类。
主要设计两个类,一个是退出的处理类 LogoutHandler,和退出成功的处理类LogoutSuccessHandler

我们只需要实现这两个类,并且配置到我们自定义权限里即可。

@Slf4j
@Component
public class JsonLogoutHandler implements LogoutHandler {

    @Autowired
    RedisClient redisClient;
    @Autowired
    JsonAuthenticationFailureHandler failureHandler;
    @Autowired
    SysProperties sysProperties;

    @SneakyThrows
    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        String headerToken = request.getHeader(HttpHeaders.AUTHORIZATION);
        if(StringUtils.isNoneBlank(headerToken)){
            String[] split = headerToken.split("@");
            if(split.length != 3){
                unsuccessfulAuthentication(request,response,new NonceExpiredException("token错误!"));
                return;
            }
            // 从缓存中获取token
            String key = RedisKeyUtils.getTokenKey(sysProperties.getRedisProject(), split[1],split[0]);
            // 判断key是否存在
            if(!redisClient.hasKey(key)){
                unsuccessfulAuthentication(request,response,new NonceExpiredException("token已过期!"));
                return;
            }
            //TODO 记录用户退出记录
            log.info(split[1]+":退出成功");
            // 清除缓存
            redisClient.del(key);
        }else{
            unsuccessfulAuthentication(request,response,new NonceExpiredException("token不存在!"));
            return;
        }
    }

    /**
     * 验证失败处理
     * @param request
     * @param response
     * @param failed
     * @throws IOException
     * @throws ServletException
     */
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        log.error(failed.getMessage());
        this.failureHandler.onAuthenticationFailure(request, response, failed);
    }

}

退出成功处理器

@Component
public class JsonLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        //响应状态
        response.setStatus(HttpServletResponse.SC_OK);
        //返回Json格式
        response.setHeader("Content-Type","application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.print(JsonUtil.objectToJson(JsonResult.result(false, ResultCode.SUCCESS_LOGOUT,"")));
        writer.flush();
        writer.close();
    }
}

同样的返回json。这样一个简单的前后端分离权限处理基本完成了。退出如何配置前面的配置类里已经提供。

到了这里基本上对于使用 Security 来进行权限控制基本没什么问题了。

当前的还是简单的前后端分离,当然除了单体架构的系统,还有后面的分布式的环境下,如何进行权限控制。Security 也提供了相应的解决方案。感兴趣的同学可以查看网上的其它文章。

最后我再介绍一个类,了解此类可以提高我们使用 Security 的上手度,那就是我们一直再使用的 HttpSecurity 对象。

HttpSecurity 对象简单介绍

除了前面的 Security 的认证和授权,我们还需要了解 HttpSecurity 这个类,它是我们配置 Security 非常重要的对象。

它用于构建基于 http 配置,用于处理 http 请求到我们 Web 系统的安全。也就是构建一个能够过滤请求的安全配置来保护我们的Web系统。默认它会过滤全部请求,当然我们可以自定义拦截的请求路径。

像我们之前授权配置的 anyRequest() 方法就是映射所有请求到授权管理器。

此类里面的方法全是用于配置我们的安全处理,包括各种处理器的设置,拦截的权限过滤器的管理等等。大家可以细看里面的方法。基本上熟悉的此类,就知道 Security 支持的大部分功能。

关于此类的具体方法描述大家可以查询网上其它文章,或者直接查看源码。这里就不再过多介绍。只是简单的让大家了解此类,对后面配置 Security 更加的上手。

其它

除了这两篇提到的功能,SpringSecurity 还有提供许多其它的功能,比如注解权限、分布式权限等等,这里就不演示了,大家有兴趣可以参考网上其它文章或者官方文档。