百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

21《Spring Boot 入门教程》Spring Boot 安全管理

nanshan 2024-10-21 06:13 14 浏览 0 评论

1. 前言

安全管理是软件系统必不可少的的功能。根据经典的“墨菲定律”——凡是可能,总会发生。如果系统存在安全隐患,最终必然会出现问题。

本节就来演示下,如何使用 Spring Boot + Spring Security 开发前后端分离的权限管理功能。

2. Spring Security 用法简介

作为一个知名的安全管理框架, Spring Security 对安全管理功能的封装已经非常完整了。

我们在使用 Spring Security 时,只需要从配置文件或者数据库中,把用户、权限相关的信息取出来。然后通过配置类方法告诉 Spring Security , Spring Security 就能自动实现认证、授权等安全管理操作了。

  • 系统初始化时,告诉 Spring Security 访问路径所需要的对应权限。
  • 登录时,告诉 Spring Security 真实用户名和密码。
  • 登录成功时,告诉 Spring Security 当前用户具备的权限。
  • 用户访问接口时,Spring Security 已经知道用户具备的权限,也知道访问路径需要的对应权限,所以自动判断能否访问。

3. 数据库模块实现

3.1 定义表结构

需要 4 张表:

  • 用户表 user:保存用户名、密码,及用户拥有的角色 id 。
  • 角色表 role :保存角色 id 与角色名称。
  • 角色权限表 roleapi:保存角色拥有的权限信息。
  • 权限表 api:保存权限信息,在前后端分离的项目中,权限指的是控制器中的开放接口。

具体表结构如下,需要注意的是 api 表中的 path 字段表示接口的访问路径,另外所有的 id 都是自增主键。

数据库表结构

3.2 构造测试数据

执行如下 SQL 语句插入测试数据,下面的语句指定了 admin 用户可以访问 viewGoods 和 addGoods 接口,而 guest 用户只能访问 viewGoods 接口。

实例:

-- 用户
INSERT INTO `user` VALUES (1, 'admin', '$2a$10$D0OvhHj2Lh92rNey1EFmM.OqltxhH1vZA8mDpxz7jEofDEqLRplQy', 1);
INSERT INTO `user` VALUES (2, 'guest', '$2a$10$D0OvhHj2Lh92rNey1EFmM.OqltxhH1vZA8mDpxz7jEofDEqLRplQy', 2);
-- 角色
INSERT INTO `role` VALUES (1, '管理员');
INSERT INTO `role` VALUES (2, '游客');
-- 角色权限
INSERT INTO `roleapi` VALUES (1, 1, 1);
INSERT INTO `roleapi` VALUES (2, 1, 2);
INSERT INTO `roleapi` VALUES (3, 2, 1);
-- 权限
INSERT INTO `api` VALUES (1, 'viewGoods');
INSERT INTO `api` VALUES (2, 'addGoods');
代码块12345678910111213

Tips:用户密码是 123 加密后的值,大家了解即可,稍后再进行解释。

4. Spring Boot 后端实现

我们新建一个 Spring Boot 项目,并利用 Spring Security 实现安全管理功能。

4.1 使用 Spring Initializr 创建项目

Spring Boot 版本选择 2.2.5 ,Group 为 com.imooc , Artifact 为 spring-boot-security,生成项目后导入 Eclipse 开发环境。

4.2 引入项目依赖

我们引入 Web 项目依赖、安全管理依赖,由于要访问数据库所以引入 JDBC 和 MySQL 依赖。

实例:

        <!-- Web项目依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 安全管理依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- JDBC -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
代码块1234567891011121314151617181920

4.3 定义数据对象

安全管理,肯定需要从数据库中读取用户信息,以便判断用户登录名、密码是否正确,所以需要定义用户数据对象。

实例:

public class UserDo {
    private Long id;
    private String username;
    private String password;
    private String roleId;
    // 省略 get set
}
代码块1234567

4.4 开发数据访问类

系统初始化时,告诉 Spring Security 访问路径所需要的对应权限,所以我们开发从数据库获取权限列表的方法。

实例:

@Repository
public class ApiDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 获取所有api
     */
    public List<String> getApiPaths() {
        String sql = "select path from api";
        return jdbcTemplate.queryForList(sql, String.class);
    }
}
代码块12345678910111213

登录时,告诉 Spring Security 真实用户名和密码。 登录成功时,告诉 Spring Security 当前用户具备的权限。

所以我们开发根据用户名获取用户信息和根据用户名获取其可访问的 api 列表方法。

实例:

@Repository
public class UserDao {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    /**
     * 根据用户名获取用户信息
     */
    public List<UserDo> getUsersByUsername(String username) {
        String sql = "select id, username, password from user where username = ?";
        return jdbcTemplate.query(sql, new String[] { username }, new BeanPropertyRowMapper<>(UserDo.class));
    }
    /**
     * 根据用户名获取其可访问的api列表
     */
    public List<String> getApisByUsername(String username) {
        String sql = "select path from user left join roleapi on user.roleId=roleapi.roleId left join api on roleapi.apiId=api.id where username = ?";
        return jdbcTemplate.queryForList(sql, new String[] { username }, String.class);
    }
}

代码块1234567891011121314151617181920

4.5 开发服务类

开发 SecurityService 类,保存安全管理相关的业务方法。

实例:

@Service
public class SecurityService {
    @Autowired
    private UserDao userDao;
    @Autowired
    private ApiDao apiDao;

    public List<UserDo> getUserByUsername(String username) {
        return userDao.getUsersByUsername(username);
    }

    public List<String> getApisByUsername(String username) {
        return userDao.getApisByUsername(username);
    }

    public List<String> getApiPaths() {
        return apiDao.getApiPaths();
    }
}

4.6 开发控制器类

开发控制器类,其中 notLogin 方法是用户未登录时调用的方法,其他方法与权限表中的 api 一一对应。

实例:

@RestController
public class TestController {
    /**
     * 未登录时调用该方法
     */
    @RequestMapping("/notLogin")
    public ResultBo notLogin() {
        return new ResultBo(new Exception("未登录"));
    }

    /**
     * 查看商品
     */
    @RequestMapping("/viewGoods")
    public ResultBo viewGoods() {
        return new ResultBo<>("viewGoods is ok");
    }

    /**
     * 添加商品
     */
    @RequestMapping("/addGoods")
    public ResultBo addGoods() {
        return new ResultBo<>("addGoods is ok");
    }
}

由于是前后端分离的项目,为了便于前端统一处理,我们封装了返回数据业务逻辑对象 ResultBo 。

实例:

public class ResultBo<T> {
    /**
     * 错误码 0表示没有错误(异常) 其他数字代表具体错误码
     */
    private int code;
    /**
     * 后端返回消息
     */
    private String msg;
    /**
     * 后端返回的数据
     */
    private T data;
    /**
     * 无参数构造函数
     */
    public ResultBo() {
        this.code = 0;
        this.msg = "操作成功";
    }
    /**
     * 带数据data构造函数
     */
    public ResultBo(T data) {
        this();
        this.data = data;
    }
    /**
     * 存在异常的构造函数
     */
    public ResultBo(Exception ex) {
        this.code = 99999;// 其他未定义异常
        this.msg = ex.getMessage();
    }
}

4.7 开发 Spring Security 配置类

现在,我们就需要将用户、权限等信息通过配置类告知 Spring Security 了。

4.7.1 定义配置类

定义 Spring Security 配置类,通过注解 @EnableWebSecurity 开启安全管理功能。

实例:

@Configuration
@EnableWebSecurity // 开启安全管理
public class SecurityConfig {
    @Autowired
    private SecurityService securityService;
}

4.7.2 注册密码加密组件

Spring Security 提供了很多种密码加密组件,我们使用官方推荐的 BCryptPasswordEncoder ,直接注册为 Bean 即可。

我们之前在数据库中预定义的密码字符串即为 123 加密后的结果。 Spring Security 在验证密码时,会自动调用注册的加密组件,将用户输入的密码加密后再与数据库密码比对。

实例:

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    public static void main(String[] args) {
        //输出 $2a$10$kLQpA8S1z0KdgR3Cr6jJJ.R.QsIT7nrCdAfsF4Of84ZBX2lvgtbE.
        System.out.println(new BCryptPasswordEncoder().encode("123"));
    }

4.7.3 将用户密码及权限告知 Spring Security

通过注册 UserDetailsService 类型的组件,组件中设置用户密码及权限信息即可。

实例:

    @Bean
    public UserDetailsService userDetailsService() {
        return username -> {
            List<UserDo> users = securityService.getUserByUsername(username);
            if (users == null || users.size() == 0) {
                throw new UsernameNotFoundException("用户名错误");
            }
            String password = users.get(0).getPassword();
            List<String> apis = securityService.getApisByUsername(username);
            // 将用户名username、密码password、对应权限列表apis放入组件
            return User.withUsername(username).password(password).authorities(apis.toArray(new String[apis.size()]))
                    .build();
        };
    }

4.7.4 设置访问路径需要的权限信息

同样,我们通过注册组件,将访问路径需要的权限信息告知 Spring Security 。

实例:

    @Bean
    public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter() {
        return new WebSecurityConfigurerAdapter() {
            @Override
            public void configure(HttpSecurity httpSecurity) throws Exception {
                // 开启跨域支持
                httpSecurity.cors();
                // 从数据库中获取权限列表
                List<String> paths = securityService.getApiPaths();
                for (String path : paths) {
                    /* 对/xxx/**路径的访问,需要具备xxx权限
                    例如访问 /addGoods,需要具备addGoods权限 */
                    httpSecurity.authorizeRequests().antMatchers("/" + path + "/**").hasAuthority(path);
                }
                // 未登录时自动跳转/notLogin
                httpSecurity.authorizeRequests().and().formLogin().loginPage("/notLogin")
                        // 登录处理路径、用户名、密码
                        .loginProcessingUrl("/login").usernameParameter("username").passwordParameter("password")
                        .permitAll()
                        // 登录成功处理
                        .successHandler(new AuthenticationSuccessHandler() {
                            @Override
                            public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,
                                    HttpServletResponse httpServletResponse, Authentication authentication)
                                    throws IOException, ServletException {
                                httpServletResponse.setContentType("application/json;charset=utf-8");
                                ResultBo result = new ResultBo<>();
                                ObjectMapper mapper = new ObjectMapper();
                                PrintWriter out = httpServletResponse.getWriter();
                                out.write(mapper.writeValueAsString(result));
                                out.close();
                            }
                        })
                        // 登录失败处理
                        .failureHandler(new AuthenticationFailureHandler() {
                            @Override
                            public void onAuthenticationFailure(HttpServletRequest httpServletRequest,
                                    HttpServletResponse httpServletResponse, AuthenticationException e)
                                    throws IOException, ServletException {
                                httpServletResponse.setContentType("application/json;charset=utf-8");
                                ResultBo result = new ResultBo<>(new Exception("登录失败"));
                                ObjectMapper mapper = new ObjectMapper();
                                PrintWriter out = httpServletResponse.getWriter();
                                out.write(mapper.writeValueAsString(result));
                                out.flush();
                                out.close();
                            }
                        });
                // 禁用csrf(跨站请求伪造)
                httpSecurity.authorizeRequests().and().csrf().disable();
            }
        };
    }

按上面的设计,当用户发起访问时:

  • 未登录的访问会自动跳转到/notLogin 访问路径。
  • 通过 /login 访问路径可以发起登录请求,用户名和密码参数名分别为 username 和 password 。
  • 登录成功或失败会返回 ResultBo 序列化后的 JSON 字符串,包含登录成功或失败信息。
  • 访问 /xxx 形式的路径,需要具备 xxx 权限。用户所具备的权限已经通过上面的 UserDetailsService 组件告知 Spring Security 了。

5. 测试

启动项目后,我们使用 PostMan 进行验证测试。

5.1 未登录测试

在未登录时,直接访问控制器方法,会自动跳转 /notLogin 访问路径,返回未登录提示信息。

未登录测试

5.2 错误登录密码测试

调用登录接口,当密码不对时,返回登录失败提示信息。

错误登录密码测试

5.3 以 guest 用户登录

使用 guest 用户及正确命名登录,返回操作成功提示信息。

以 guest 用户登录

5.4 guest 用户访问授权接口

按照数据库中定义的规则, guest 用户可以访问 viewGoods 接口方法。

guest 用户访问授权接口

5.5 guest 用户访问未授权接口

按照数据库中定义的规则, guest 没有访问 addGoods 接口方法的权限。

guest 用户访问未授权接口

5.6 admin 用户登录及访问授权接口

按照数据库中定义的规则, admin 用户登录后可以访问 viewGoods 和 addGoods 两个接口方法。

admin 用户登录

admin 用户访问授权接口

admin 用户访问授权接口

6. 小结

Spring Boot 整合 Spring Security ,实际上大部分工作都在安全管理配置类上。

我们通过安全管理配置类,将用户、密码及其对应的权限信息放入容器,同时将访问路径所需要的权限信息放入容器, Spring Security 就会按照用户访问路径--判断所需权限--用户是否具备该权限--允许或拒绝访问这样的逻辑实施权限管理了。

相关推荐

0722-6.2.0-如何在RedHat7.2使用rpm安装CDH(无CM)

文档编写目的在前面的文档中,介绍了在有CM和无CM两种情况下使用rpm方式安装CDH5.10.0,本文档将介绍如何在无CM的情况下使用rpm方式安装CDH6.2.0,与之前安装C5进行对比。环境介绍:...

ARM64 平台基于 openEuler + iSula 环境部署 Kubernetes

为什么要在arm64平台上部署Kubernetes,而且还是鲲鹏920的架构。说来话长。。。此处省略5000字。介绍下系统信息;o架构:鲲鹏920(Kunpeng920)oOS:ope...

生产环境starrocks 3.1存算一体集群部署

集群规划FE:节点主要负责元数据管理、客户端连接管理、查询计划和查询调度。>3节点。BE:节点负责数据存储和SQL执行。>3节点。CN:无存储功能能的BE。环境准备CPU检查JDK...

在CentOS上添加swap虚拟内存并设置优先级

现如今很多云服务器都会自己配置好虚拟内存,当然也有很多没有配置虚拟内存的,虚拟内存可以让我们的低配服务器使用更多的内存,可以减少很多硬件成本,比如我们运行很多服务的时候,内存常常会满,当配置了虚拟内存...

国产深度(deepin)操作系统优化指南

1.升级内核随着deepin版本的更新,会自动升级系统内核,但是我们依旧可以通过命令行手动升级内核,以获取更好的性能和更多的硬件支持。具体操作:-添加PPAs使用以下命令添加PPAs:```...

postgresql-15.4 多节点主从(读写分离)

1、下载软件[root@TX-CN-PostgreSQL01-252software]#wgethttps://ftp.postgresql.org/pub/source/v15.4/postg...

Docker 容器 Java 服务内存与 GC 优化实施方案

一、设置Docker容器内存限制(生产环境建议)1.查看宿主机可用内存bashfree-h#示例输出(假设宿主机剩余16GB可用内存)#Mem:64G...

虚拟内存设置、解决linux内存不够问题

虚拟内存设置(解决linux内存不够情况)背景介绍  Memory指机器物理内存,读写速度低于CPU一个量级,但是高于磁盘不止一个量级。所以,程序和数据如果在内存的话,会有非常快的读写速度。但是,内存...

Elasticsearch性能调优(5):服务器配置选择

在选择elasticsearch服务器时,要尽可能地选择与当前业务量相匹配的服务器。如果服务器配置太低,则意味着需要更多的节点来满足需求,一个集群的节点太多时会增加集群管理的成本。如果服务器配置太高,...

Es如何落地

一、配置准备节点类型CPU内存硬盘网络机器数操作系统data节点16C64G2000G本地SSD所有es同一可用区3(ecs)Centos7master节点2C8G200G云SSD所有es同一可用区...

针对Linux内存管理知识学习总结

现在的服务器大部分都是运行在Linux上面的,所以,作为一个程序员有必要简单地了解一下系统是如何运行的。对于内存部分需要知道:地址映射内存管理的方式缺页异常先来看一些基本的知识,在进程看来,内存分为内...

MySQL进阶之性能优化

概述MySQL的性能优化,包括了服务器硬件优化、操作系统的优化、MySQL数据库配置优化、数据库表设计的优化、SQL语句优化等5个方面的优化。在进行优化之前,需要先掌握性能分析的思路和方法,找出问题,...

Linux Cgroups(Control Groups)原理

LinuxCgroups(ControlGroups)是内核提供的资源分配、限制和监控机制,通过层级化进程分组实现资源的精细化控制。以下从核心原理、操作示例和版本演进三方面详细分析:一、核心原理与...

linux 常用性能优化参数及理解

1.优化内核相关参数配置文件/etc/sysctl.conf配置方法直接将参数添加进文件每条一行.sysctl-a可以查看默认配置sysctl-p执行并检测是否有错误例如设置错了参数:[roo...

如何在 Linux 中使用 Sysctl 命令?

sysctl是一个用于配置和查询Linux内核参数的命令行工具。它通过与/proc/sys虚拟文件系统交互,允许用户在运行时动态修改内核参数。这些参数控制着系统的各种行为,包括网络设置、文件...

取消回复欢迎 发表评论: