1. 程式人生 > >基於Spring Security實現許可權管理系統

基於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":"刪除使用者"
    }
  }
]

在對許可權的定義中,關鍵是resourceIdprivilegeskey,後續將使用這兩者結合來對使用者的許可權進行判斷。我這裡使用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設定到Authenticationauthorities中以供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