2023-06-12  阅读(1)
原文作者:发飙的蜗牛咻咻咻~ 原文地址:https://blog.csdn.net/qq_36221788/category_11009647.html

〇、写在前面

上两篇讲解了如何做登录授权和接口认证,本篇是实战篇终篇,主要讲讲如何做权限控制,前两篇博文地址:
《手把手教你如何使用Spring Security(上):登录授权》
《手把手教你如何使用Spring Security(中):接口认证》
同时附上项目地址:spring-security-demo


一、理论知识

官方介绍

  • Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

Spring Security是一个功能强大且高度可定制的 身份认证访问控制 框架,它是保护基于spring应用程序的事实标准。

通俗解释

  • 官方介绍中强调了身份认证和访问控制,前两篇实现了身份认证的功能,而本篇就来说说使用Spring Security进行请求的访问控制。
  • 简单来说,就是 给用户角色添加角色权限,使得不同的用户角色只能访问特定的接口资源,对于其他接口无法访问。

分类

根据业务的不同将权限的控制分为两类,一类是 To-C简单角色的权限控制 ,一类是 To-B基于RBAC数据模型的权限控制

  • To-C简单角色的权限控制

例如 买家和卖家,这两者都是单独的个体,一般来说都是只有一种独立的角色,比如卖家角色:ROLE_SELLER,买家角色:ROLE_BUYER。这类一般比较粗粒度的将角色划分,且角色比较固定,角色拥有的权限也是比较固定,在项目启动的时候就固定了。

  • To-B基于RBAC数据模型的权限控制

例如 PC后台的管理端,能登录的是企业的人员,企业人员可以有不同的角色,角色的权限也可以比较随意地去改变,比如总经理角色可以访问所有资源,店铺管理人员只能访问店铺和卖家相关信息,会员管理人员可以访问买家相关信息等等,这时候就可以使用基于RBAC数据模型结合Spring Security的访问控制来实现权限方案。这类一般角色划分较细,角色的权限也是上线后在PC端可任意配置


二、To-C:简单角色的权限控制

在配置角色前,先定义买家和卖家的一些接口。

  • 买家:
    @RestController
    @RequestMapping("/buyer")
    public class BuyerController {
    
        /**
         * 买家下订单
         *
         * @return
         */
        @GetMapping("/order:create")
        public String receiveOrder() {
            return "买家下单啦!";
        }
    
        /**
         * 买家订单支付
         *
         * @return
         */
        @GetMapping("/order:pay")
        public String deliverOrder() {
            return "买家付款了!";
        }
    }
  • 卖家:
    @RestController
    @RequestMapping("/seller")
    public class SellerController {
    
        /**
         * 卖家接单
         *
         * @return
         */
        @GetMapping("/order:receive")
    	@Secured("SELLER")
        public String receiveOrder() {
            return "卖家接单啦!";
        }
    
        /**
         * 卖家订单发货
         *
         * @return
         */
        @GetMapping("/order:deliver")
        @Secured("SELLER")
        public String deliverOrder() {
            return "卖家发货啦!";
        }
    }

我们要做到的是,买家角色只拥有买家接口权限,卖家角色只拥有卖家接口权限。而关于配置角色权限有两种实现方式,一种是在核心配置类中统一配置(买家角色演示),还有一种是在接口上以注解的方式配置(卖家角色演示)。

统一配置

  • 在核心配置类(WebSecurityConfig)中,统一配置买家角色权限,角色名称是 ROLE_BUYER,拥有访问 /buyer/** 接口的权限。以下代码第九行配置:
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http.authorizeRequests()
    				// 配置白名单(比如登录接口)
    				.antMatchers(securityConfig.getPermitUrls()).permitAll()
    				// 匿名访问的URL,即不用登录也可以访问(比如广告接口)
    				.antMatchers(securityConfig.getAnonymousUrls()).permitAll()
    				// 买家接口需要 “ROLE_BUYER” 角色权限才能访问
    				.antMatchers("/buyer/**").hasRole("BUYER")
    				// 其他任何请求满足 rbacService.hasPermission() 方法返回true时,能够访问
    				.anyRequest().access("@rbacService.hasPermission(request, authentication)")
    				// 其他URL一律拒绝访问
    //				.anyRequest().denyAll()
    				.and()
    				// 禁用跨站点伪造请求
    				.csrf().disable()
    				// 启用跨域资源共享
    				.cors()
    				.and()
    				// 添加请求头
    				.headers().addHeaderWriter(
    				new StaticHeadersWriter(Collections.singletonList(
    						new Header("Access-control-Allow-Origin", "*"))))
    				.and()
    				// 自定义的登录过滤器,不同的登录方式创建不同的登录过滤器,一样的配置方式
    				.apply(new UserLoginConfigurer<>(securityConfig))
    				.and()
    				// 自定义的JWT令牌认证过滤器
    				.apply(new JwtLoginConfigurer<>(securityConfig))
    				.and()
    				// 登出过滤器
    				.logout()
    				// 登出成功处理器
    				.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
    				.and()
    				// 禁用Session会话机制(我们这个demo用的是JWT令牌的方式)
    				.sessionManagement().disable()
    				// 禁用SecurityContext,这个配置器实际上认证信息会保存在Session中,但我们并不用Session机制,所以也禁用
    				.securityContext().disable();
    	}

注解方式

  • 可以使用注解的方式配置接口所能访问的角色,比如卖家端两个接口配置了 ROLE_SELLER 角色才能访问
    @RestController
    @RequestMapping("/seller")
    public class SellerController {
    
        /**
         * 卖家接单
         *
         * @return
         */
        @GetMapping("/order:receive")
    	@Secured("SELLER")
        public String receiveOrder() {
            return "卖家接单啦!";
        }
    
        /**
         * 卖家订单发货
         *
         * @return
         */
        @GetMapping("/order:deliver")
        @Secured("SELLER")
        public String deliverOrder() {
            return "卖家发货啦!";
        }
    }
  • @Secured@RolesAllowed@PreAuthorize 注解都可以达到这样的效果,所有注解能发挥有效的前提是需要在核心配置类加上注解 @EnableGlobalMethodSecurity,然后在此注解上启用对应的注解配置方式,注解才能生效,否则无法起作用,比如要使 @Secured 注解生效需要配置 @EnableGlobalMethodSecurity(securedEnabled = true)

    202306122223195881.png

  • 注解能否生效和启用注解的属性对应关系如下,简单解释就是要使接口上的注解生效,就需要在核心过滤器配置注解 @EnableGlobalMethodSecurity,然后启用注解对应的属性,就是将属性值设为true。具体请参考:@EnableGlobalMethodSecurity的三种开启注解方式

生效注解 启用注解的属性 核心配置器上注解配置
@Secured securedEnabled @EnableGlobalMethodSecurity(securedEnabled=true)
@RolesAllowed jsr250Enabled @EnableGlobalMethodSecurity(jsr250Enabled=true)
@PreAuthorize prePostEnabled @EnableGlobalMethodSecurity(prePostEnabled=true)

认证Token添加角色

上面的步骤只是配置了:

  • ROLE_BUYER角色拥有访问 /buyer/** 接口的权限
  • ROLE_SELLER角色拥有访问/order:receive/seller/order:deliver接口的权限
    而在接口认证的时候也需要将角色放到认证令牌中,来看看:
  • 在接口认证的 JwtAuthenticationProvider 中查询出用户角色,然后放进将要返回的 JwtAuthenticationToken 令牌中,其实是放到它父类的 authorities 属性中

    202306122223202002.png

  • 角色名称并非私密信息,可以直接 在登录授权的时候放进JWT中,为了提高性能这里就不需要每次都根据用户重新查询一遍角色。

  • JWT中新增 roleName 字段,注意生成JWT和校验JWT要补充该字段,在登录的 UserAuthenticationProvider 中将用户角色放进JWT中:

    202306122223208253.png

  • 接口认证时直接从JWT中获取角色,放进认证Token中,过滤器链的最后一个过滤器 FilterSecurityInterceptor 会进行权限判断,关于它是如何进行权限判断的,后面有源码篇讲解,文末放出了相关链接。

    202306122223214204.png

效果演示

现在登录用户是买家角色,拥有 ROLE_BUYER 角色权限,所以他只能访问买家的接口,无法访问卖家接口,注意:要重新登录,请求接口时将登录授权后返回的JWT令牌带上。

  • 访问买家接口,都能正常访问

    202306122223219865.png

  • 访问卖家接口,报拒绝访问

    202306122223223406.png


三、To-B:基于RBAC数据模型的权限控制

RBAC数据模型

  • 全称:Role-Based Access Control(基于角色的访问控制)

  • 一般会有五个表组成,三张主体表(用户、角色、权限),两张关联表(用户-角色、角色-权限)

    202306122223227317.png

实战

首先关于RBAC的数据模型大家应该都很熟悉,这里不再创建,即不会涉及到存储。其实这一类相对上面那类区别在于这类的权限不是固定的,需要实时的重新查询出来,再进行判断请求是否有权访问,所以判断是否有权访问的逻辑需要自己完善,写好之后再配置进框架中即可。

  • 先贴一下测试接口
    @RestController
    @RequestMapping("/business")
    public class BusinessController {
    
        /**
         * 店铺列表
         *
         * @return
         */
        @GetMapping("/stores")
        public String stores() {
            return "这是店铺列表!";
        }
    
        /**
         * 卖家列表
         *
         * @return
         */
        @GetMapping("/sellers")
        public String sellers() {
            return "这是卖家列表!";
        }
    
        /**
         * 买家列表
         *
         * @return
         */
        @GetMapping("/buyers")
        public String deliverOrder() {
            return "这是买家列表!";
        }
    }
  • 权限判断逻辑,主要实现一个 hasPermission 方法,这里配置了 ROLE_ADMIN 角色可以访问任意接口,其它角色需要重新查询出它所能访问的接口,这里假如查询出能访问的接口为 /business/stores/business/sellers
    public interface RbacService {
    
    	/**
    	 * 是否有权限访问
    	 *
    	 * @param request
    	 * @param authentication
    	 * @return
    	 */
    	boolean hasPermission(HttpServletRequest request, Authentication authentication);
    }
    @Component("rbacService")
    public class RbacServiceImpl implements RbacService {
    
    	private AntPathMatcher antPathMatcher = new AntPathMatcher();
    
    	@Override
    	public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
    		Object principal = authentication.getPrincipal();
    
    		boolean hasPermission = false;
    
    		if (principal instanceof JwtUserLoginDTO) {
    			// 如果角色是“ROLE_ADMIN”,就永远返回true
    			if (StringUtils.equals(((JwtUserLoginDTO) principal).getRoleName(), "ROLE_ADMIN")) {
    				hasPermission = true;
    			} else {
    				// 查询用户角色所拥有权限的所有URL,这里假设是从数据库或缓存(或者登录的时候可以直接将该角色拥有的权限保存到JWT)中查的
    				List<String> urls = Arrays.asList("/business/stores", "/business/sellers");
    				for (String url : urls) {
    					if (antPathMatcher.match(url, request.getRequestURI())) {
    						hasPermission = true;
    						break;
    					}
    				}
    			}
    		}
    
    		return hasPermission;
    	}
    }
  • 将权限判断逻辑配置进框架,只要在核心配置器中添加一行,如下11行
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http.authorizeRequests()
    				// 配置白名单(比如登录接口)
    				.antMatchers(securityConfig.getPermitUrls()).permitAll()
    				// 匿名访问的URL,即不用登录也可以访问(比如广告接口)
    				.antMatchers(securityConfig.getAnonymousUrls()).permitAll()
    				// 买家接口需要 “ROLE_BUYER” 角色权限才能访问
    				.antMatchers("/buyer/**").hasRole("BUYER")
    				// 其他任何请求满足 rbacService.hasPermission() 方法返回true时,能够访问
    				.anyRequest().access("@rbacService.hasPermission(request, authentication)")
    				// 其他URL一律拒绝访问
    //				.anyRequest().denyAll()
    				.and()
    				// 禁用跨站点伪造请求
    				.csrf().disable()
    				// 启用跨域资源共享
    				.cors()
    				.and()
    				// 添加请求头
    				.headers().addHeaderWriter(
    				new StaticHeadersWriter(Collections.singletonList(
    						new Header("Access-control-Allow-Origin", "*"))))
    				.and()
    				// 自定义的登录过滤器,不同的登录方式创建不同的登录过滤器,一样的配置方式
    				.apply(new UserLoginConfigurer<>(securityConfig))
    				.and()
    				// 自定义的JWT令牌认证过滤器
    				.apply(new JwtLoginConfigurer<>(securityConfig))
    				.and()
    				// 登出过滤器
    				.logout()
    				// 登出成功处理器
    				.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
    				.and()
    				// 禁用Session会话机制(我们这个demo用的是JWT令牌的方式)
    				.sessionManagement().disable()
    				// 禁用SecurityContext,这个配置器实际上认证信息会保存在Session中,但我们并不用Session机制,所以也禁用
    				.securityContext().disable();
    	}

效果演示

  • 现在登录的是其他角色(非ROLE_ADMIN)的用户,即能访问店铺列表和卖家列表,不能访问买家列表,访问买家店铺列表:

    202306122223233558.png

  • 访问卖家列表:

    202306122223238099.png


四、权限表达式

  • 上面.permitAll().hasRole().access()表示权限表达式,而权限表达式实际上都是 Spring中强大的Spel表达式,如下还有很多可以使用的权限表达式以及和Spel表达式的转换关系:
权限表达式(ExpressionUrlAuthorizationConfigurer) 说明 Spel表达式 Spel表达式实际执行方法(SecurityExpressionOperations)
permitAll() 表示允许所有,永远返回true permitAll permitAll()
denyAll() 表示拒绝所有,永远返回false denyAll denyAll()
anonymous() 当前用户是anonymous时返回true anonymous isAnonymous()
rememberMe() 当前用户是rememberMe用户时返回true rememberMe isRememberMe()
authenticated() 当前用户不是anonymous时返回true authenticated isAuthenticated()
fullyAuthenticated() 当前用户既不是anonymous也不是rememberMe用户时返回true fullyAuthenticated isFullyAuthenticated()
hasRole(“BUYER”) 用户拥有指定权限时返回true hasRole(‘ROLE_BUYER’) hasRole(Stringrole)
hasAnyRole(“BUYER”,“SELLER”) 用于拥有任意一个角色权限时返回true hasAnyRole(‘ROLE_BUYER’,‘ROLE_BUYER’) hasAnyRole(String…roles)
hasAuthority(“BUYER”) 同hasRole hasAuthority(‘ROLE_BUYER’) hasAuthority(Stringrole)
hasAnyAuthority(“BUYER”,“SELLER”) 同hasAnyRole hasAnyAuthority(‘ROLE_BUYER’,‘ROLE_BUYER’) hasAnyAuthority(String…authorities)
hasIpAddress(‘192.168.1.0/24’) 请求发送的Ip匹配时返回true hasIpAddress(‘192.168.1.0/24’) hasIpAddress(StringipAddress),该方法在WebSecurityExpressionRoot类中
access("@rbacService.hasPermission(request,authentication)") 可以自定义Spel表达式 @rbacService.hasPermission(request,authentication) hasPermission(request,authentication),该方法在自定义的RbacServiceImpl类中

五、总结

  • Spring Security框架不仅提供了身份认证的功能,也提供了强大的访问控制支持。
  • 访问控制判断权限利用了Spring中强大的Spel表达式,具体请自行搜索相关文章。

六、系列文章

Spring Security 系列

Spring Security OAuth 系列


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文