基於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. 角色相關的操作
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