基於Spring Security實現許可權管理系統
基於Spring Security
實現許可權管理系統
稍微複雜一點的後臺系統都會涉及到使用者許可權管理。何謂使用者許可權?我的理解就是,許可權就是對資料(系統的實體類
)和資料可進行的操作(增刪查改
)的集中管理。要構建一個可用的許可權管理系統,涉及到三個核心類:一個是使用者User
,一個是角色Role
,最後是許可權Permission
。接下來本文將介紹如何基於Spring Security 4.0
一步一步構建起一個介面級別的許可權管理系統。
1. 相關概念
- 許可權(Permission) = 資源(Resource) + 操作(Privilege)
- 角色(Role) = 許可權的集合(a set of low-level permissions)
- 使用者(User) = 角色的集合(high-level roles)
2. Spring Security的maven
依賴
Spring Boot
版本雖然已經到2.0
了,但是之前使用的時候發現了一些坑,所以推薦還是暫時使用比較穩定的1.5
版本。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xxx.xxx</groupId>
<artifactId>api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>security-demo</name>
<description>Demo project for spring security</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.7</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Camden.SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
3. 定義系統的許可權集合
許可權是資源以及可對資源進行的操作的一個集合。對於我們的系統來說,幾乎所有實體類都可以看作一個資源,而常見的操作也就是增刪查改
四類,當然,根據我們實際的業務需要,可能還有其他的特殊操作,比如我們這裡加了一個匯入使用者
的操作。這裡簡單列舉兩個基本的許可權集合:
[
{
"resourceId":"permission",
"resourceName":"許可權",
"privileges": {
"read":"檢視",
"write":"新增",
"update":"更新",
"delete":"刪除"
}
},
{
"resourceId":"user",
"resourceName":"使用者",
"privileges": {
"read":"檢視使用者列表",
"write":"新增使用者",
"import":"匯入使用者",
"update":"修改使用者資訊",
"delete":"刪除使用者"
}
}
]
在對許可權的定義中,關鍵是resourceId
和privileges
的key
,後續將使用這兩者結合來對使用者的許可權進行判斷。我這裡使用resourceId-privilege
這樣的形式來唯一表示對某個資源進行的某個操作。
4. 角色相關的操作
資源與操作許可權集合類定義JsonPermissions
:
@Data
public class JsonPermissions {
private List<SimplePermission> permissions;
@Data
public static class SimplePermission {
/**
* 資源id
*/
private String resourceId;
/**
* 資源名
*/
private String resourceName;
/**
* 許可權列表
*/
private Map<String, String> privileges;
/**
* 是否被遺棄
*/
private boolean abandon = false;
}
}
角色類定義Role
:
import lombok.Data;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.List;
@Document(collection = "role")
@Data
public class Role {
@Id
private String id;
/**
* 建立時間
*/
private Long createdTime = System.currentTimeMillis();
/**
* 是否被移除
*/
private Boolean isRemoved = false;
/**
* 角色名,用於許可權校驗
*/
private String name;
/**
* 角色中文名,用於顯示
*/
private String nickname;
/**
* 角色描述資訊
*/
private String description;
/**
* 是否為內建
*/
private boolean builtIn = false;
/**
* 角色狀態,是否已禁用
*/
private Boolean banned = false;
/**
* 角色可進行的操作列表
*/
private List<JsonPermissions.SimplePermission> permissions;
/**
* 角色建立者
*/
private String proposer;
/**
* Spring Security 4.0以上版本角色都預設以'ROLE_'開頭
* @param name
*/
public void setName(String name) {
if (name.indexOf("ROLE_") == -1) {
this.name = "ROLE_" + name;
} else {
this.name = name;
}
}
}
5. 給使用者賦予角色
Spring Security
框架提供了一個基礎使用者介面UserDetails
,該介面提供了基本的使用者相關的操作,比如獲取使用者名稱/密碼、使用者賬號是否過期和使用者認證是否過期等,我們定義自己的User
類時需要實現該介面。
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.*;
@Data
@NoArgsConstructor
public class User implements UserDetails {
public static final PasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder();
@Id
private String id;
/**
* 建立時間
*/
private Long createdTime = System.currentTimeMillis();
/**
* 使用者登入名
*/
private String username;
/**
* 使用者真實姓名
*/
private String realName;
/**
* 使用者登入密碼,使用者的密碼不應該暴露給客戶端
*/
@JsonIgnore
private String password;
/**
* 使用者型別
*/
private String type;
/**
* 該使用者關聯的企業/區塊id
*/
private Map<String, Object> associatedResources = new HashMap<>();
/**
* 使用者關注的企業列表
*/
private List<String> favourite = new ArrayList<>();
/**
* 使用者在系統中的角色列表,將根據角色對使用者操作許可權進行限制
*/
private List<String> roles = new ArrayList<>();
public void setPassword(String password) {
this.password = PASSWORD_ENCODER.encode(password);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
6. 建立系統的初始角色和超級管理員
如果我們對系統的所有介面都加上了訪問限制,那麼由誰來作為初始使用者登入系統並建立其他使用者呢?所以我們需要定義系統的初始角色和初始使用者,並在系統啟動時將初始角色和初始使用者自動錄入系統,然後再使用初始使用者登入系統去建立其他業務相關的使用者。定義系統的超級管理員角色:roles.json
[
{
"name":"ROLE_ADMINISTRATOR",
"nickname":"管理員",
"description":"系統超級管理員,不允許使用者更改",
"banned":false,
"state":"normal",
"permissions":[
{
"resourceId":"permission",
"resourceName":"許可權",
"privileges": {
"read":"檢視",
"write":"新增",
"update":"更新",
"delete":"刪除"
}
},
{
"resourceId":"user",
"resourceName":"使用者",
"privileges": {
"read":"檢視使用者列表",
"write":"新增使用者",
"import":"匯入使用者",
"update":"修改使用者資訊",
"delete":"刪除使用者"
}
}
]
}
]
定義系統的初始管理員使用者:users.json
[
{
"username":"admin",
"realName":"超超超級管理員",
"password":"$2a$10$GhI1umKcTHysip4iSFXPXOQG1x9U.4eCWMEFwF/h3LBAt98K4o1B.",
"number":"admin",
"type":"system",
"activated":true,
"roles":["ROLE_ADMINISTRATOR"]
}
]
7. 載入系統初始化角色和使用者資料
在系統部署時,需要將系統的初始化角色和使用者自動載入到資料庫中,這樣才能正常登入使用。使用@Component
和@PostConstruct
註解在系統啟動時自動匯入初始化角色和使用者。
import com.google.gson.reflect.TypeToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.beans.factory.annotation.Value;
import javax.annotation.PostConstruct;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
/**
* 系統初始化配置類,主要用於載入內建資料到目標資料庫上
*/
@Component
public class SystemInitializer {
@Value("${initialzation.file.users:users.json}") private String userFileName;
@Value("${initialzation.file.roles:roles.json}") private String roleFileName;
@Autowired
private UserRepository userRepository;
@Autowired private RoleRepository roleRepository;
@PostConstruct
public boolean initialize() throws Exception {
try {
InputStream userInputStream = getClass().getClassLoader().getResourceAsStream(userFileName);
if(userInputStream == null){
throw new Exception("initialzation user file not found: " + userFileName);
}
InputStream roleInputStream = getClass().getClassLoader().getResourceAsStream(roleFileName);
if(roleInputStream == null){
throw new Exception("initialzation role file not found: " + roleFileName);
}
//匯入初始的系統超級管理員角色
Type roleTokenType = new TypeToken<ArrayList<Role>>(){}.getType();
ArrayList<Role> roles = CommonGsonBuilder.create().fromJson(new InputStreamReader(roleInputStream, StandardCharsets.UTF_8), roleTokenType);
for (Role role: roles) {
if (roleRepository.findByName(role.getName()) == null) {
roleRepository.save(role);
}
}
//匯入初始的系統管理員使用者
Type teacherTokenType = new TypeToken<ArrayList<User>>(){}.getType();
ArrayList<User> users = CommonGsonBuilder.create().fromJson(new InputStreamReader(userInputStream, StandardCharsets.UTF_8), teacherTokenType);
for (User user : users) {
if (userRepository.findByUsername(user.getUsername()) == null) {
userRepository.save(user);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
}
8. 實現自己的UserDetailsService
在UserDetailService
中自定義載入使用者資訊,並將使用者角色role
相關的所有Permissions
設定到Authentication
的authorities
中以供PermissionEvaluator
對使用者許可權進行判斷。注意這裡使用了resourceId-privilege
的形式進行了拼接後存放。我這裡使用者資訊是存放在MongoDB
資料庫中的,也可以換成其他的資料庫。
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private IUserService userService;
@Autowired
private MongoTemplate mongoTemplate;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(String.format("No user found with username: %s", username));
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
List<String> roles = user.getRoles();
for (String roleName : roles) {
Role role = mongoTemplate.findOne(Query.query(Criteria.where("name").is(roleName)), Role.class);
if (role == null) {
continue;
}
for (JsonPermissions.SimplePermission permission : role.getPermissions()) {
for (String privilege : permission.getPrivileges().keySet()) {
authorities.add(new SimpleGrantedAuthority(String.format("%s-%s", permission.getResourceId(), privilege)));
}
}
}
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), authorities);
}
}
9. 配置UserDetailsService
import org.springframework.beans.factory.annotation.Autowired;
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.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.web.authentication.logout.LogoutHandler;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeRequests().antMatchers(
"/js/**",
"/css/**",
"/img/**",
"/login/**").permitAll()
.anyRequest().authenticated()
.and().formLogin().permitAll()
.cors();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
}
10. 後端介面根據許可權實現訪問限制
在需要進行訪問限制的介面方法上面加上PreAuthorize
註解,在該註解中我們可以使用多種校驗方法,比較常見的有hasPermisson
和hasRole
兩個。而和PreAuthorize
類似的還有PostAuthorize
註解,從字面意義也比較好理解PreAuthorize
是在訪問介面前進行校驗,而PostAuthorize
是在訪問介面後返回結果時進行校驗。
@GetMapping(value = "/list")
@PreAuthorize("hasPermission('user', 'read') or hasRole('ROLE_ADMINISTRATOR')")
public List<?> getUserList(@RequestParam(value = "text", defaultValue = "") String text,
@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "20") int size) {
return userService.list(text, page, size);
}
以此類推,可以在需要對使用者訪問進行限制的介面上面加上相應的訪問限制。
11. 實現自己的PermissionEvaluator
在介面方法上面增加了PreAuthorize
註解後還需要實現自己的PermissionEvaluator
,Spring Security
將在hasPermission()
方法中對當前登入使用者正在訪問的資源及其對資源進行的操作進行合法性校驗。
注意,這裡targetDomainObject
即是我們之前定義的resourceId
,而permission
即為privilege
,在校驗時要將其組合為和UserDetailsService
中儲存格式一致的格式,我們這裡是使用-
中劃線進行連線的。
import java.io.Serializable;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
@Configuration
public class MyPermissionEvaluator implements PermissionEvaluator {
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
boolean accessable = false;
if(authentication.getPrincipal().toString().compareToIgnoreCase("anonymousUser") != 0){
String privilege = targetDomainObject + "-" + permission;
for(GrantedAuthority authority : authentication.getAuthorities()){
if(privilege.equalsIgnoreCase(authority.getAuthority())){
accessable = true;
break;
}
}
return accessable;
}
return accessable;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
// TODO Auto-generated method stub
return false;
}
}
12. 註解支援
實現了PermissionEvaluator之後必須新增globalMethodSecurity
的註解,否則在介面上面加的許可權判斷不會生效。在SpringBootServletInitializer
的繼承類上面加上該註解啟用method security
。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MyApiApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder applicationBuilder) {
return applicationBuilder.sources(MyApiApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(MyApiApplication.class, args);
}
}
13. 訪問測試:403
由於我目前登入的使用者還沒有為其設定角色和訪問許可權,所以我沒有訪問list
介面的許可權,強行訪問的時候就出現瞭如下的403
的錯誤提示:
14. 前端頁面根據許可權實現個性化頁面
後端實現了介面級別的訪問限制之後並沒有結束。對於使用者可見的介面部分,不同角色的使用者登入系統時應該根據自己的角色而看到不同的介面。我們目前的經驗是,使用者登入成功後返回給前端該使用者的許可權列表,然後由前端對許可權進行判斷,如果沒有許可權則隱藏相應的按鈕或者功能模組。通過前後端這樣的結合,使用者將只能看到自己許可權允許範圍內的操作介面和資料,同時,即使某些使用者直接修改介面引數來獲取資料,在後端也會對其進行二次判斷,確保使用者自己看到自己的資料,只能進行許可權範圍內的操作!
相關文章
- 基於Spring Security和 JWT的許可權系統設計SpringJWT
- Spring security(五)-完美許可權管理系統(授權過程分析)Spring
- 基於RBAC實現許可權管理
- Spring Security 基於URL的許可權判斷Spring
- Spring Security實現統一登入與許可權控制Spring
- 基於 Spring Security 的前後端分離的許可權控制系統Spring後端
- 基於RBAC的許可權管理系統
- springBoot整合spring security實現許可權管理(單體應用版)--築基初期Spring Boot
- 最新版 Spring Security,該如何實現動態許可權管理?Spring
- Spring Security實現基於RBAC的許可權表示式動態訪問控制Spring
- Spring Security + jwt 許可權系統設計,包含SQLSpringJWTSQL
- Security 10:許可權管理
- 基於Spring框架應用的許可權控制系統的研究和實現Spring框架
- spring security許可權認證Spring
- 基於RBAC的許可權控制淺析(結合Spring Security)Spring
- SpringBoot整合Spring security JWT實現介面許可權認證Spring BootJWT
- 基於tp3.2.3 的許可權管理系統更新
- 基於SSM框架的JavaWeb通用許可權管理系統SSM框架JavaWeb
- 基於tp3.2.3開發的許可權管理系統,路由,微信,cdn,許可權路由
- 基於vue的簡單許可權管理實現總結Vue
- 管理系統之許可權的設計和實現
- SpringSecurity許可權管理系統實戰—九、資料許可權的配置SpringGse
- 基於Spring Security Oauth2的SSO單點登入+JWT許可權控制實踐SpringOAuthJWT
- Spring Security 許可權管理的投票器與表決機制Spring
- Vue管理系統前端系列六動態路由-許可權管理實現Vue前端路由
- 企業許可權管理系統
- Winner許可權管理系統3.0
- 後端基於方法的許可權控制--Spirng-Security後端
- springBoot整合spring security+JWT實現單點登入與許可權管理前後端分離--築基中期Spring BootJWT後端
- Django實戰1-許可權管理功能實現-04:系統入口Django
- 基於.NET 5實現的開源通用許可權管理平臺
- OA管理系統,有哪些許可權管理?
- shiro教程(1):基於url許可權管理
- 前端許可權控制系統的實現思路前端
- SpringSecurity許可權管理系統實戰—三、主要頁面及介面實現SpringGse
- SpringSecurity許可權管理系統實戰—二、日誌、介面文件等實現SpringGse
- 基於LDAP&&Role-based Authorization Strategy實現Jenkins團隊許可權管理LDAJenkins
- Confluence6對比系統管理員許可權和Confluence管理員許可權