<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
最近在做 TienChin 專案,用的是 RuoYi-Vue 腳手架,在這個腳手架中,存取某個介面需要什麼許可權,這個是在程式碼中寫死的,具體怎麼實現的,鬆哥下篇文章來和大家分析,有的小夥伴可能希望能讓這個東西像 vhr 一樣,可以在資料庫中動態設定,因此這篇文章和小夥伴們簡單介紹下 Spring Security 中的動態許可權方案,以便於小夥伴們更好的理解 TienChin 專案中的許可權方案。
通過程式碼來設定 URL 攔截規則和請求 URL 所需要的許可權,這樣就比較死板,如果想要調整存取某一個 URL 所需要的許可權,就需要修改程式碼。
動態管理許可權規則就是我們將 URL 攔截規則和存取 URL 所需要的許可權都儲存在資料庫中,這樣,在不改變原始碼的情況下,只需要修改資料庫中的資料,就可以對許可權進行調整。
簡單起見,我們這裡就不引入許可權表了,直接使用角色表,使用者和角色關聯,角色和資源關聯,設計出來的表結構如圖 13-9 所示。
圖13-9 一個簡單的許可權資料庫結構
menu 表是相當於我們的資源表,它裡邊儲存了存取規則,如圖 13-10 所示。
圖13-10 存取規則
role 是角色表,裡邊定義了系統中的角色,如圖 13-11 所示。
圖13-11 使用者角色表
user 是使用者表,如圖 13-12 所示。
圖13-12 使用者表
user_role 是使用者角色關聯表,使用者具有哪些角色,可以通過該表體現出來,如圖 13-13 所示。
圖13-13 使用者角色關聯表
menu_role 是資源角色關聯表,存取某一個資源,需要哪些角色,可以通過該表體現出來,如圖 13-14 所示。
圖13-14 資源角色關聯表
至此,一個簡易的許可權資料庫就設計好了(在本書提供的案例中,有SQL指令碼)。
專案建立
建立 Spring Boot 專案,由於涉及資料庫操作,這裡選用目前大家使用較多的 MyBatis 框架,所以除了引入 Web、Spring Security 依賴之外,還需要引入 MyBatis 以及 MySQL 依賴。
最終的 pom.xml 檔案內容如下:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies>
專案建立完成後,接下來在 application.properties 中設定資料庫連線資訊:
spring.datasource.username=root spring.datasource.password=123 spring.datasource.url=jdbc:mysql:///security13?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
設定完成後,我們的準備工作就算完成了。
建立實體類
根據前面設計的資料庫,我們需要建立三個實體類。
首先來建立角色類 Role:
public class Role { private Integer id; private String name; private String nameZh; //省略getter/setter }
然後建立選單類 Menu:
public class Menu { private Integer id; private String pattern; private List<Role> roles; //省略getter/setter }
選單類中包含一個 roles 屬性,表示存取該項資源所需要的角色。
最後我們建立 User 類:
public class User implements UserDetails { private Integer id; private String password; private String username; private boolean enabled; private boolean locked; private List<Role> roles; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles.stream() .map(r -> new SimpleGrantedAuthority(r.getName())) .collect(Collectors.toList()); } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return !locked; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return enabled; } //省略其他getter/setter }
由於資料庫中有 enabled 和 locked 欄位,所以 isEnabled() 和 isAccountNonLocked() 兩個方法如實返回,其他幾個賬戶狀態方法預設返回 true 即可。在 getAuthorities() 方法中,我們對 roles 屬性進行遍歷,組裝出新的集合物件返回即可。
建立Service
接下來我們建立 UserService 和 MenuService,並提供相應的查詢方法。
先來看 UserService:
@Service public class UserService implements UserDetailsService { @Autowired UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.loadUserByUsername(username); if (user == null) { throw new UsernameNotFoundException("使用者不存在"); } user.setRoles(userMapper.getUserRoleByUid(user.getId())); return user; } }
這段程式碼應該不用多說了,不熟悉的讀者可以參考本書 2.4 節。
對應的 UserMapper 如下:
@Mapper public interface UserMapper { List<Role> getUserRoleByUid(Integer uid); User loadUserByUsername(String username); }
UserMapper.xml:
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.javaboy.base_on_url_dy.mapper.UserMapper"> <select id="loadUserByUsername" resultType="org.javaboy.base_on_url_dy.model.User"> select * from user where username=#{username}; </select> <select id="getUserRoleByUid" resultType="org.javaboy.base_on_url_dy.model.Role"> select r.* from role r,user_role ur where ur.uid=#{uid} and ur.rid=r.id </select> </mapper>
再來看 MenuService,該類只需要提供一個方法,就是查詢出所有的 Menu 資料,程式碼如下:
@Service public class MenuService { @Autowired MenuMapper menuMapper; public List<Menu> getAllMenu() { return menuMapper.getAllMenu(); } }
MenuMapper:
@Mapper public interface MenuMapper { List<Menu> getAllMenu(); }
MenuMapper.xml:
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.javaboy.base_on_url_dy.mapper.MenuMapper"> <resultMap id="MenuResultMap" type="org.javaboy.base_on_url_dy.model.Menu"> <id property="id" column="id"/> <result property="pattern" column="pattern"></result> <collection property="roles" ofType="org.javaboy.base_on_url_dy.model.Role"> <id column="rid" property="id"/> <result column="rname" property="name"/> <result column="rnameZh" property="nameZh"/> </collection> </resultMap> <select id="getAllMenu" resultMap="MenuResultMap"> select m.*,r.id as rid,r.name as rname,r.nameZh as rnameZh from menu m left join menu_role mr on m.`id`=mr.`mid` left join role r on r.`id`=mr.`rid` </select> </mapper>
需要注意,由於每一個 Menu 物件都包含了一個 Role 集合,所以這個查詢是一對多,這裡通過 resultMap 來進行查詢結果對映。
至此,所有基礎工作都完成了,接下來設定 Spring Security。
設定Spring Security
回顧 13.3.6 小節的內容,SecurityMetadataSource 介面負責提供受保護物件所需要的許可權。在本案例中,受保護物件所需要的許可權儲存在資料庫中,所以我們可以通過自定義類繼承自 FilterInvocationSecurityMetadataSource,並重寫 getAttributes 方法來提供受保護物件所需要的許可權,程式碼如下:
@Component public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { @Autowired MenuService menuService; AntPathMatcher antPathMatcher = new AntPathMatcher(); @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { String requestURI = ((FilterInvocation) object).getRequest().getRequestURI(); List<Menu> allMenu = menuService.getAllMenu(); for (Menu menu : allMenu) { if (antPathMatcher.match(menu.getPattern(), requestURI)) { String[] roles = menu.getRoles().stream() .map(r -> r.getName()).toArray(String[]::new); return SecurityConfig.createList(roles); } } return null; } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } }
自定義 CustomSecurityMetadataSource 類並實現 FilterInvocationSecurityMetadataSource 介面,然後重寫它裡邊的三個方法:
/admin/hello
,然後通過 menuService 物件查詢出所有的選單資料(每條資料中都包含存取該條記錄所需要的許可權),遍歷查詢出來的選單資料,如果當前請求的 URL 地址和選單中某一條記錄的 pattern 屬性匹配上了(例如 /admin/hello
匹配上 /admin/**
),那麼我們就可以獲取當前請求所需要的許可權。從 menu 物件中獲取 roles 屬性,並將其轉為一個陣列,然後通過 SecurityConfig.createList
方法建立一個 Collection<ConfigAttribute>
物件並返回。如果當前請求的 URL 地址和資料庫中 menu 表的所有項都匹配不上,那麼最終返回 null。如果返回 null,那麼受保護物件到底能不能存取呢?這就要看 AbstractSecurityInterceptor 物件中的 rejectPublicInvocations 屬性了,該屬性預設為 false,表示當 getAttributes 方法返回 null 時,允許存取受保護物件(回顧 13.4.4 小節中關於 AbstractSecurityInterceptor#beforeInvocation
的講解)。CustomSecurityMetadataSource
類設定完成後,接下來我們要用它來代替預設的 SecurityMetadataSource
物件,具體設定如下:
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired CustomSecurityMetadataSource customSecurityMetadataSource; @Autowired UserService userService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService); } @Override protected void configure(HttpSecurity http) throws Exception { ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class); http.apply(new UrlAuthorizationConfigurer<>(applicationContext)) .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setSecurityMetadataSource(customSecurityMetadataSource); return object; } }); http.formLogin() .and() .csrf().disable(); } }
關於使用者的設定無需多說,我們重點來看 configure(HttpSecurity) 方法。
由於存取路徑規則和所需要的許可權之間的對映關係已經儲存在資料庫中,所以我們就沒有必要在 Java 程式碼中設定對映關係了,同時這裡的許可權對比也不會用到許可權表示式,所以我們通過 UrlAuthorizationConfigurer 來進行設定。
在設定的過程中,通過 withObjectPostProcessor 方法呼叫 ObjectPostProcessor 物件後置處理器,在物件後置處理器中,將 FilterSecurityInterceptor 中的 SecurityMetadataSource 物件替換為我們自定義的 customSecurityMetadataSource 物件即可。
接下來建立 HelloController,程式碼如下:
@RestController public class HelloController { @GetMapping("/admin/hello") public String admin() { return "hello admin"; } @GetMapping("/user/hello") public String user() { return "hello user"; } @GetMapping("/guest/hello") public String guest() { return "hello guest"; } @GetMapping("/hello") public String hello() { return "hello"; } }
最後啟動專案進行測試。
首先使用 admin/123
進行登入,該使用者具備 ROLE_ADMIN
角色,ROLE_ADMIN
可以存取 /admin/hello
、/user/hello
以及 /guest/hello
三個介面。
接下來使用 user/123
進行登入,該使用者具備 ROLE_USER
角色,ROLE_USER
可以存取 /user/hello
以及 /guest/hello
兩個介面。
最後使用 javaboy/123
進行登入,該使用者具備 ROLE_GUEST
角色,ROLE_GUEST
可以存取 /guest/hello
介面。
由於 /hello
介面不包含在 URL-許可權
對映關係中,所以任何使用者都可以存取 /hello
介面,包括匿名使用者。如果希望所有的 URL
地址都必須在資料庫中設定 URL-許可權
對映關係後才能存取,那麼可以通過如下設定實現:
http.apply(new UrlAuthorizationConfigurer<>(applicationContext)) .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setSecurityMetadataSource(customSecurityMetadataSource); object.setRejectPublicInvocations(true); return object; } });
通過設定 FilterSecurityInterceptor 中的 rejectPublicInvocations 屬性為 true,就可以關閉URL的公開存取,所有 URL 必須具備對應的許可權才能存取。
以上就是Spring Security動態許可權的實現方法詳解的詳細內容,更多關於Spring Security動態許可權的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45