SpringSecurity 6 快速入门
SpringSecurity 6 是 Spring 全家桶中属于安全权限的一类框架产品,同时它对于 Spring 框架的兼容性以及高定制性以及开源等优点,使得有些企业或个人开发者都会使用该框架
SpringSecurity 6 快速入门
本篇文章只是本人的一些学习的见解以及经验,属实管中窥豹,如果有哪些地方写错了,请多多见谅,也希望可以指点一下本人
首先 SpringSecurity 项目必须包含三种关系,用户
,角色
,权限
用户是具体的用户,但是用户想要拿到权限必须通过角色这个 “中间商” 获取,这个图可能有些不太符合
新建一个 SpringBoot 项目
需要初始化你的项目一些参数
这里选择 SpringBoot 的版本,这里我选择的版本是 3.2.2
- 勾选
Spring Web
- 勾选
Spring Security
- 创建项目
第一次创建项目很大可能需要下载依赖
下载完成后就可以开始编写 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 没有了需要继承类这个做法,但是需要配置注解
- EnableWebSecurity,启动 SpringSecurity
- Configuration,以配置类的形式纳入到 Spring 容器管理
- EnableMethodSecurity,启动全局函数权限
@EnableWebSecurity
@Configuration
@EnableMethodSecurity
@EnableGlobalAuthentication
public class SpringSecurityConfig {
}
配置类中应该至少需要有两个 Bean 方法
- SecurityFilterChain
- 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 配置
- 添加
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
,按照我们正常的思维逻辑,整个用户验证登录流程应该是
- 前端发送用户登录请求
- 后端验证,返回登录成功或登录失败
因为我们使用的是 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,虽然不能说是精通,但是至少能用 👀,哈哈哈
更多推荐
所有评论(0)