基於RABC模型的SpringSecurity許可權控制能力
RBAC許可權模型
全名為:Role-Based Access Control 譯為基於角色的訪問控制。
RBAC許可權框架基於角色進行鑑權,在該框架中具有三大模組:角色(Role)、使用者(User)、許可權(Permissions),
RBAC使用最小特權原則,當前請求訪問的使用者具備那些角色,該角色具備那些許可權,所具備的許可權中是否包含本次訪問所需的許可權?若具有,正常訪問返回,若不具有,給予使用者提示,所以,RBAC可以把許可權粒度做到方法級。
SpringSecurity是基於RBAC模型輕量級許可權控框架,與之對等的還有Apache Shiro,由於Spring的生態不斷完善、功能日益豐富,使得SpringSecurity越來越越受歡迎。
一般的,SpringSecurity的許可權控制設計思路為:User - User_Role -Role -Role_Menu -Menu,即:使用者屬於什麼角色,該角色具有什麼許可權,具有該許可權可以訪問那些頁面,如若把許可權控制在方法級別,可以使用SpringSecurity註解在後端方法上,從而做到按鈕級別的許可權控制,以上,便完成了許可權訪問控制。
資料庫設計便為:
- User:使用者表
- User_Role:使用者角色中間表
- Role:角色表
- Role_Menu:角色選單中間表
- Menu:選單表
(使用者可能有多個角色,一個角色可能有多個使用者,所以使用者和角色是多對多的關係)
Menu可以理解為許可權,在Web中,選單中的顯示與否可以視為使用者是否具備該許可權
如此便完成了許可權控制的設計方案。
SpringSecurity使用步驟:
引入SpringSecurity模組
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
加入這個依賴後表示所有的介面都是被保護的狀態,訪問的時候被Security攔截。
在瀏覽器輸入該請求路徑,會自重定向到Spring Security的登入頁。預設的使用者名稱是user,密碼請去IDEA的Consolse去找專案每次啟動時隨機生成的字串:
Using generated security password: 5a38aea2-81d0-485d-bf5c-12c73b0aad27
(複製passwor後的內容即可訪問)
同時也支援在資料庫配置使用者名稱和密碼(正式專案一般處理方式)或在配置檔案配置使用者名稱密碼,本文使用的是yml配置,properties同理,如果不知道如何配置,請自行百度。配置後在Console中便不自動生成password
配置HTTP攔截規則
配置介面放行規則
SecurityConfig.class
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Autowired
private RedisService redisUtil;
@Autowired
private SecurityFilter securityFilter;
@Autowired
private OwnAccessDecisionManager ownAccessDecisionManager;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
/**
* 定義角色繼承
*/
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_dba> ROLE_admin > ROLE_user");
return roleHierarchy;
}
/**
* 配置HTTP請求規則
* 什麼請求路徑需要什麼許可權才能訪問,
* 登入介面,都可訪問
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("user/**").hasAnyRole("admin", "user")
.anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/doLogin")
.usernameParameter("userName")
.passwordParameter("passWord")
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
//登陸成功處理控制程式碼,前後分離專案,給前端返回Json即可
resp.setContentType("application/json;charset=utf-8");
Map<String, Object> map = new HashMap<>();
map.put("status", HttpServletResponse.SC_OK);
User principal = (User) authentication.getPrincipal();
String token = JwtUtil.sign(principal.getUsername(), principal.getPassword());
map.put("msg", authentication.getPrincipal());
map.put("token", token);
map.put("userName", principal.getUsername());
redisUtil.setCacheObject(BusinessConstant.REDIS_RELATED.PREFIX + LocalDateUtils.getStartTimeOfDayStr() + principal.getId(), map);
ResponseUtil.responseJson(resp, HttpStatus.OK.value(), map);
}
})
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
//登入失敗處理 AuthenticationException:鎖定異常問題
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
Map<String, Object> map = new HashMap<>();
map.put("status", HttpServletResponse.SC_UNAUTHORIZED);
if (e instanceof LockedException) {
map.put("msg", ResponseEnum.USER_ACCOUNT_LOCKED.getMessage());
} else if (e instanceof BadCredentialsException) {
map.put("msg", ResponseEnum.USER_NOT_EXIST_OR_ERROR.getMessage());
} else {
map.put("msg", ResponseEnum.LOGIN_FILURE.getMessage());
}
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
})
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString("登出成功!"));
out.flush();
out.close();
}
})
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setAccessDecisionManager(ownAccessDecisionManager);
o.setSecurityMetadataSource(securityFilter);
return o;
}
})
.and()
.csrf().disable()
.exceptionHandling()
//沒有許可權時返回Json,而不是重定向到登入頁,方便前後端分離專案使用
.authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException authException) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
if (authException instanceof InsufficientAuthenticationException) {
throw new RuntimeException(ResponseEnum.SYSTEM_INNER_ERROR.getMessage());
}
// ResponseEnum.SYSTEM_INNER_ERROR.assertException(authException);
out.write(new ObjectMapper().writeValueAsString(authException));
out.flush();
out.close();
}
});
}
SecurityFilter.class
根據請求地址分析出該地址需要那些角色,並檢視請求的使用者是否具備該角色
@Component
public class SecurityFilter implements FilterInvocationSecurityMetadataSource{
@Autowired
private MenuService menuService;
@Autowired
private RedisService redisService;
/**
* Ant規則匹配符
*/
AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) o).getRequestUrl();
List<Menu> allMenus;
allMenus = redisService.getCacheObject(BusinessConstant.REDIS_RELATED.MENU_ALL);
if (CollectionUtils.isEmpty(allMenus)) {
allMenus = menuService.getAllMenus();
}
for (Menu menu : allMenus) {
if (pathMatcher.match(menu.getPattern(), requestUrl)) {
List<Role> roles = menu.getRoles();
String[] rolesStr = new String[roles.size()];
for (int i = 0; i < roles.size(); i++) {
rolesStr[i] = roles.get(i).getName();
}
return SecurityConfig.createList(rolesStr);
}
}
//需要的角色都不滿足條件,非法請求
return SecurityConfig.createList("ROLE_login");
}
@Override
/**
*根據需要的角色檢視當前使用者是否具有該角色
*/
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
OwnAccessDecisionManager.class
為當前的訪問規則進行決策,是否給予訪問的許可權。
@Component
public class OwnAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute attribute : collection) {
if ("ROLE_login".equals(attribute.getAttribute())) {
if (authentication instanceof AnonymousAuthenticationToken) {
throw new AccessDeniedException(ResponseEnum.PERMISSION_NOT_SAFE.getMessage());
} else {
return;
}
}
//查詢訪問所需角色,當前登入使用者是否具備所需角色的其中的一個
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(attribute.getAttribute())) {
return;
}
}
throw new AccessDeniedException(ResponseEnum.USER_ROLE_PERMISSION_ERROR.getMessage());
}
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
使用postman測試,所以關閉CSRF攻擊,正式環境請開啟 記得要刪掉super.configure(http); 不然會報錯IllegalStateException: Can't configure anyRequest after itself ObjectMapper類是Jackson庫的主要類。它提供一些功能將轉換成Java物件匹配JSON結構,反之亦然。它使用JsonParser和JsonGenerator的例項實現JSON實際的讀/寫。
表單登入測試
使用post請求構造表單登入,SpringSecurity已做密碼脫敏,許可權中預設使用"ROLE_"為字首。
表單登出測試
登出配置如上程式碼,構造get請求即可。
使用資料庫的方式做登入認證
由於篇幅原因,不宜過長,所以我是分開書寫的,許可權功能需要整合資料庫相關,在我的另一篇文章中:
SpringBoot整合Redis、MyBatis-Plus
因為敲完這個demo,時間不是很充足,所以沒有更新文章,SpringBoot下與資料庫互動使用許可權認證請去我的github去尋找原始碼,思路根據江南一點雨(鬆哥)的許可權認證思路而來。
相關文章
- 基於RBAC的許可權設計模型模型
- SpringSecurity許可權管理系統實戰—九、資料許可權的配置SpringGse
- React基於RBAC的許可權控制React
- casbin基於golang的許可權控制Golang
- 許可權模型:ACL模型
- .NET 8 + Vue 3 極簡 RABC 許可權管理系統Vue
- SpringSecurity許可權管理系統實戰—四、整合SpringSecurity(上)SpringGse
- SpringSecurity許可權管理系統實戰—五、整合SpringSecurity(下)SpringGse
- SpringSecurity許可權管理系統實戰—六、SpringSecurity整合JWTSpringGseJWT
- 基於casbin的RBAC許可權實踐
- 基於RBAC的許可權管理系統
- 基於 PHP 反射的許可權匯入PHP反射
- SpringSecurity:hasAuthority與自定義許可權校驗SpringGse
- 基於RBAC實現許可權管理
- 基於RBAC做資料許可權
- 基於 PHP-Casbin 的 ABAC 許可權控制PHP
- 基於Linux許可權提升的資訊收集Linux
- 基於tp3.2.3開發的許可權管理系統,路由,微信,cdn,許可權路由
- 【SpringSecurity系列3】基於Spring Webflux整合SpringSecurity實現前後端分離無狀態Rest API的許可權控制SpringGseWebUX後端RESTAPI
- 基於角色的許可權系統的問題
- Android安全—許可權模型Android模型
- 許可權系統:6個許可權概念模型設計模型
- TP 基於選單的許可權控制CMS框架框架
- 基於vue(element ui) + ssm + shiro 的許可權VueUISSM
- Spring Security 基於URL的許可權判斷Spring
- 基於二進位制的許可權判斷
- SpringBoot--- SpringSecurity進行登出,許可權控制Spring BootGse
- Oracle的物件許可權、角色許可權、系統許可權Oracle物件
- shiro教程(1):基於url許可權管理
- RBAC_許可權模型介紹模型
- 許可權+訂單資料模型模型
- 後端基於方法的許可權控制--Spirng-Security後端
- 基於tp3.2.3 的許可權管理系統更新
- 基於SSM框架的JavaWeb通用許可權管理系統SSM框架JavaWeb
- 基於vue(element ui) + ssm + shiro 的許可權框架VueUISSM框架
- Jenkins配置基於角色的專案許可權管理Jenkins
- Quarkus中基於角色的許可權訪問控制教程
- 關於動態許可權