javascript
SpringSecurity - 用户动态授权 及 动态角色权限
一、SpringSecurity 動態授權
上篇文章我們介紹了SpringSecurity的動態認證,上篇文章就說了SpringSecurity 的兩大主要功能就是認證和授權,既然認證以及學習了,那本篇文章一起學習了SpringSecurity 的動態授權。
上篇文章地址:https://blog.csdn.net/qq_43692950/article/details/122393435
二、SpringSecurity 授權
我們接著上篇文章的項目繼續修改,上篇文章中有說到我們WebSecurityConfig配制類中的configure(HttpSecurity http)這個方法就是用來做授權的,現在就可以來體驗一下了,比如我們修改以admin為開頭的接口,權限或角色中需要有admin:
@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/admin/**").hasAuthority("admin").antMatchers("/**").fullyAuthenticated().and().formLogin().permitAll().and().csrf().disable();}下面使用admin用戶訪問admin/test接口:
報了403無權限的錯誤,因為我們設置了admin/**接口必須要有admin這個權限,可以看下上篇文章中寫的UserService類:
這邊直接給用戶設定死了一個admin角色,這里就有個問題了權限和角色有什么區別,其實在SpringSecurity 中權限和角色都放在了一起,可以說概念上是一樣的,但角色是以ROLE_開頭的。
其中還需注意的是如果授權角色可以使用hasRole()和hasAnyRole(),如果是授權權限則使用hasAuthority() 和 hasAnyAuthority()
角色授權:授權代碼需要加ROLE_前綴,controller上使用時不要加前綴。
權限授權:設置和使用時,名稱保持一至即可。
所以可以修改UserService類:
在此請求接口:
現在就有權限訪問了,但是寫死肯定不是我們要的效果,所以此時可以將角色放在數據庫中,通過查詢數據庫動態獲取用戶的角色。
下面就需要在數據庫中創建role角色表:
CREATE TABLE `role` (`id` int(11) NOT NULL AUTO_INCREMENT,`role` varchar(255) NOT NULL,`role_describe` varchar(255) NOT NULL,PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;角色肯定是和人有關系的,而且有時多對多的關系,所以根據關系模型我們要抽取出一個角色用戶關系表:
CREATE TABLE `user_role` (`id` int(11) NOT NULL AUTO_INCREMENT,`userid` int(11) NOT NULL,`roleid` int(11) NOT NULL,PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
對于角色的新增和關聯用戶,無非就是數據庫的增刪改,這里不做演示了,直接在創建好表可以在表中添加幾條角色,并關聯用戶:
添加RoleEntity實體
RoleMapper類,并寫根據用戶id查詢全部角色的接口:
@Mapper @Repository public interface RoleMapper extends BaseMapper<RoleEntity> {@Select("SELECT r.id,r.role,r.role_describe FROM user_role u,role r where u.roleid = r.id AND u.userid = #{userId}")List<RoleEntity> getAllRoleByUserId(@Param("userId") Integer userId); }修改UserService類:
@Service public class UserService implements UserDetailsService {@AutowiredUserMapper userMapper;@AutowiredRoleMapper roleMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {LambdaQueryWrapper<UserEntity> wrapper = new LambdaQueryWrapper<UserEntity>().eq(UserEntity::getUsername, username);UserEntity userEntity = userMapper.selectOne(wrapper);if (userEntity == null) {throw new UsernameNotFoundException("用戶不存在!");}List<GrantedAuthority> auths = roleMapper.getAllRoleByUserId(userEntity.getId()).stream().map(r -> new SimpleGrantedAuthority(r.getRole())).collect(Collectors.toList());userEntity.setRoles(auths);return userEntity;}public boolean register(String userName, String password) {UserEntity entity = new UserEntity();entity.setUsername(userName);entity.setPassword(new BCryptPasswordEncoder().encode(password));entity.setEnabled(true);entity.setLocked(false);return userMapper.insert(entity) > 0;} }下面就可以測試了,在瀏覽器再次訪問上面的接口:
但是發現是403,原因是我們給admin設置的是權限admin,不是角色,數據庫中存的是ROLE_admin,這里是想讓大家對兩者的區別更加深刻下,修改數據庫為admin
重新啟動再次訪問:
已經可以訪問了。上面大家應該對權限和角色有了一定的了解,下面對授權和授予角色的方法做下說明:
-
hasRole :
如果用戶具備給定角色就允許訪問,否則出現 403。給接口授權時無需寫ROLE_開頭,因為底層代碼會自動添加與之進行匹配,用戶添加角色時必須寫ROLE_。 -
hasAnyRole
表示用戶具備任何一個條件都可以訪問。 -
hasAuthority :
如果當前的主體具有指定的權限,則返回 true,否則返回 false -
hasAnyAuthority
如果當前的主體有任何提供的角色(給定的作為一個逗號分隔的字符串列表)的話,返true
現在我們已經了解怎么樣給用戶授權了,也知道怎么給接口賦予權限了,但是還是有個問題:
這個都在代碼里面寫死也不合適呀,其實這里有兩種方案,一種是地址和角色的固定變化不大的場景下,可以在這里從數據庫中讀取出來通過HttpSecurity對象映射角色,但這種方案不太好在項目運行期間動態添加角色。還有一種方案就是實現FilterInvocationSecurityMetadataSource接口,在這里面根據當前訪問的url返回該url所具有的全部角色。顯然后者更為靈活,但每次訪問一次接口都取獲取全部的角色肯定性能有所損失。
下面分別實現下這兩種情況:
三、數據庫讀取通過HttpSecurity授權
上面已經創建了role角色表,現在要做url和role的關聯,所以添加一個menu表用來存放url:
CREATE TABLE `menu` (`id` int(11) NOT NULL AUTO_INCREMENT,`pattern` varchar(255) NOT NULL,PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;menu和role也都是多對多的關系,所以也需要建一個menu_role關系表:
CREATE TABLE `menu_role` (`id` int(11) NOT NULL AUTO_INCREMENT,`menu_id` int(11) NOT NULL,`role_id` int(11) NOT NULL,PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;還是在表中添加一些數據:
創建MeunEntity實體類:
MeunMapper 繼承BaseMapper
@Mapper @Repository public interface MeunMapper extends BaseMapper<MeunEntity> { }修改RoleMapper:
@Mapper @Repository public interface RoleMapper extends BaseMapper<RoleEntity> {@Select("SELECT r.id,r.role,r.role_describe FROM user_role u,role r where u.roleid = r.id AND u.userid = #{userId}")List<RoleEntity> getAllRoleByUserId(@Param("userId") Integer userId);@Select("SELECT r.id,r.role,r.role_describe FROM menu_role m,role r where m.role_id = r.id AND m.menu_id = #{menuId}")List<RoleEntity> getAllRoleByMenuId(@Param("menuId") Integer menuId); }修改WebSecurityConfig:
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserService userService;@AutowiredMeunMapper meunMapper;@AutowiredRoleMapper roleMapper;@Overridepublic void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userService).passwordEncoder(password());}@BeanPasswordEncoder password() {return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests = http.authorizeRequests();List<MeunEntity> meunEntities = meunMapper.selectList(null);meunEntities.forEach(m -> {authorizeRequests.antMatchers(m.getPattern()).hasAnyAuthority(roleMapper.getAllRoleByMenuId(m.getId()).stream().map(RoleEntity::getRole).toArray(String[]::new));});authorizeRequests.antMatchers("/**").fullyAuthenticated().and().formLogin().permitAll().and().csrf().disable();}@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/register/**");} }重啟項目,然后再次訪問測試接口,已經實現和上面相同的效果:
四、通過FilterInvocationSecurityMetadataSource 動態角色
上面已經實現了第一種方案,下面繼續實現第二中方案,下面創建一個類實現FilterInvocationSecurityMetadataSource 接口:
@Component public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {@AutowiredMeunMapper meunMapper;@AutowiredRoleMapper roleMapper;//用來實現ant風格的Url匹配AntPathMatcher antPathMatcher = new AntPathMatcher();@Overridepublic Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {//獲取當前請求的UrlString requestUrl = ((FilterInvocation) object).getRequestUrl();List<MeunEntity> list = meunMapper.selectList(null);List<ConfigAttribute> roles = new ArrayList<>();list.forEach(m -> {if (antPathMatcher.match(m.getPattern(), requestUrl)) {List<ConfigAttribute> allRoleByMenuId = roleMapper.getAllRoleByMenuId(m.getId()).stream().map(r -> new SecurityConfig(r.getRole())).collect(Collectors.toList());roles.addAll(allRoleByMenuId);}});if (!roles.isEmpty()) {return roles;}return SecurityConfig.createList("ROLE_LOGIN");}@Overridepublic Collection<ConfigAttribute> getAllConfigAttributes() {return null;}@Overridepublic boolean supports(Class<?> clazz) {return true;} }還需創建一個CustomAccessDecisionManager用來實現AccessDecisionManager:
@Component public class CustomAccessDecisionManager implements AccessDecisionManager {@Overridepublic void decide(Authentication auth, Object object, Collection<ConfigAttribute> ca) throws AccessDeniedException, InsufficientAuthenticationException {for (ConfigAttribute configAttribute : ca) {//如果請求Url需要的角色是ROLE_LOGIN,說明當前的Url用戶登錄后即可訪問if ("ROLE_LOGIN".equals(configAttribute.getAttribute()) && auth instanceof UsernamePasswordAuthenticationToken){ return;}Collection<? extends GrantedAuthority> auths = auth.getAuthorities(); //獲取登錄用戶具有的角色for (GrantedAuthority grantedAuthority : auths) {if (configAttribute.getAttribute().equals(grantedAuthority.getAuthority())){return;}}}throw new AccessDeniedException("權限不足");}@Overridepublic boolean supports(ConfigAttribute configAttribute) {return true;}@Overridepublic boolean supports(Class<?> aClass) {return true;} }修改WebSecurityConfig
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserService userService;@AutowiredCustomAccessDecisionManager customAccessDecisionManager;@AutowiredCustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;@Overridepublic void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userService).passwordEncoder(password());}@BeanPasswordEncoder password() {return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {@Overridepublic <O extends FilterSecurityInterceptor> O postProcess(O o) {o.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);o.setAccessDecisionManager(customAccessDecisionManager);return o;}}).antMatchers("/**").fullyAuthenticated().and().formLogin().permitAll().and().csrf().disable();}@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/register/**");}@BeanRoleHierarchy roleHierarchy() {RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();String hierarchy = "ROLE_admin > ROLE_user > ROLE_common";roleHierarchy.setHierarchy(hierarchy);return roleHierarchy;} }再次測試上面的測試接口,可以發現也達到了相同的效果:
但是此時是動態角色的,我們可以創建一個新用戶,給新用戶一個新的角色,再給該角色賦予admin/**的權限。
創建用戶adc
添加角色:
角色綁定用戶:
角色綁定menu:
下面清楚瀏覽器的緩存,使用abc用戶登錄:
成功訪問接口,說明動態角色權限已經生效了。
喜歡的小伙伴可以關注我的個人微信公眾號,獲取更多學習資料!
總結
以上是生活随笔為你收集整理的SpringSecurity - 用户动态授权 及 动态角色权限的全部內容,希望文章能夠幫你解決所遇到的問題。