前言
上篇文章介紹了Spring Boot Security基于Redis的Spring Session管理
本篇文章,可以說比較核心、實用的功能,動態(tài)用戶角色資源管理(RBAC),可能篇幅會比較長,廢話不多說,馬上進入正題
簡單介紹
相信每個正規(guī)的系統(tǒng),都會對系統(tǒng)安全和訪問權(quán)限有嚴格的控制。簡單的一句話總結(jié),就是對的人訪問對的資源,這里可能會比較抽象,小編給大家舉個例子就懂了:
現(xiàn)在假設有個系統(tǒng),里面有菜單A、菜單B和菜單C
客戶有這么個需求,就是對于管理員來說,可以訪問所有資源菜單,對于普通用戶來說,只能訪問菜單A和菜單B,如圖:
相信這個也是廣大系統(tǒng)都有的最基礎的需求,那么在系統(tǒng)中的表現(xiàn),就是用戶登錄了系統(tǒng)后,如果是普通用戶的話,前端只顯示菜單A和菜單B,其他途徑訪問(直接輸入URL)菜單C會被提示無權(quán)限,而管理員則顯示所有菜單
那么怎么實現(xiàn)呢,小編這里就是基于RABC模型去實現(xiàn)的,簡單來說就是:
舉個例子:
- 用戶就是登陸系統(tǒng)的用戶,像張三、李四、小王這樣的具體登陸用戶
- 角色就是假如張三是教師、李四是學生,那么教師和學生角色,也可能可以分得更細,這個根據(jù)需求來定義
- 資源就是訪問系統(tǒng)的資源,如查詢學生信息、編輯學生信息等等之類
用戶和資源是沒有直接關聯(lián)的,用戶是通過關聯(lián)角色,角色再關聯(lián)資源這種間接的方式去判斷自己的資源權(quán)限。這樣做的好處就是可以更簡單直觀的去管理用戶資源間的關聯(lián),不需要說每創(chuàng)建一個用戶,就去再重新分配資源這么繁瑣,減少數(shù)據(jù)庫冗余設計
數(shù)據(jù)庫設計
數(shù)據(jù)庫表的設計如圖:
這里有幾點要說明下:
- 一般 用戶 與 角色 是一對一或者一對多的關系,我這里為了方便所以選擇一對一的關系
- 角色 與 資源 是多對多的關系,所以需要中間表 sys_role_resource 存儲中間的聯(lián)系
實體代碼如下:
Role.java
package com.demo.ssdemo.sys.entity;
import com.alibaba.fastjson.annotation.JSONField;
import org.springframework.security.core.GrantedAuthority;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Set;
@Entity
@Table(name = "sys_role")
public class Role implements GrantedAuthority {
//id
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column
protected Integer id;
//角色標識
@Column
private String roleKey;
//角色名稱
@Column
private String roleName;
//角色擁有的資源
@ManyToMany(targetEntity = Resource.class, fetch = FetchType.EAGER)
@JoinTable(
name = "sys_role_resource",
joinColumns = {
@JoinColumn(name = "role_id", referencedColumnName = "id", nullable = false)
},
inverseJoinColumns = {
@JoinColumn(name = "resource_id", referencedColumnName = "id", nullable = false)
})
private Set<Resource> resources;
@Override
public String getAuthority() {
return roleKey;
}
...get、set方法...
}
這里要說明下,GrantedAuthority 接口中的getAuthorities()方法返回的當前用戶對象擁有的權(quán)限,簡單的說就是該用戶的角色信息,所以這里我用角色標識roleKey表示
Resource.java
package com.demo.ssdemo.sys.entity;
import javax.persistence.*;
import java.io.Serializable;
@Entity
@Table(name = "sys_resource")
public class Resource implements Serializable {
//id
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column
protected Integer id;
//資源名稱
@Column(nullable = false)
private String resourceName;
//資源標識
@Column(nullable = false)
private String resourceKey;
//資源url
@Column(nullable = false)
private String url;
/**
* 資源類型
* 0:菜單
* 1:按鈕
*/
@Column(nullable = false)
private Integer type;
...get、set方法...
}
相信這些代碼大家都看得明白,下面開始進入核心部分
實現(xiàn)
在這里,小編介紹下怎么在Spring Security中實現(xiàn)資源管理功能,也就是針對不同的用戶角色,動態(tài)的判斷是否能訪問相應的資源菜單
先看看項目結(jié)構(gòu)圖:
首先,我們需要在自定義登錄認證那里,設置權(quán)限信息:
LoginValidateAuthenticationProvider.java
package com.demo.ssdemo.core;
import com.demo.ssdemo.sys.entity.User;
import com.demo.ssdemo.sys.service.UserService;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.HashSet;
import java.util.Set;
/**
* @Description 自定義登陸驗證
**/
@Component
public class LoginValidateAuthenticationProvider implements AuthenticationProvider {
@Resource
private UserService userService;
@Resource
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//獲取輸入phone
String username = authentication.getName();
String rawPassword = (String) authentication.getCredentials();
//查詢用戶是否存在
User user = (User) userService.loadUserByUsername(username);
if (user.isEnabled()) {
throw new DisabledException("該賬戶已被禁用,請聯(lián)系管理員");
} else if (user.isAccountNonLocked()) {
throw new LockedException("該賬號已被鎖定");
} else if (user.isAccountNonExpired()) {
throw new AccountExpiredException("該賬號已過期,請聯(lián)系管理員");
} else if (user.isCredentialsNonExpired()) {
throw new CredentialsExpiredException("該賬戶的登錄憑證已過期,請重新登錄");
}
//驗證密碼
if (!passwordEncoder.matches(rawPassword, user.getPassword())) {
throw new BadCredentialsException("輸入密碼錯誤!");
}
//設置角色權(quán)限信息
Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
grantedAuthorities.add(new SimpleGrantedAuthority(user.getRole().getRoleKey()));
user.setAuthorities(grantedAuthorities);
return new UsernamePasswordAuthenticationToken(user, rawPassword, user.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
//確保authentication能轉(zhuǎn)成該類
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
這里要注意的是,我們把resource實體的resourceKey作為資源的權(quán)限標識,設置進grantedAuthorities集合里面,以便spring security根據(jù)注解@PreAuthorize自動權(quán)限判斷
由于我們設計的用戶與角色是一對一關聯(lián),所以我們這里GrantedAuthority集合就只有一條角色信息數(shù)據(jù)
然后就是自定義權(quán)限不足handler
PerAccessDeniedHandler.java
package com.demo.ssdemo.core.handler;
import com.alibaba.fastjson.JSONObject;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
@Component
public class PerAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//登錄成功返回
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("code", "503");
paramMap.put("message", accessDeniedException.getMessage());
//設置返回請求頭
response.setContentType("application/json;charset=utf-8");
//寫出流
PrintWriter out = response.getWriter();
out.write(JSONObject.toJSONString(paramMap));
out.flush();
out.close();
}
}
最后我們看看Spring Security配置類的變化:
SecurityConfig.java
package com.demo.ssdemo.config;
import com.demo.ssdemo.core.LoginValidateAuthenticationProvider;
import com.demo.ssdemo.core.handler.LoginFailureHandler;
import com.demo.ssdemo.core.handler.LoginSuccessHandler;
import com.demo.ssdemo.core.handler.PerAccessDeniedHandler;
import com.demo.ssdemo.sys.service.UserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.annotation.Resource;
import javax.sql.DataSource;
/**
* @Author OZY
* @Date 2019/08/08 13:59
* @Description
* @Version V1.0
**/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 數(shù)據(jù)源
*/
@Resource
private DataSource dataSource;
/**
* 用戶業(yè)務層
*/
@Resource
private UserService userService;
/**
* 自定義認證
*/
@Resource
private LoginValidateAuthenticationProvider loginValidateAuthenticationProvider;
/**
* 登錄成功handler
*/
@Resource
private LoginSuccessHandler loginSuccessHandler;
/**
* 登錄失敗handler
*/
@Resource
private LoginFailureHandler loginFailureHandler;
/**
* 權(quán)限不足handler
*/
@Resource
private PerAccessDeniedHandler perAccessDeniedHandler;
/**
* 權(quán)限核心配置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//基礎設置
http.httpBasic()//配置HTTP基本身份驗證
.and()
.authorizeRequests()
.anyRequest().authenticated()//所有請求都需要認證
.and()
.formLogin() //登錄表單
.loginPage("/login")//登錄頁面url
.loginProcessingUrl("/login")//登錄驗證url
.defaultSuccessUrl("/index")//成功登錄跳轉(zhuǎn)
.successHandler(loginSuccessHandler)//成功登錄處理器
.failureHandler(loginFailureHandler)//失敗登錄處理器
.permitAll()//登錄成功后有權(quán)限訪問所有頁面
.and()
.exceptionHandling().accessDeniedHandler(perAccessDeniedHandler)//設置權(quán)限不足handler
.and()
.rememberMe()//記住我功能
.userDetailsService(userService)//設置用戶業(yè)務層
.tokenRepository(persistentTokenRepository())//設置持久化token
.tokenValiditySeconds(24 * 60 * 60); //記住登錄1天(24小時 * 60分鐘 * 60秒)
//關閉csrf跨域攻擊防御
http.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//這里要設置自定義認證
auth.authenticationProvider(loginValidateAuthenticationProvider);
}
/**
* BCrypt加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 記住我功能,持久化的token服務
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
//數(shù)據(jù)源設置
tokenRepository.setDataSource(dataSource);
//啟動的時候創(chuàng)建表,這里只執(zhí)行一次,第二次就注釋掉,否則每次啟動都重新創(chuàng)建表
//tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
}
在Spring Security配置文件中,我們只需要設置PerAccessDeniedHandler 就可以了,還要記得在頭部添加@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled=true)注解,以啟動spring security注解生效
接下來就是前端頁面和控制層:
package com.demo.ssdemo.sys.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/")
public class UserController {
/**
* 登錄頁面跳轉(zhuǎn)
* @return
*/
@GetMapping("login")
public String login() {
return "login.html";
}
/**
* index頁跳轉(zhuǎn)
* @return
*/
@GetMapping("index")
public String index() {
return "index.html";
}
/**
* menu1
* @return
*/
@PreAuthorize("hasAuthority('menu1')")
@GetMapping("menu1")
@ResponseBody
public String menu1() {
return "menu1";
}
/**
* menu2
* @return
*/
@PreAuthorize("hasAuthority('menu2')")
@GetMapping("menu2")
@ResponseBody
public String menu2() {
return "menu2";
}
/**
* menu3
* @return
*/
@PreAuthorize("hasAuthority('menu3')")
@GetMapping("menu3")
@ResponseBody
public String menu3() {
return "menu3";
}
}
這里要注意的是,每個需要權(quán)限判斷的方法中,都需要增加@PreAuthorize("hasAuthority('key')")注解,否則權(quán)限判斷不生效,key對應數(shù)據(jù)庫資源表中的資源標識字段
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index頁</title>
</head>
<body>
index頁<br/><br/>
<button id="menu1Btn" type="button" onclick="sendAjax('/menu1')">菜單1</button>
<button id="menu2Btn" type="button" onclick="sendAjax('/menu2')">菜單2</button>
<button id="menu3Btn" type="button" onclick="sendAjax('/menu3')">菜單3</button>
<script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
<script type="text/javascript">
function sendAjax(url) {
$.ajax({
type: "GET",
url: url,
dataType: "text",
success: function (data) {
console.log(data);
}
});
}
</script>
</body>
</html>
這里簡單的說說數(shù)據(jù)庫的數(shù)據(jù)
用戶表:admin、teacher1和student1
角色表:管理員、教師和學生
資源表:menu1、menu2、menu3
對應權(quán)限:
管理員:menu1、menu2、menu3
教師:menu1、menu2
學生:meun1
下面我們看看效果,登錄頁:
index頁:
這里我們先用admin管理員角色登錄,然后點擊所有菜單:
可以看到數(shù)據(jù)正常,并且已經(jīng)訪問到了所有資源菜單
然后我們用 teacher1教師角色 登錄,也是點擊所有菜單:
會發(fā)現(xiàn),在點擊第三個菜單的時候,會返回沒有權(quán)限訪問
我們再用 student1學生角色 登錄,也是點擊所有菜單:
這里說明我們的動態(tài)權(quán)限資源管理都生效了
那么文章就介紹到這里,在這里留了個坑,一般系統(tǒng)是不會讓用戶去點擊了菜單才發(fā)現(xiàn)沒有權(quán)限訪問,而是針對不同的用戶,動態(tài)顯示不同的菜單,這個內(nèi)容小編下篇文章就會講解
demo也已經(jīng)放到github,獲取方式在文章的Spring Boot2 + Spring Security5 系列搭建教程開頭篇(1) 結(jié)尾處
如果小伙伴遇到什么問題,或者哪里不明白歡迎評論或私信,也可以在公眾號里面私信問都可以,謝謝大家~