vue3整合SpringSecurity加JWT實(shí)現(xiàn)權(quán)限校驗(yàn)
我本來(lái)是想著登錄認(rèn)證和權(quán)限校驗(yàn)放在一篇文章里的,但是上次寫(xiě)登錄認(rèn)證就寫(xiě)了非常多了,實(shí)在是有些寫(xiě)不動(dòng)了,所以才分為了兩篇文章。
本文適合有一定基礎(chǔ)的人來(lái)看,如果你對(duì)springsecurity安全框架還不是很了解,建議你先去看一下我之前寫(xiě)過(guò)的spring security框架的快速入門(mén):
springboot3整合SpringSecurity實(shí)現(xiàn)登錄校驗(yàn)與權(quán)限認(rèn)證(萬(wàn)字超詳細(xì)講解)
技術(shù)棧版本:vue3.3.11、springboot3.1.5、spring security6.x
之前的登錄認(rèn)證文章:
前后端分離,使用vue3整合SpringSecurity加JWT實(shí)現(xiàn)登錄認(rèn)證
在上次的文章中,只寫(xiě)到登錄成功和退出之后就不寫(xiě)了,這次會(huì)加上權(quán)限校驗(yàn)。
首先,在原來(lái)數(shù)據(jù)庫(kù)的基礎(chǔ)上再新建:角色表、權(quán)限表、用戶(hù)角色表、角色權(quán)限表四張表:
2、角色表
CREATE TABLE roles ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL UNIQUE, description VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
3、權(quán)限表
CREATE TABLE permissions ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL UNIQUE, description VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
4、用戶(hù)角色表
CREATE TABLE user_roles ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, role_id INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (role_id) REFERENCES roles(id) );
5、角色權(quán)限表
CREATE TABLE role_permissions ( id INT AUTO_INCREMENT PRIMARY KEY, role_id INT NOT NULL, permission_id INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (role_id) REFERENCES roles(id), FOREIGN KEY (permission_id) REFERENCES permissions(id) );
現(xiàn)在,我們的數(shù)據(jù)庫(kù)中共有5張表,分別創(chuàng)建相應(yīng)的server、mapper和controller層。
接下來(lái),再原來(lái)的登錄認(rèn)證的代碼的基礎(chǔ)上就可以來(lái)實(shí)現(xiàn)我們的權(quán)限校驗(yàn)了;
權(quán)限校驗(yàn)這方面主要體現(xiàn)在后端代碼上,所以前端我只是進(jìn)行一些簡(jiǎn)單的演示即可;
1、在我們的MyTUserDetail類(lèi)中定義角色和權(quán)限的屬性集合,并添加到UserDetails類(lèi)的getAuthorities方法中(角色和權(quán)限我都使用Set定義,這樣能夠去重)
代碼如下:
@Data public class MyUserDetail implements Serializable, UserDetails { private static final long serialVersionUID = 1L; private Users Users; // 角色 private Set<String> roles; // 權(quán)限 private Set<String> permissions; @JsonIgnore //json忽略 @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<GrantedAuthority> list = new ArrayList<>(); // 如果角色不用空,則將角色添加到list中 if (!ObjectUtils.isEmpty(roles)){ roles.forEach(role->list.add(new SimpleGrantedAuthority("ZML_"+role))); } // 如果權(quán)限不用空,則將權(quán)限添加到list中 if (!ObjectUtils.isEmpty(permissions)){ permissions.forEach(permission->list.add(new SimpleGrantedAuthority(permission))); } return list; } @JsonIgnore @Override public String getPassword() { return this.getUsers().getPassword(); } @JsonIgnore @Override public String getUsername() { return this.getUsers().getUsername(); } @JsonIgnore @Override public boolean isAccountNonExpired() { return this.getUsers().getStatus()==0; } @JsonIgnore @Override public boolean isAccountNonLocked() { return this.getUsers().getStatus()==0; } @JsonIgnore @Override public boolean isCredentialsNonExpired() { return this.getUsers().getStatus()==0; } @JsonIgnore @Override public boolean isEnabled() { return this.getUsers().getStatus()==0; } }
Authentication 討論了所有 Authentication 實(shí)現(xiàn)如何存儲(chǔ) GrantedAuthority 對(duì)象的列表。這些對(duì)象代表已經(jīng)授予委托人(principal)的權(quán)限。GrantedAuthority 對(duì)象由 AuthenticationManager 插入到 Authentication 對(duì)象中,隨后由 AccessDecisionManager 實(shí)例在做出授權(quán)決定時(shí)讀取。
GrantedAuthority 接口只有一個(gè)方法。
String getAuthority();
這個(gè)方法被 AuthorizationManager 實(shí)例用來(lái)獲取 GrantedAuthority 的一個(gè)精確的 String 表示。通過(guò)返回一個(gè) String 表示,一個(gè) GrantedAuthority 可以被大多數(shù) AuthorizationManager 實(shí)現(xiàn)輕松 "讀取"。如果 GrantedAuthority 不能被精確地表示為一個(gè) String,那么該 GrantedAuthority 被認(rèn)為是 "復(fù)雜的",getAuthority() 必須返回 null。
一個(gè)復(fù)雜的 GrantedAuthority 的例子是一個(gè)實(shí)現(xiàn),它存儲(chǔ)了一個(gè)適用于不同客戶(hù)賬號(hào)的操作和權(quán)限閾值的列表。將這種復(fù)雜的 GrantedAuthority 表示為一個(gè) String 將是相當(dāng)困難的。因此,getAuthority() 方法應(yīng)該返回 null。這向任何 AuthorizationManager 表明,它需要支持特定的 GrantedAuthority 實(shí)現(xiàn)來(lái)理解其內(nèi)容。
Spring Security 包括一個(gè)具體的 GrantedAuthority 實(shí)現(xiàn)。SimpleGrantedAuthority。這個(gè)實(shí)現(xiàn)允許任何用戶(hù)指定的字符串被轉(zhuǎn)換為 GrantedAuthority。安全架構(gòu)中包含的所有 AuthenticationProvider 實(shí)例都使用 SimpleGrantedAuthority 來(lái)填充 Authentication 對(duì)象。
默認(rèn)情況下,基于角色的授權(quán)規(guī)則包括 ROLE_ 作為前綴。這意味著,如果有一個(gè)授權(quán)規(guī)則要求 security context 的角色是 "USER",Spring Security 將默認(rèn)尋找返回 "ROLE_USER" 的 GrantedAuthority#getAuthority。
你可以用 GrantedAuthorityDefaults 來(lái)定制這個(gè)。GrantedAuthorityDefaults 的存在是為了允許自定義基于角色的授權(quán)規(guī)則所使用的前綴。
你可以通過(guò)暴露一個(gè) GrantedAuthorityDefaults Bean 來(lái)配置授權(quán)規(guī)則以使用不同的前綴,像這樣:
@Bean static GrantedAuthorityDefaults grantedAuthorityDefaults() { return new GrantedAuthorityDefaults("ZML_"); }
我們需要特別注意的一點(diǎn)是,在spring security中。我們的角色和權(quán)限是存儲(chǔ)在一起的,沒(méi)有分開(kāi)存儲(chǔ) 如:
參考來(lái)源:授權(quán)架構(gòu) :: Spring Security Reference
2、在MyUserDetailServerImpl類(lèi)的loadUserByUsername方法中查出登錄用戶(hù)的權(quán)限集合:
代碼如下:
@Service @Slf4j public class MyUserDetailServerImpl implements MyUserDetailServer { @Autowired UsersMapper userService; /** * 返回一個(gè)賬號(hào)所擁有的權(quán)限碼集合 */ // 角色權(quán)限表 @Autowired IRolePermissionsService rolePermissionsService; // 用戶(hù)角色表 @Autowired IUserRolesService userRolesService; //權(quán)限表 @Autowired IPermissionsService permissionsService; // 角色表 @Autowired IRolesService rolesService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Users users = userService.selectOne(new LambdaQueryWrapper<Users>(). eq(username != null, Users::getUsername, username)); if (users == null) { throw new UsernameNotFoundException("用戶(hù)名不存在"); } log.info("UserDetailServer中的user:=========>"+users); MyUserDetail myTUserDetail=new MyUserDetail(); myTUserDetail.setUsers(users); // 查詢(xún)用戶(hù)權(quán)限 // 根據(jù)用戶(hù)id從用戶(hù)角色表中獲取角色id List<UserRoles> roleIds = userRolesService.list(new LambdaQueryWrapper<UserRoles>() .eq(UserRoles::getUserId,users.getId())); List<Integer> rolesList = roleIds.stream().map(UserRoles::getRoleId).toList(); if (!(roleIds.size() >0)){ // 用戶(hù)沒(méi)有分配角色 return myTUserDetail; } Set<String> listPermission = new HashSet<>(); rolesList.forEach(roleId ->{ // 根據(jù)角色id從角色權(quán)限表中獲取權(quán)限id List<RolePermissions> rolePermissions = rolePermissionsService.list(new LambdaQueryWrapper<RolePermissions>(). eq(RolePermissions::getRoleId, roleId)); // 根據(jù)權(quán)限id從權(quán)限表中獲取權(quán)限名稱(chēng) rolePermissions.forEach(permissionsId->{ Permissions permissions = permissionsService.getById(permissionsId.getPermissionId()); listPermission.add(permissions.getName()); }); }); myTUserDetail.setPermissions( listPermission); // 查詢(xún)角色角色 Set<String> listRole = new HashSet<>(); roleIds.forEach(roleId ->{ Roles byId = rolesService.getById(roleId.getRoleId()); listRole.add(byId.getName()); }); myTUserDetail.setRoles(listRole); log.info("UserDetailServer中的查完權(quán)限的myTUserDetail:=========>"+myTUserDetail); return myTUserDetail; } }
我所實(shí)現(xiàn)的是標(biāo)準(zhǔn)的RBAC(基于用戶(hù)、角色、權(quán)限的訪問(wèn)控制模型)。所以,在得到用戶(hù)id的情況下、先根據(jù)用戶(hù)角色表查出角色id(如果角色id的集合為空,說(shuō)明用戶(hù)沒(méi)有分配任何角色,直接返回用戶(hù)信息)、在根據(jù)角色權(quán)限表查詢(xún)權(quán)限id,在根據(jù)權(quán)限表查出具體權(quán)限名稱(chēng)。
上面使用了Mybatis-plus的條件構(gòu)造器和stream流的形式進(jìn)行查詢(xún)。
3、在JwtAuthenticationTokenFilter攔截器中,在查詢(xún)到用戶(hù)信息時(shí),將用戶(hù)的標(biāo)識(shí)和用戶(hù)擁有的權(quán)限一起放到SecurityContextHolder中,這樣后面的過(guò)濾器在獲取到用戶(hù)信息的同時(shí)也能獲取到用戶(hù)所擁有的權(quán)限;
代碼如下:
@Component @Slf4j public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisTemplate<String,String> redisTemplate; @Autowired private JwtUtil jwtUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //獲取請(qǐng)求頭中的token String token = request.getHeader("token"); System.out.println("前端的token信息=======>"+token); //如果token為空直接放行,由于用戶(hù)信息沒(méi)有存放在SecurityContextHolder.getContext()中所以后面的過(guò)濾器依舊認(rèn)證失敗符合要求 if(!StringUtils.hasText(token)){ filterChain.doFilter(request,response); return; } // 解析Jwt中的用戶(hù)id Integer userId = jwtUtil.getUsernameFromToken(token); //從redis中獲取用戶(hù)信息 String redisUser = redisTemplate.opsForValue().get(String.valueOf(userId)); if(!StringUtils.hasText(redisUser)){ filterChain.doFilter(request,response); return; } MyUserDetail myTUserDetail= JSON.parseObject(redisUser, MyUserDetail.class); log.info("Jwt過(guò)濾器中MyUserDetail的值============>"+myTUserDetail.toString()); //將用戶(hù)信息存放在SecurityContextHolder.getContext(),后面的過(guò)濾器就可以獲得用戶(hù)信息了。這表明當(dāng)前這個(gè)用戶(hù)是登錄過(guò)的,后續(xù)的攔截器就不用再攔截了 UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(myTUserDetail,null,myTUserDetail.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); filterChain.doFilter(request,response); } }
在這里解釋一下UsernamePasswordAuthenticationToken類(lèi):
UsernamePasswordAuthenticationToken是Spring Security中用于表示基于用戶(hù)名和密碼的身份驗(yàn)證令牌的類(lèi)。它主要有以下兩個(gè)構(gòu)造方法:
UsernamePasswordAuthenticationToken(Object principal, Object credentials)
- principal參數(shù)表示認(rèn)證主體,通常是用戶(hù)名或用戶(hù)對(duì)象。在身份驗(yàn)證過(guò)程中,這通常是用來(lái)標(biāo)識(shí)用戶(hù)的信息,可以是用戶(hù)名、郵箱等。
- credentials參數(shù)表示憑據(jù),通常是用戶(hù)的密碼或其他憑證信息。在身份驗(yàn)證過(guò)程中,這用于驗(yàn)證用戶(hù)的身份。
UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities)
- 除了上述兩個(gè)參數(shù)外,這個(gè)構(gòu)造方法還接受一個(gè)授權(quán)權(quán)限集合(authorities參數(shù))。這個(gè)集合表示用戶(hù)所擁有的權(quán)限,通常是一個(gè)包含用戶(hù)權(quán)限信息的集合。
- GrantedAuthority接口代表了用戶(hù)的權(quán)限信息,可以通過(guò)該接口的實(shí)現(xiàn)類(lèi)來(lái)表示用戶(hù)具體的權(quán)限。
這兩個(gè)構(gòu)造方法的作用是創(chuàng)建一個(gè)包含用戶(hù)身份信息、憑據(jù)信息和權(quán)限信息的身份驗(yàn)證令牌,以便在Spring Security中進(jìn)行身份驗(yàn)證和授權(quán)操作。通過(guò)這些構(gòu)造方法,可以將用戶(hù)的相關(guān)信息封裝成一個(gè)完整的身份驗(yàn)證對(duì)象,方便在安全框架中進(jìn)行處理和驗(yàn)證。
總之,UsernamePasswordAuthenticationToken是在Spring Security中用于表示用戶(hù)名密碼身份驗(yàn)證信息的重要類(lèi),通過(guò)不同的構(gòu)造方法可以滿(mǎn)足不同場(chǎng)景下的需求
所以我們通過(guò)myTUserDetail.getAuthorities()方法完全可以將用戶(hù)擁有的權(quán)限方法Security容器中,并供后續(xù)的攔截器獲取用戶(hù)信息和權(quán)限;
4、運(yùn)行測(cè)試:
接下來(lái)我編寫(xiě)一個(gè)基于方法的權(quán)限校驗(yàn),看我們編寫(xiě)的代碼是否生效;
(基于方法的權(quán)限認(rèn)證要在SecurityConfig類(lèi)上加上@EnableMethodSecurity注解,表示開(kāi)啟了方法權(quán)限的使用;)
新建一個(gè)TestController,并在這個(gè)類(lèi)中定義一個(gè)方法,用來(lái)測(cè)試:
@RestController @RequestMapping("/test") public class TestController { @PreAuthorize("hasAnyAuthority('所有權(quán)限')") @GetMapping("/hello") public Result hello(){ System.out.println("test接口中的hello方法調(diào)用========================"); return Result.successData("hello"); } }
在前端的Layout.vue頁(yè)面中新增一個(gè)按鈕,并綁定指定的方法用來(lái)測(cè)試;
代碼如圖:
const testHello = async() => { let data:any= await api.get("/test/hello") if(data.code===200){ ElMessage('有權(quán)限') } else{ ElMessage.error('沒(méi)有權(quán)限') } }
現(xiàn)在,我們來(lái)測(cè)試看看這個(gè)方法能不能被調(diào)用到:
可以看到這個(gè)方法被正確的訪問(wèn)到了,這是必須的因?yàn)檫@個(gè)”張喬“用戶(hù)有這個(gè)權(quán)限,那么我們改一下所需的權(quán)限看還能不能訪問(wèn)到;
點(diǎn)擊前端按鈕:
可以看到確實(shí)不能訪問(wèn)到了,這說(shuō)明我們的代碼是正確的;
我們權(quán)限校驗(yàn)的邏輯是:直接在登錄時(shí)查詢(xún)用戶(hù)的權(quán)限,并放在我們自定義的實(shí)現(xiàn)了UserDetail的接口類(lèi)中(MyUserDetail),用來(lái)表示登錄用戶(hù)的全部信息;
至此:我們前后端分離,使用vue3整合SpringSecurity實(shí)現(xiàn)登錄認(rèn)證和權(quán)限校驗(yàn)就已經(jīng)全部的講解完畢了,我還是會(huì)將前后端的源碼放在碼云上,有需要的童靴可以自行的下載:
碼云地址:
Vue-Security: 前后端分離的Security
到此這篇關(guān)于vue3整合SpringSecurity加JWT實(shí)現(xiàn)權(quán)限校驗(yàn)的文章就介紹到這了,更多相關(guān)vue3 SpringSecurity 權(quán)限校驗(yàn)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue中進(jìn)入詳情頁(yè)記住滾動(dòng)位置的方法(keep-alive)
今天小編就為大家分享一篇vue中進(jìn)入詳情頁(yè)記住滾動(dòng)位置的方法(keep-alive),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-09-09vue 返回上一頁(yè),頁(yè)面樣式錯(cuò)亂的解決
今天小編就為大家分享一篇vue 返回上一頁(yè),頁(yè)面樣式錯(cuò)亂的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-11-11如何使用Vue做個(gè)簡(jiǎn)單的比較兩個(gè)數(shù)字大小頁(yè)面
這篇文章主要給大家介紹了關(guān)于如何使用Vue做個(gè)簡(jiǎn)單的比較兩個(gè)數(shù)字大小頁(yè)面的相關(guān)資料,實(shí)現(xiàn)一個(gè)比較兩個(gè)數(shù)字大小的頁(yè)面,練習(xí)Vue實(shí)例的創(chuàng)建、數(shù)據(jù)綁定和事件監(jiān)聽(tīng)方法,需要的朋友可以參考下2023-10-10vue實(shí)現(xiàn)的微信機(jī)器人聊天功能案例【附源碼下載】
這篇文章主要介紹了vue實(shí)現(xiàn)的微信機(jī)器人聊天功能,結(jié)合實(shí)例形式分析了基于vue.js的微信機(jī)器人聊天相關(guān)界面布局、ajax交互等操作技巧,并附帶源碼供讀者下載參考,需要的朋友可以參考下2019-02-02lottie實(shí)現(xiàn)vue自定義loading指令及常用指令封裝詳解
這篇文章主要為大家介紹了lottie實(shí)現(xiàn)vue自定義loading指令及常用指令封裝,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09vue實(shí)現(xiàn)頁(yè)面刷新動(dòng)畫(huà)
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)頁(yè)面刷新動(dòng)畫(huà),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04Vue Element-UI中el-table實(shí)現(xiàn)單選的示例代碼
在element-ui中是為我們準(zhǔn)備好了可直接使用的單選與多選屬性的,本文主要介紹了Vue Element-UI中el-table實(shí)現(xiàn)單選的示例代碼,具有一定的參考價(jià)值,感興趣的可以了解一下2023-12-12vue3 + vite + ts 中使用less文件全局變量的操作方法
這篇文章主要介紹了vue3 + vite + ts 中使用less文件全局變量的操作方法,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-03-03vue3在單個(gè)組件中實(shí)現(xiàn)類(lèi)似mixin的事件調(diào)用
這篇文章主要為大家詳細(xì)介紹了vue3如何在單個(gè)組件中實(shí)現(xiàn)類(lèi)似mixin的事件調(diào)用,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-01-01Vue全家桶實(shí)踐項(xiàng)目總結(jié)(推薦)
本篇文章主要介紹了Vue全家桶實(shí)踐項(xiàng)目總結(jié)(推薦),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-11-11