SpringSecurity 6 快速入门

本篇文章只是本人的一些学习的见解以及经验,属实管中窥豹,如果有哪些地方写错了,请多多见谅,也希望可以指点一下本人

首先 SpringSecurity 项目必须包含三种关系,用户角色权限

用户是具体的用户,但是用户想要拿到权限必须通过角色这个 “中间商” 获取,这个图可能有些不太符合
在这里插入图片描述

新建一个 SpringBoot 项目

在这里插入图片描述
需要初始化你的项目一些参数
在这里插入图片描述
这里选择 SpringBoot 的版本,这里我选择的版本是 3.2.2

  1. 勾选 Spring Web
  2. 勾选 Spring Security
  3. 创建项目
    在这里插入图片描述

第一次创建项目很大可能需要下载依赖
在这里插入图片描述
下载完成后就可以开始编写 SpringSecurity 的项目了

SpringSecurity 配置类的编写

在 SpringSecurity 5 中的配置类是这样写的,只需要一个注解就可以了

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}

我们查看注解源代码,原来里面已经有 @EnableGlobalAuthentication @Configuration 注解了

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({ GlobalMethodSecuritySelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableGlobalMethodSecurity {
	......
}

以下才是 SpingSecurity 6 的配置类写法,SpringSecurity 6 没有了需要继承类这个做法,但是需要配置注解

  1. EnableWebSecurity,启动 SpringSecurity
  2. Configuration,以配置类的形式纳入到 Spring 容器管理
  3. EnableMethodSecurity,启动全局函数权限
@EnableWebSecurity
@Configuration
@EnableMethodSecurity
@EnableGlobalAuthentication
public class SpringSecurityConfig {
}

配置类中应该至少需要有两个 Bean 方法

  1. SecurityFilterChain
  2. PasswordEncoder
@EnableWebSecurity
@Configuration
@EnableMethodSecurity
public class SpringSecurityConfig {
	// SpringSecurityHandler 处理器
    @Resource
    private SpringSecurityHandler handler;
    // 修改输出流的输出格式
    @Resource
    private ResponseFilter responseFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http)
            throws Exception {
        http.authorizeHttpRequests(request -> {
                    request.anyRequest().authenticated();
                }).addFilterBefore(responseFilter, WebAsyncManagerIntegrationFilter.class) // 在 Web...过滤器之前添加过滤器
                .formLogin(request -> { // 添加登录处理器
                    request.successHandler(handler)
                            .failureHandler(handler);
                }).exceptionHandling(request -> { // 添加访问拒绝处理
                    request.accessDeniedHandler(handler);
                }).logout(request -> { // 添加退出处理器
                    request.logoutSuccessHandler(handler)
                            .addLogoutHandler(handler);
                });
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

SpringSecurity 处理器的编写

SpringSecurity 处理器是为了处理 登录成功登录失败访问拒绝退出成功退出操作 等操作的

前面我的代码中有一个 SpringSecurityHandler 处理器对象

/**
 * AuthenticationSuccessHandler 登录成功处理器
 * AuthenticationFailureHandler 登录失败处理器
 * AccessDeniedHandler 权限访问拒绝处理器
 * LogoutSuccessHandler 退出成功处理器
 * LogoutHandler 退出时执行处理器
 */
@Component
public class SpringSecurityHandler
        implements AuthenticationSuccessHandler, AuthenticationFailureHandler
        , AccessDeniedHandler, LogoutSuccessHandler, LogoutHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        PrintWriter writer = response.getWriter();
        writer.println("登录失败");
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        PrintWriter writer = response.getWriter();
        writer.println("登录成功");
    }

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        PrintWriter writer = response.getWriter();
        writer.println("访问被拒绝");
    }

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        PrintWriter writer = null;
        try {
            writer = response.getWriter();
            writer.println("正在退出账号");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        PrintWriter writer = response.getWriter();
        writer.println("退出账号成功");
    }
}

修改输出流格式的 ResponseFilter

该过滤器只是我为了方便我自己的项目才写的,各位可不写,因为我需要输出流的格式为 json 以及字符编码为 utf-8 才需要该过滤器的

@Component
public class ResponseFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        response.setHeader("Content-Type", "application/json;charset=utf-8");
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

依据数据库授权验证

导入数据库

/*
 Navicat Premium Data Transfer

 Source Server         : MySQL
 Source Server Type    : MySQL
 Source Server Version : 80033 (8.0.33)
 Source Host           : localhost:3306
 Source Schema         : security_01

 Target Server Type    : MySQL
 Target Server Version : 80033 (8.0.33)
 File Encoding         : 65001

 Date: 02/02/2024 10:54:48
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu`  (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '编号',
  `pid` int NULL DEFAULT NULL COMMENT '父级编号',
  `name` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '名称',
  `code` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '权限编码',
  `type` int NULL DEFAULT NULL COMMENT '0代表菜单1权限2 url',
  `delete_flag` tinyint NULL DEFAULT 0 COMMENT '0代表未删除,1 代表已删除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of sys_menu
-- ----------------------------
INSERT INTO `sys_menu` VALUES (1, 0, '学生管理', '/student/**', 0, 0);
INSERT INTO `sys_menu` VALUES (2, 1, '学生查询', 'student:query', 1, 0);
INSERT INTO `sys_menu` VALUES (3, 1, '学生添加', 'student:add', 1, 0);
INSERT INTO `sys_menu` VALUES (4, 1, '学生修改', 'student:update', 1, 0);
INSERT INTO `sys_menu` VALUES (5, 1, '学生删除', 'student:delete', 1, 0);
INSERT INTO `sys_menu` VALUES (6, 1, '导出学生信息', 'student:export', 1, 0);
INSERT INTO `sys_menu` VALUES (7, 0, '教师管理', '/teacher/**', 0, 0);
INSERT INTO `sys_menu` VALUES (9, 7, '教师查询', 'teacher:query', 1, 0);

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `rolename` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '角色名称,英文名称',
  `remark` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, 'ROLE_ADMIN', '管理员');
INSERT INTO `sys_role` VALUES (2, 'ROLE_TEACHER', '老师');
INSERT INTO `sys_role` VALUES (3, 'ROLE_STUDENT', '学生');

-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu`  (
  `rid` int NOT NULL COMMENT '角色表的编号',
  `mid` int NOT NULL COMMENT '菜单表的编号',
  PRIMARY KEY (`mid`, `rid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of sys_role_menu
-- ----------------------------
INSERT INTO `sys_role_menu` VALUES (1, 1);
INSERT INTO `sys_role_menu` VALUES (3, 1);
INSERT INTO `sys_role_menu` VALUES (2, 2);
INSERT INTO `sys_role_menu` VALUES (3, 2);
INSERT INTO `sys_role_menu` VALUES (1, 3);
INSERT INTO `sys_role_menu` VALUES (2, 3);
INSERT INTO `sys_role_menu` VALUES (1, 4);
INSERT INTO `sys_role_menu` VALUES (2, 4);
INSERT INTO `sys_role_menu` VALUES (1, 5);
INSERT INTO `sys_role_menu` VALUES (2, 5);
INSERT INTO `sys_role_menu` VALUES (3, 6);
INSERT INTO `sys_role_menu` VALUES (1, 9);
INSERT INTO `sys_role_menu` VALUES (2, 9);
INSERT INTO `sys_role_menu` VALUES (3, 9);
INSERT INTO `sys_role_menu` VALUES (1, 10);
INSERT INTO `sys_role_menu` VALUES (1, 17);

-- ----------------------------
-- Table structure for sys_role_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_user`;
CREATE TABLE `sys_role_user`  (
  `uid` int NOT NULL COMMENT '用户编号',
  `rid` int NOT NULL COMMENT '角色编号',
  PRIMARY KEY (`uid`, `rid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of sys_role_user
-- ----------------------------
INSERT INTO `sys_role_user` VALUES (1, 1);
INSERT INTO `sys_role_user` VALUES (2, 2);
INSERT INTO `sys_role_user` VALUES (3, 3);

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `user_id` int NOT NULL AUTO_INCREMENT COMMENT '编号',
  `username` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '登陆名',
  `password` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '密码',
  `sex` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '性别',
  `address` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '地址',
  `enabled` int NULL DEFAULT 1 COMMENT '是否启动账户0禁用 1启用',
  `account_no_expired` int NULL DEFAULT 1 COMMENT '账户是否没有过期0已过期 1 正常',
  `credentials_no_expired` int NULL DEFAULT 1 COMMENT '密码是否没有过期0已过期 1 正常',
  `account_no_locked` int NULL DEFAULT 1 COMMENT '账户是否没有锁定0已锁定 1 正常',
  PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'obama', '$2a$10$KyXAnVcsrLaHMWpd3e2xhe6JmzBi.3AgMhteFq8t8kjxmwL8olEDq', '男', '武汉', 1, 1, 1, 1);
INSERT INTO `sys_user` VALUES (2, 'thomas', '$2a$10$KyXAnVcsrLaHMWpd3e2xhe6JmzBi.3AgMhteFq8t8kjxmwL8olEDq', '男', '北京', 1, 1, 1, 1);
INSERT INTO `sys_user` VALUES (3, 'eric', '$2a$10$KyXAnVcsrLaHMWpd3e2xhe6JmzBi.3AgMhteFq8t8kjxmwL8olEDq', '男', '成都', 1, 1, 1, 1);

SET FOREIGN_KEY_CHECKS = 1;

用户的密码都是加密后的 123456

导入完成后,可以看到这个架构
在这里插入图片描述
sys_menu 是权限表
sys_role 是角色表
sys_role_menu 是角色权限表,中间表
sys_role_user 是用户角色表,中间表
sys_user 是角色表

SpringBoot + Mybatis 配置

  1. 添加 MySQL 依赖MyBatis集成SpringBoot
<dependency>
  <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>

配置 application.yaml 文件

server:
  port: 80 # 服务器端口号

mybatis:
  mapper-locations: classpath:/mapper/*.xml # MyBatis 的 Mapper 文件路径
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 日志实现类
	map-underscore-to-camel-case: true # 驼峰下划线转换
  type-aliases-package: com.cwy.bean # 类型别名的包名

spring:
  datasource:
    # MySQL 相关配置
    url: jdbc:mysql://127.0.0.1:3306/security_01
    username: root
    password: 123456c
    driver-class-name: com.mysql.cj.jdbc.Driver

查询用户

SysUser 实体类

表示用户的实体类需要实现 UserDetails,这是框架规范的用户的实体类

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class SysUser implements UserDetails, Serializable {
    private Integer userId;
    private String username;
    private String password;
    private String sex;
    private String address;
    private Integer enabled;
    private Integer accountNoExpired;
    private Integer credentialsNoExpired;
    private Integer accountNoLocked;
    private List<? extends  GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return getAccountNoExpired().equals(1);
    }

    @Override
    public boolean isAccountNonLocked() {
        return getAccountNoLocked().equals(1);
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return getCredentialsNoExpired().equals(1);
    }

    @Override
    public boolean isEnabled() {
        return getEnabled().equals(1);
    }
}

SysUserMapper 接口类

public interface SysUserMapper {
    SysUser getSysUserByUsername(@Param("username") String username);
}

SysUserMapper.xml 配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cwy.mapper.SysUserMapper">
    <select id="getSysUserByUsername" resultType="sysUser">
        SELECT *
        FROM sys_user
        WHERE username = #{username}
    </select>
</mapper>

SysUserServiceImpl

@Service
public class SysUserServiceImpl implements SysUserService {
    @Resource
    private SysUserMapper sysUserMapper;

    @Override
    public SysUser getSysUserByUsername(String username) {
        return sysUserMapper.querySysUserByUsername(username);
    }
}

查询用户权限

Menu实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Menu {
    private Integer id;
    private Integer pid;
    private String name;
    private String code;
    private Integer type;
    private Integer deleteFlag;
}

MenuMapper 接口类

public interface SysMenuMapper {
    String getCodeByUserId(@Param("userId") Integer userId);
}

MenuMapper.xml 文件

查询用户权限流程:通过用户ID 查询 用户角色ID,通过角色ID查询角色权限ID,通过角色权限ID查询权限的 Code

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cwy.mapper.SysMenuMapper">
    <select id="getCodeByUserId" resultType="string">
        SELECT code
        FROM sys_menu
        WHERE id
        in(SELECT mid
            FROM sys_role_menu
            WHERE rid
            in(SELECT rid
                FROM sys_role_user
                WHERE uid = #{userId}));
    </select>
</mapper>

SysMenuServiceImpl

@Service
public class SysMenuServiceImpl implements SysMenuService {
    @Resource
    private SysMenuMapper sysMenuMapper;

    @Override
    public List<SecurityGrantedAuthority> getCodesByUserId(Integer userId) {
        List<String> codes = sysMenuMapper.queryCodesByUserId(userId);
        return codes.stream().map(SecurityGrantedAuthority::new).toList();
    }
}

编写用户验证(依照框架规范)

既然配置都好了,那么就需要去用户授权验证了,本人直接就使用数据库的验证方式

目前我们已经写好了 SpringSecurity 的配置类 以及解密器 PasswordEncoder ,按照我们正常的思维逻辑,整个用户验证登录流程应该是

  1. 前端发送用户登录请求
  2. 后端验证,返回登录成功或登录失败

因为我们使用的是 SpringSecurity,这个框架的密码验证是它底层实现的,不需要程序员手动实现,我们只需要提供解密器(PasswordEncoder)给框架就行了,所以我们程序员需要做的是验证用户名是否正确即可,而因为整个验证过程的 Controller,Service,Mapper 我们作为零基础都是不知道底层是怎么实现的,我们该怎么办,怎么验证?

其实 Controller 是框架提供的,这个我们无法知道底层实现,但是 SpringSecurity 框架提供了一个 UserDeatilsService 接口给我们,就是用来提供给程序员编写用户验证过程的

SecurityGrantedAuthority

这个类是权限类,需要实现框架的GrantedAuthority 才是权限类

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SecurityGrantedAuthority implements GrantedAuthority, Serializable {
    private String authority;

    @Override
    public String getAuthority() {
        return authority;
    }
}

UserDetailService

@Service
public class UserDetailService implements UserDetailsService {
    @Resource
    private SysUserService sysUserService;
    @Resource
    private SysMenuService sysMenuService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = sysUserService.getSysUserByUsername(username);
        if (sysUser == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        List<SecurityGrantedAuthority> authorities = sysMenuService.getCodesByUserId(sysUser.getUserId());
        sysUser.setAuthorities(authorities);
        return sysUser;
    }
}

运行程序
在这里插入图片描述

运行程序后,在浏览器上输入 127.0.0.1:你自己程序的端口号
在这里插入图片描述
输入数据库的任意的用户名 + 123456,即可跳转到登录成功的处理器
在这里插入图片描述

设置方法权限

给 Controller 的方法设置权限

StudentController

@RestController
@RequestMapping("/student")
public class StudentController {
    @GetMapping("/query")
    @PreAuthorize("hasAnyAuthority('student:query')")
    public String queryStudent() {
        return "Query Student";
    }

    @GetMapping("/add")
    @PreAuthorize("hasAnyAuthority('student:add')")
    public String addStudent() {
        return "Add Student";
    }

    @GetMapping("/update")
    @PreAuthorize("hasAnyAuthority('student:update')")
    public String updateStudent() {
        return "Update Student";
    }

    @GetMapping("/delete")
    @PreAuthorize("hasAnyAuthority('student:delete')")
    public String deleteStudent() {
        return "Delete Student";
    }

    @GetMapping("/export")
    @PreAuthorize("hasAnyAuthority('student:export')")
    public String exportStudent() {
        return "Export Student";
    }
}

TeacherController

@RestController
@RequestMapping("/teacher")
public class TeacherController {
    @GetMapping("/query")
    @PreAuthorize("hasAnyAuthority('teacher:query')")
    public String queryStudent() {
        return "Query Student";
    }
}

当我使用 eric 账号访问 http://localhost/student/add 时,会返回访问拒绝,如果访问 http://localhost/student/query 时,会返回一个值

在这里插入图片描述

在这里插入图片描述
至此,这就是该项目的整个 Demo 目录
在这里插入图片描述

总结

以我个人的使用看法来觉得,整个 SpringSecurity 框架的验证流程大体是底层有一个 Controller,对程序员来说是透明的,它帮我们验证了密码的正确性,而我们只需要编写验证用户并且是框架规范的 Service 和 验证用户的 Mapper 就可以,本质上还是 MVC 架构,我本人其实也没有深入了解该 Controller 类是叫什么,这只是本人的一些看法,只有参考借鉴的意义,切勿把这些当作真理,另外还有框架规范提供的 UserDetails 接口,其实也是为了符合框架规范,只要学过 SpringBoot 以及 MVC 都应该很快了解,我本人也是一次偶然的机会接触到了若依框架,抱着学若依也要把若依实现的技术学一遍的心态就学了 SpringSecurity,虽然不能说是精通,但是至少能用 👀,哈哈哈

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐