草庐IT

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

木子教程 2023-03-28 原文

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 都是自增主键。


5eca178b095bfe7a07810163.jpg

数据库表结构

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 访问路径,返回未登录提示信息。

5eca3f6f095e3ba407420354.jpg

未登录测试

5.2 错误登录密码测试

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

5eca3f7909f4b45207440414.jpg

错误登录密码测试

5.3 以 guest 用户登录

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

5eca3f8209458abe07420411.jpg

以 guest 用户登录

5.4 guest 用户访问授权接口

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


5eca3f8b09829ae807440352.jpg

guest 用户访问授权接口

5.5 guest 用户访问未授权接口

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

5eca3f910930576c07430382.jpg

guest 用户访问未授权接口

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

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

5eca3f970900259307410418.jpg

admin 用户登录

5eca3f9d09e265ae07400353.jpg

admin 用户访问授权接口

5eca3fa4092dfbc707440354.jpg

admin 用户访问授权接口

6. 小结

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

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

有关21《Spring Boot 入门教程》Spring Boot 安全管理的更多相关文章

  1. ruby - i18n Assets 管理/翻译 UI - 2

    我正在使用i18n从头开始​​构建一个多语言网络应用程序,虽然我自己可以处理一大堆yml文件,但我说的语言(非常)有限,最终我想寻求外部帮助帮助。我想知道这里是否有人在使用UI插件/gem(与django上的django-rosetta不同)来处理多个翻译器,其中一些翻译器不愿意或无法处理存储库中的100多个文件,处理语言数据。谢谢&问候,安德拉斯(如果您已经在ruby​​onrails-talk上遇到了这个问题,我们深表歉意) 最佳答案 有一个rails3branchofthetolkgem在github上。您可以通过在Gemfi

  2. ruby - 如何使用 Ruby aws/s3 Gem 生成安全 URL 以从 s3 下载文件 - 2

    我正在编写一个小脚本来定位aws存储桶中的特定文件,并创建一个临时验证的url以发送给同事。(理想情况下,这将创建类似于在控制台上右键单击存储桶中的文件并复制链接地址的结果)。我研究过回形针,它似乎不符合这个标准,但我可能只是不知道它的全部功能。我尝试了以下方法:defauthenticated_url(file_name,bucket)AWS::S3::S3Object.url_for(file_name,bucket,:secure=>true,:expires=>20*60)end产生这种类型的结果:...-1.amazonaws.com/file_path/file.zip.A

  3. ruby-on-rails - 获取 inf-ruby 以使用 ruby​​ 版本管理器 (rvm) - 2

    我安装了ruby​​版本管理器,并将RVM安装的ruby​​实现设置为默认值,这样'哪个ruby'显示'~/.rvm/ruby-1.8.6-p383/bin/ruby'但是当我在emacs中打开inf-ruby缓冲区时,它使用安装在/usr/bin中的ruby​​。有没有办法让emacs像shell一样尊重ruby​​的路径?谢谢! 最佳答案 我创建了一个emacs扩展来将rvm集成到emacs中。如果您有兴趣,可以在这里获取:http://github.com/senny/rvm.el

  4. ruby - 如何安全地删除文件? - 2

    在Ruby中是否有Gem或安全删除文件的方法?我想避免系统上可能不存在的外部程序。“安全删除”指的是覆盖文件内容。 最佳答案 如果您使用的是*nix,一个很好的方法是使用exec/open3/open4调用shred:`shred-fxuz#{filename}`http://www.gnu.org/s/coreutils/manual/html_node/shred-invocation.html检查这个类似的帖子:Writingafileshredderinpythonorruby?

  5. ruby-on-rails - 事件管理员日期过滤器日期格式自定义 - 2

    是否有简单的方法来更改默认ISO格式(yyyy-mm-dd)的ActiveAdmin日期过滤器显示格式? 最佳答案 您可以像这样为日期选择器提供额外的选项,而不是覆盖js:=f.input:my_date,as::datepicker,datepicker_options:{dateFormat:"mm/dd/yy"} 关于ruby-on-rails-事件管理员日期过滤器日期格式自定义,我们在StackOverflow上找到一个类似的问题: https://s

  6. postman接口测试工具-基础使用教程 - 2

    1.postman介绍Postman一款非常流行的API调试工具。其实,开发人员用的更多。因为测试人员做接口测试会有更多选择,例如Jmeter、soapUI等。不过,对于开发过程中去调试接口,Postman确实足够的简单方便,而且功能强大。2.下载安装官网地址:https://www.postman.com/下载完成后双击安装吧,安装过程极其简单,无需任何操作3.使用教程这里以百度为例,工具使用简单,填写URL地址即可发送请求,在下方查看响应结果和响应状态码常用方法都有支持请求方法:getpostputdeleteGet、Post、Put与Delete的作用get:请求方法一般是用于数据查询,

  7. LC滤波器设计学习笔记(一)滤波电路入门 - 2

    目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称

  8. 在VMware16虚拟机安装Ubuntu详细教程 - 2

    在VMware16.2.4安装Ubuntu一、安装VMware1.打开VMwareWorkstationPro官网,点击即可进入。2.进入后向下滑动找到Workstation16ProforWindows,点击立即下载。3.下载完成,文件大小615MB,如下图:4.鼠标右击,以管理员身份运行。5.点击下一步6.勾选条款,点击下一步7.先勾选,再点击下一步8.去掉勾选,点击下一步9.点击下一步10.点击安装11.点击许可证12.在百度上搜索VM16许可证,复制填入,然后点击输入即可,亲测有效。13.点击完成14.重启系统,点击是15.双击VMwareWorkstationPro图标,进入虚拟机主

  9. 微信小程序开发入门与实战(Behaviors使用) - 2

    @作者:SYFStrive @博客首页:HomePage📜:微信小程序📌:个人社区(欢迎大佬们加入)👉:社区链接🔗📌:觉得文章不错可以点点关注👉:专栏连接🔗💃:感谢支持,学累了可以先看小段由小胖给大家带来的街舞👉微信小程序(🔥)目录自定义组件-behaviors    1、什么是behaviors    2、behaviors的工作方式    3、创建behavior    4、导入并使用behavior    5、behavior中所有可用的节点    6、同名字段的覆盖和组合规则总结最后自定义组件-behaviors    1、什么是behaviorsbehaviors是小程序中,用于实现

  10. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

随机推荐