學習SpringBoot+Vue前後端分離項目,原項目GitHub位址,項目作者江雨一點雨部落格。
前後端分離的權限管理
該部分來自江雨一點雨
在傳統的前後端不分的開發中,權限管理主要通過過濾器或者攔截器來進行(權限管理架構本身也是通過過濾器來實作功能),如果使用者不具備某一個角色或者某一個權限,則無法通路某一個頁面。
但是在前後端分離中,頁面的跳轉統統交給前端去做,後端隻提供資料,這種時候,權限管理不能再按照之前的思路來。
首先要明确一點,前端是展示給使用者看的,所有的菜單顯示或者隐藏目的不是為了實作權限管理,而是為了給使用者一個良好的體驗,不能依靠前端隐藏控件來實作權限管理,即資料安全不能依靠前端。
這點就像普通的表單送出一樣,前端做資料校驗是為了提高效率,提高使用者體驗,後端才是真正的確定資料完整性。
是以,真正的資料安全管理是在後端實作的,後端在接口設計的過程中,就要確定每一個接口都是在滿足某種權限的基礎上才能通路,也就是說,不怕将後端資料接口位址暴露出來,即使暴露出來,隻要你沒有相應的角色,也是通路不了的。
前端為了良好的使用者體驗,需要将使用者不能通路的接口或者菜單隐藏起來。
有人說,如果使用者直接在位址攔輸入某一個頁面的路徑,怎麼辦?此時,如果沒有做任何額外的處理的話,使用者确實可以通過直接輸入某一個路徑進入到系統中的某一個頁面中,但是,不用擔心資料洩露問題,因為沒有相關的角色,就無法通路相關的接口。
但是,如果使用者非這樣操作,進入到一個空白的頁面,使用者體驗不好,此時,我們可以使用 Vue 中的前置路由導航守衛,來監聽頁面跳轉,如果使用者想要去一個未獲授權的頁面,則直接在前置路由導航守衛中将之攔截下來,重定向到登入頁,或者直接就停留在目前頁,不讓使用者跳轉,也可以順手再給使用者一點點未獲授權的提示資訊。
總而言之一句話,前端的所有操作,都是為了提高使用者體驗,不是為了資料安全,真正的權限校驗要在後端來做,後端如果是 SSM 架構,建議使用 Shiro ,如果是 Spring Boot + 微服務,建議使用 Spring Security 。
後端接口權限設計
建立src/config/CustomFilterInvocationSecurityMetadataSource
@Component //注冊為元件
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
MenuService menuService;
//比對工具,這裡用來比對request的url和menu的url
AntPathMatcher antPathMatcher=new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
//目前請求的位址
String requestUrl = ((FilterInvocation) object).getRequestUrl();
List<Menu> menus = menuService.getAllMenusWithRole();
for (Menu menu : menus) {
//match()中第一個是比對規則,第二個是需要比對的對象
if (antPathMatcher.match(menu.getUrl(),requestUrl)){
List<Role> roles = menu.getRoles();
String[] str =new String[roles.size()];
for (int i = 0; i < roles.size(); i++) {
str[i]=roles.get(i).getName();
}
return SecurityConfig.createList(str);
}
}
//沒有比對上的 登陸後通路 标記 後續判斷用
return SecurityConfig.createList("ROLE_LOGIN");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
在model/Menu和Hr中添加Role及其getter、setter方法
修改Hr中的Collection
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities =new ArrayList<>(roles.size());
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
在HrService中給使用者設定角色
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Hr hr=hrMapper.loadUserByUsername(username);
if (hr==null){
throw new UsernameNotFoundException("使用者名不存在");
}
//設定角色
hr.setRoles(hrMapper.getHrRolesById(hr.getId()));
return hr;
}
在HrMapper建立getHrRolesById這個方法
在HrMapper.xml寫SQL語句
<select id="getHrRolesById" resultType="org.javaboy.vhr.model.Role">
SELECT r.* FROM role r,hr_role hrr WHERE hrr.`rid`=r.`id` AND hrr.`hrid`=#{id}
</select>
在MenuService中添加getAllMenusWithRole方法
//@Cacheable 緩存 後面再用
public List<Menu> getAllMenusWithRole(){
return menuMapper.getAllMenusWithRole();
}
在MenuMapper中也添加這個方法
在MenuMapper.xml中添加SQL語句
<resultMap id="MenuWithRole" type="org.javaboy.vhr.model.Menu" extends="BaseResultMap">
<collection property="roles" ofType="org.javaboy.vhr.model.Role">
<id column="rid" property="id" />
<result column="rname" property="name"/>
<result column="rnameZh" property="nameZh"/>
</collection>
</resultMap>
<select id="getAllMenusWithRole" resultMap="MenuWithRole">
select m.*,r.`id` as rid,r.`name` as rname,r.`nameZh` as rnameZh
from menu m,menu_role mr,role r
where m.`id`=mr.`mid` and mr.`rid`=r.`id` order by m.`id`
</select>
先在資料庫中寫好SQL語句,測試無誤後,再寫在xml中。
建立src/config/CustomUrlDecisionManager
@Component //注冊為元件
public class CustomUrlDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute configAttribute : configAttributes) {
//使用者所需角色
String needRole = configAttribute.getAttribute();
if ("ROLE_LOGIN".equals(needRole)){
//判斷目前使用者是否是匿名使用者
if (authentication instanceof AnonymousAuthenticationToken){
throw new AccessDeniedException("尚未登入,請登入!");
}else {
return;
}
}
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)){
return;
}
}
throw new AccessDeniedException("權限不足,請聯系管理者");
}
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
再SecurityConfig中引入上面兩個
@Autowired
CustomUrlDecisionManager customUrlDecisionManager;
@Autowired
CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//.anyRequest().authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(customUrlDecisionManager);
object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
return object;
}
})
.and()
.formLogin()
再添加一個方法
//給登入頁放行 不會被攔截
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/login");
}