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

资源与操作权限集合类定义JsonPermissions

@Data
public class JsonPermissions {private List<SimplePermission> permissions;@Datapublic 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 {@Idprivate 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();@Idprivate String id;/*** 创建时间*/private Long createdTime = System.currentTimeMillis();/*** 用户登录名*/private String username;/*** 用户真实姓名*/private String realName;/*** 用户登录密码,用户的密码不应该暴露给客户端*/@JsonIgnoreprivate 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);}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic 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;@Autowiredprivate UserRepository userRepository;@Autowired private RoleRepository roleRepository;@PostConstructpublic 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 {@Autowiredprivate IUserService userService;@Autowiredprivate MongoTemplate mongoTemplate;@Overridepublic 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 {@Overrideprotected void configure(HttpSecurity httpSecurity) throws Exception {httpSecurity.authorizeRequests().antMatchers("/js/**","/css/**","/img/**","/login/**").permitAll().anyRequest().authenticated().and().formLogin().permitAll().cors();}@Autowiredpublic void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService());}
}

10. 后端接口根据权限实现访问限制

在需要进行访问限制的接口方法上面加上PreAuthorize注解,在该注解中我们可以使用多种校验方法,比较常见的有hasPermissonhasRole两个。而和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注解后还需要实现自己的PermissionEvaluatorSpring 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 {@Overridepublic 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;}@Overridepublic boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {// TODO Auto-generated method stubreturn 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 {@Overrideprotected 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实现权限管理系统相关推荐

  1. Spring security开发权限管理系统(一)

    从今天起,我将使用SpringBoot+SpringBoot+Mybatis+Vue从到一开发一个系统. 今天将说明Spring Security+SpringBoot+Mybatis的结合 引入PO ...

  2. java oauth sso 源码_基于Spring Security Oauth2的SSO单点登录+JWT权限控制实践

    概 述 在前文<基于Spring Security和 JWT的权限系统设计>之中已经讨论过基于 Spring Security和 JWT的权限系统用法和实践,本文则进一步实践一下基于 Sp ...

  3. 基于Spring Security 的Java SaaS应用的权限管理

    1. 概述 权限管理,一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源.资源包括访问的页面,访问的数据等,这在传统的应用系统中比较常见.本文介绍的则是基于Saas系统 ...

  4. 基于 Spring Security 搭建用户权限系统(二) - 自定义配置

    说明 本文的目的是如何基于 Spring Security 去扩展实现一个基本的用户权限模块, 内容会覆盖到 Spring Security 常用的配置. 文中涉及到的业务代码是不完善的, 甚至会存在 ...

  5. 【毕业设计】基于spring boot的图书管理系统 -java 计算机 软件工程

    文章目录 1 前言 2 系统简介 2.1 领域模型 2.2 技术栈 2.3 表结构设计 2.4 接口设计 2.4.1 接口定义 2.4.2 接口测试 2.5 权限设计 3 运行效果 3.1 系统登录 ...

  6. 基于 Spring Security OAuth2和 JWT 构建保护微服务系统

    我们希望自己的微服务能够在用户登录之后才可以访问,而单独给每个微服务单独做用户权限模块就显得很弱了,从复用角度来说是需要重构的,从功能角度来说,也是欠缺的.尤其是前后端完全分离之后,我们的用户信息不一 ...

  7. 基于 Spring Security 的开源统一角色访问控制系统 URACS

    URACS Java语言开发的统一角色访问控制系统(Unified Role Access Control System),基于Spring Security 3实现的权限控制系统 程序框架版本说明: ...

  8. 基于Spring Security与JWT实现单点登录

    基于RBAC的权限管理 RBAC(Role-Based Access Control):基于角色的访问控制 当前项目中,RBAC具体的表现为: 管理员表:ams_admin 角色表:ams_role ...

  9. 基于Spring Security的AJAX请求需要登录的解决方案

    基于Spring Security的AJAX请求需要登录的解决方案 参考文章: (1)基于Spring Security的AJAX请求需要登录的解决方案 (2)https://www.cnblogs. ...

最新文章

  1. 第十六周程序阅读(4)
  2. 用python随机画多个圆_Python Pygame随机绘制不重叠的圆圈
  3. 微信小游戏复活了传统PC游戏
  4. 【算法微解读】浅谈线段树
  5. 手机当电脑麦克风 linux,WO Mic让手机成为电脑的无线麦克风
  6. linux shell: 搜索字符串,剔除包含特定字符的行
  7. VMware 大中华区原厂认证讲师(大陆地区)2019-8-19 更新
  8. mybatis xml sql
  9. [译] 如何学习 CSS
  10. lookup函数和vlookup_VLOOKUP和LOOKUP两个函数PK,哪个才是你心目中的查找之王?
  11. Qt5Widgetsd.lib(Qt5Widgetsd.dll) : fatalerror LNK1112: 模块计算机类型“x64”与目标计算机类型“X86”冲突
  12. python生成器的惰性计算
  13. 怎么获取大量新鲜可用的迅雷白金会员账号!?
  14. 信度和效度经典例子_信度与效度公式的纠正
  15. 手机无线可以上网 电脑却没网络连接服务器,电脑连接手机热点不能上网怎么办 值得一看...
  16. Java那些不为人知的技巧
  17. Python实战之函数的一些奇技淫巧
  18. SpringBoot实现发送QQ邮箱验证码
  19. lisp不是函授型语言_LISP语言
  20. Vue项目中使用Tinymce

热门文章

  1. java计算机毕业设计学校食堂库存在线管理源码+系统+数据库+lw文档
  2. ad如何绘制拼版_Altium Designer PCB拼板流程权威指导
  3. truncate命令简介
  4. 【codeforces103A】Testing Pants for Sadness
  5. 递归回溯算法一文读懂详解图文
  6. 斑马888-TT不认新纸
  7. 明日之后良心造房技巧分享:更有十一庄别墅房闪亮登场
  8. 5个你应该知道的建站平台
  9. 忘记Excel密码?3种解决办法!
  10. python属于什么类型的语言-python语言属于()