一、Spring Security
①:什么是Spring Security
Spring Security是一个能够为基于Spring的企业应用系统提供声明式(注解)的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
可以一句话来概括:SpringSecurity 是一个安全框架。
②:官方网址
https://spring.io/projects/spring-security/
中文网址:https://springdoc.cn/spring-security/servlet/authorization/authorize-http-requests.html
二、认证入门
①:安全入门项目
1.新建一个项目01_springsecurity
2.添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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-web</artifactId>
</dependency>
<!--spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
3.创建3个controller
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("/query")
public String queryInfo(){
return "当前登录用户: admin";
}
}
@RestController
@RequestMapping("/student")
public class StudentController {
@GetMapping("/query")
public String queryInfo(){
return "当前登录用户: student";
}
}
@RestController
@RequestMapping("/teacher")
public class TeacherController {
@GetMapping("/query")
public String queryInfo(){
return "当前登录用户: teacher";
}
}
4.启动程序
1.访问 http://localhost:8080/admin/query 会自动跳转到登录页面
框架生成的用户
- 用户名: user
- 密码: 在启动项目时,生成的临时密码(98d61d12-378d-45ab-97b4-04241651ccd2)
2.登录成功
3.登出http://localhost:8080/logout
②:自定义用户名和密码
application.yaml中配置如下
spring:
security:
user:
name: root
password: root
使用刚刚自定义的用户名和密码登录
③:多用户管理(基于内存)
1.创建配置类
/**
* 用户详情服务接口
*
* @author: Coke
* @DateTime: 2023/11/07/20:48
**/
@Configuration
public class MySecurityUserConfig {
@Bean
public UserDetailsService userDetailsService () {
// 创建两个用户
UserDetails zhangsan = User.builder().username("张三").password("123456").roles("student").build();
UserDetails lisi = User.builder().username("李四").password("123456").roles("teacher").build();
// 创建一个内存用户详细信息管理器
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 将两个用户放到 内存用户详细信息管理器中
manager.createUser(zhangsan);
manager.createUser(lisi);
return manager;
}
/*
* 自定义用户 必须配置密码编辑器
* NoOpPasswordEncoder(没有加密)
* @DateTime: 2023/11/7 21:11
*
* @return PasswordEncoder
* @author: Coke
*/
@Bean
public PasswordEncoder passwordEncoder () {
return NoOpPasswordEncoder.getInstance();
}
}
2.启动程序(使用配置类中的用户登录)
3.退出登录使用前面配置文件中的用户登录
- 结论:可以删除配置文件中的用户了
④:密码处理(加密)
- 前面的用户并没有真正加密
使用BCryptPasswordEncoder进行加密 (重新运行程序测试)
@Configuration
public class MySecurityUserConfig {
@Bean
public UserDetailsService userDetailsService () {
// 创建两个用户
UserDetails zhangsan = User.builder().username("张三").password(passwordEncoder().encode("123456")).roles("student").build();
UserDetails lisi = User.builder().username("李四").password(passwordEncoder().encode("123456")).roles("teacher").build();
// 创建一个内存用户详细信息管理器
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 将两个用户放到 内存用户详细信息管理器中
manager.createUser(zhangsan);
manager.createUser(lisi);
return manager;
}
/*
* 自定义用户 必须配置密码编辑器
* NoOpPasswordEncoder(没有加密)
* @DateTime: 2023/11/7 21:11
*
* @return PasswordEncoder
* @author: Coke
*/
@Bean
public PasswordEncoder passwordEncoder () {
return new BCryptPasswordEncoder();
}
}
⑤:获取当前登录用户信息
1.创建CurrentLoginUserController
@RestController
@RequestMapping("/getLogin")
public class CurrentLoginUserController {
@GetMapping("/user1")
public Authentication getUser1(Authentication authentication){
return authentication;
}
@GetMapping("/user2")
public Principal getUser2(Principal principal){
return principal;
}
@GetMapping("/user3")
public Principal getUser3(){
// 通过安全上下文持有器获取安全上下文,再获取认证信息
return SecurityContextHolder.getContext().getAuthentication();
}
}
2.启动程序 并登录
3.访问刚刚写的第一个controller的第一个接口
⑥:配置用户权限
@Bean
public UserDetailsService userDetailsService(){
// 创建两个用户
UserDetails user1 = User.builder()
.username("张三")
.password(passwordEncoder().encode("123456"))
.roles("student")
.authorities("student_delete", "student_add")
.build();
UserDetails user2 = User.builder()
.username("李四")
.password(passwordEncoder().encode("123456"))
.authorities("teacher_delete", "teacher_add")
.roles("teacher")
.build();
// 创建一个内存用户详细信息管理器
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 将两个用户放到 内存用户详细信息管理器中
manager.createUser(user1);
manager.createUser(user2);
return manager;
}
1.登录
张三
这个用户 然后查询用户信息
2.在登录
李四
这个账户然后查询用户信息
- 结论:
权限和角色按照配置的顺序生效 后者覆盖前者
- 问题:虽然有了权限 但是并没对访问url生效
⑦:针对url进行授权
上面讲的实现了认证功能,但是受保护的资源是默认的,歌认所有认证(登录)用户均可以访问所有资源瓤不能根据实际情况进行角色管理,要实现授权功能,需重写WebSecurityConfigureAdapter中的一个configure方法
1.新建
WebSecurityConfig
类,重写configure(HttpSecurity http)
方法
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // 授权请求
// 匹配路径url的写法有三种
// .regexMatchers("/student/**")
// .antMatchers("/student/**")
.mvcMatchers("/student/**") // 推荐这种, 匹配这个url
// 判断 权限的五种
// .hasAuthority( ) // 是否有单个权限
// .access()
// .hasRole() // 是否有单个角色
// .hasAnyRole() // 是否有任意角色
.hasAnyAuthority("student_add") // 拥有这个权限的用户可以访问 /student/** 这个url
.mvcMatchers("/teacher/**") // 匹配url
.hasAuthority("ROLE_teacher") // 拥有这个权限的用户可以访问 /teacher/** 这个url
.anyRequest() // 任何请求
.authenticated(); // 都需要登录,注意:没有配置mV℃匹配器的只要登录成功就可以访问
http.formLogin().permitAll(); // 允许表单登录permit:允许
}
}
⑧:针对方法进行授权
1.拷贝
01_spring_security
改名为02_spring_security
删除:
AdminController和StudentController
新增:
TeacherService、TeacherServiceImpl
1. TeacherController |
---|
@RestController
@RequestMapping("/teacher")
public class TeacherController {
@Autowired
private TeacherService teacherService;
@GetMapping("/add")
public String add(){
return teacherService.add();
}
@GetMapping("/delete")
public String delete(){
return teacherService.delete();
}
@GetMapping("/update")
public String update(){
return teacherService.update();
}
@GetMapping("/query")
public String query(){
return teacherService.query();
}
}
2.TeacherService |
---|
public interface TeacherService {
// 添加教师
String add();
// 删除教师
String delete();
// 修改教师
String update();
// 查询教师
String query();
}
3.TeacherServiceImpl |
---|
@Service
@Slf4j
public class TeacherServiceImpl implements TeacherService {
@Override
public String add() {
log.info("添加教师成功!");
return "添加教师成功!";
}
@Override
public String delete() {
log.info("删除教师成功!");
return "删除教师成功!";
}
@Override
public String update() {
log.info("修改教师成功!");
return "修改教师成功!";
}
@Override
public String query() {
log.info("查询教师成功!");
return "查询教师成功!";
}
}
2.修改MySecurityUserConfig配置类
@Configuration
public class MySecurityUserConfig {
@Bean
public UserDetailsService userDetailsService(){
// 创建两个用户
UserDetails user1 = User.builder()
.username("张三")
.password(passwordEncoder().encode("123456"))
.roles("student") // 角色的前面加上 ROLE_ 就成了权限
.build();
UserDetails user2 = User.builder()
.username("李四")
.password(passwordEncoder().encode("123456"))
.authorities("teacher:query") // 配置了教师的查询权限
.build();
UserDetails user3 = User.builder()
.username("admin")
.password(passwordEncoder().encode("123456"))
.authorities("teacher:add","teacher:delete","teacher:update","teacher:query") // 配置了教师的增删改查权限
.build();
// 创建一个内存用户详细信息管理器
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 将两个用户放到 内存用户详细信息管理器中
manager.createUser(user1);
manager.createUser(user2);
manager.createUser(user3);
return manager;
}
/*
* 自定义用户 必须配置密码编辑器
* NoOpPasswordEncoder(没有加密)
* @DateTime: 2023/11/7 21:11
*
* @return PasswordEncoder
* @author: Coke
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
3.修改WebSecurityConfig配置类
- 加上启用全局方法安全注解
@EnableGlobalMethodSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启全局方法安全,启用预授权注解和后授权注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated(); // 任何请求 都需要登录,注意:没有配置mV℃匹配器的只要登录成功就可以访问
http.formLogin().permitAll(); // 放开登录页面
}
}
4.修改
TeacherServiceImpl
类 在方法上加上预授权
注解
@Service
@Slf4j
public class TeacherServiceImpl implements TeacherService {
@Override
@PreAuthorize("hasAuthority('teacher:add')") // 预授权 // hasAuthority('teacher:add') 一个权限可访问
public String add() {
log.info("添加教师成功!");
return "添加教师成功!";
}
@Override
@PreAuthorize("hasAnyAuthority('teacher:delete')") // hasAnyAuthority('teacher:add','teacher:delete'...) 可以有多权限
public String delete() {
log.info("删除教师成功!");
return "删除教师成功!";
}
@Override
@PreAuthorize("hasAnyAuthority('teacher:update')")
public String update() {
log.info("修改教师成功!");
return "修改教师成功!";
}
@Override
@PreAuthorize("hasAnyAuthority('teacher:query')")
public String query() {
log.info("查询教师成功!");
return "查询教师成功!";
}
}
5.测试
1.登录用户 张三 没有teacher的任何权限 |
---|
2.登录用户 李四 有teacher的查询权限 |
---|
3.登录用户 admin 有teacher的所有权限 |
---|
⑨:处理返回结果及自定义用户信息
1. 处理返回结果
1.拷贝
02_spring_security
改名为03_spring_security
新增:
WebSecurityConfig、Response
1.WebSecurityConfig |
---|
package com.it.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.it.vo.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @Author: CaoYouGen
* @DateTime: 2023/11/08/13:21
* @注释: TODO
**/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启全局方法安全,启用预授权注解和后授权注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ObjectMapper objectMapper; // 可以进行序列号(json)和反序列化
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated(); // 任何请求 都需要登录,注意:没有配置mV℃匹配器的只要登录成功就可以访问
http.formLogin()
// 配置登录成功的处理器
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String responseJson = objectMapper.writeValueAsString(Response.ok("登录成功!"));
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.println(responseJson);
writer.flush();
}
})
// 配置登录失败的处理器
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
String responseJson = objectMapper.writeValueAsString(Response.error(1,"登录失败!"));
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.println(responseJson);
writer.flush();
}
}).permitAll();
// 配置退出成功处理器
http.logout().logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String responseJson = objectMapper.writeValueAsString(Response.ok("退出成功!"));
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.println(responseJson);
writer.flush();
}
});
// 配置访问拒绝处理器
http.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
String responseJson = objectMapper.writeValueAsString(Response.error(1,"您没有权限访问该资源!"));
response.setCharacterEncoding("utf-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.println(responseJson);
writer.flush();
}
});
}
}
2.Response |
---|
package com.it.vo;
import lombok.Data;
@Data
public class Response<T> {
/**
* 结果
*
* @mock true
*/
private boolean success;
/**
* 状态码
*
* @mock 200
*/
private int code;
/**
* 消息提示
*
* @mock 操作成功
*/
private String msg;
/**
* 结果体
*
* @mock null
*/
private T data;
public Response() {
}
public Response(int code, Object status) {
super();
this.code = code;
this.msg = status.toString();
if (code == 1) {
this.success = true;
} else {
this.success = false;
}
}
public Response(int code, String status, T result) {
super();
this.code = code;
this.msg = status;
this.data = result;
if (code == 1) {
this.success = true;
} else {
this.success = false;
}
}
public static Response<?> ok() {
return new Response<>(1, "success");
}
public static <T> Response<T> ok(T t) {
return new Response<T>(1, "success", t);
}
public static Response<?> error(String status) {
return new Response<>(500, status);
}
public static Response<?> error(int code, String status) {
return new Response<>(code, status);
}
}
2.自定义用户信息
1.
删除:
MySecurityUserConfig
新增:
SecurityUser、UserServiceImpl
1.新增SecurityUser |
---|
public class SecurityUser implements UserDetails {
// 用户的权限
@Override
public Collection<? extends GrantedAuthority> getAuthorities () {
return null;
}
@Override
public String getPassword () {
return new BCryptPasswordEncoder().encode("123456");
}
@Override
public String getUsername () {
return "张三";
}
// 判断帐号是否已经过期
@Override
public boolean isAccountNonExpired () {
return true;
}
// 判断帐号是否已被锁定
@Override
public boolean isAccountNonLocked () {
return true;
}
// 判断用户凭证是否已经过期
@Override
public boolean isCredentialsNonExpired () {
return true;
}
// 是否有效
@Override
public boolean isEnabled () {
return true;
}
}
2.新增UserServiceImpl |
---|
@Service
public class UserServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException {
// 判断用户名是否为空
if (!StringUtils.hasText(username)) {
throw new UsernameNotFoundException("用户名不存在!");
}
// 判断用户是否正确
if (!username.equals("张三")) {
throw new UsernameNotFoundException("用户名不正确!");
}
// 执行到这里 说明用户名不为空 并且 用户名正确
return new SecurityUser();
}
}
3.修改WebSecurityConfig (在该类中新增以下方法) |
---|
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
2.测试
- 使用 李四 这个用户登录
- 使用 张三 这个用户登录
- 登录成功后 访问teacher/query 的资源
- 查看张三的权限
三、基于数据库认证
①:创建数据库和表
1.创建数据库(
security_study
)
2.创建表
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 = 3 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 = 3 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;
- 执行完以上sql后 一共创建了5张表
②:创建新的模块
1. 创建、引入依赖、添加配置
1.创建新的模块(
04_spring_security
)
2.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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-web</artifactId>
</dependency>
<!--spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<!--支持使用 JDBC 访问数据库 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<!-- mybatis-plus-generator -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.16</version>
</dependency>
3.配置文件(数据库等配置信息)
server:
port: 8099
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://您的ip地址:3306/security_study?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 您的密码
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true # 数据库中下划线 映射到实体类中大小写
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 输出sql语句
2.创建实体类与DAO
1.创建实体类
SysUser
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SysUser implements 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;
}
2.创建Mapper
SysUseMapper
@Mapper
public interface SysUserMapper {
/*
* 根据用户名获取用户信息
* @DateTime: 2023/11/8 21:40
*
* @param userName:
* @return SysUser
* @author: Coke
*/
SysUser getUserName (@Param ("userName") String userName);
}
3.创建
SysUserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.it.mapper.SysUserMapper">
<!-- 这里定义SQL语句 -->
<select id="getUserName" resultType="com.it.entity.SysUser">
select user_id,
username,
password,
sex,
address,
enabled,
account_no_expired,
credentials_no_expired,
account_no_locked
from sys_user
where username = #{userName}
</select>
</mapper>
3.实现Service层
1.创建
SysUserService
public interface SysUserService {
/*
* 根据用户名获取用户信息
* @DateTime: 2023/11/8 21:40
*
* @param userName:
* @return SysUser
* @author: Coke
*/
SysUser getUserName (String userName);
}
2.创建实现了
SysUserServiceImpl
@Service
@Slf4j
public class SysUserServiceImpl implements SysUserService {
@Autowired
private SysUserMapper sysUserMapper;
@Override
public SysUser getUserName (String userName) {
return sysUserMapper.getUserName(userName);
}
}
4. 创建安全用户与实现
1.创建
SecurityUser
public class SecurityUser implements UserDetails {
private final SysUser sysUser;
public SecurityUser (SysUser sysUser) {
this.sysUser = sysUser;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities () {
// todo 还没有配置权限
return null;
}
@Override
public String getPassword () {
return this.sysUser.getPassword();
}
@Override
public String getUsername () {
return this.sysUser.getUsername();
}
@Override
public boolean isAccountNonExpired () {
return this.sysUser.getAccountNoExpired().equals(1);
}
@Override
public boolean isAccountNonLocked () {
return this.sysUser.getAccountNoLocked().equals(1);
}
@Override
public boolean isCredentialsNonExpired () {
return this.sysUser.getCredentialsNoExpired().equals(1);
}
@Override
public boolean isEnabled () {
return this.sysUser.getEnabled().equals(1);
}
}
2.创建
SecurityUserDetailsServiceImpl
@Service
public class SecurityUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getUserName(username);
// 判断对象是否为空
if (ObjectUtils.isEmpty(sysUser)){
throw new UsernameNotFoundException("该用户不存在!");
}
// 判断用户是否可用
if (!sysUser.getAccountNoExpired().equals(1)) {
throw new UsernameNotFoundException("该账户已过期!");
}
return new SecurityUser(sysUser);
}
}
5. 创建安全配置类与Controller层
1.创建安全配置类``
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 对密码进行编码
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure (HttpSecurity http) throws Exception {
// 任何请求 都需要登录,注意:没有配置mV℃匹配器的只要登录成功就可以访问
http.authorizeRequests().anyRequest().authenticated();
http.formLogin().permitAll();
}
}
2.新建三个Controller
StudentController
TeacherController
CurrentLoginUserController
- StudentController
@RestController
@Slf4j
@RequestMapping ("/student")
public class StudentController {
@GetMapping ("/query")
public String queryInfo(){
return "query student";
}
@GetMapping("/add")
public String addInfo(){
return "add student!";
}
@GetMapping("/update")
public String updateInfo(){
return "update student";
}
@GetMapping("/delete")
public String deleteInfo(){
return "delete student!";
}
@GetMapping("/export")
public String exportInfo(){
return "export student!";
}
}
- TeacherController
@RestController
@Slf4j
@RequestMapping ("/teacher")
public class TeacherController {
@GetMapping ("/query")
@PreAuthorize ("hasAuthority('teacher:query')")
public String queryInfo(){
return "I am a teacher!";
}
}
- CurrentLoginUserController
@RestController
@RequestMapping("/getLogin")
public class CurrentLoginUserController {
@GetMapping("/user1")
public Authentication getUser1(Authentication authentication) {
return authentication;
}
@GetMapping("/user2")
public Principal getUser2(Principal principal){
return principal;
}
@GetMapping("/user3")
public Principal getUser3(){
// 通过安全上下文持有器获取安全上下文,再获取认证信息
return SecurityContextHolder.getContext().getAuthentication();
}
}
6.启动测试
1.使用thomas和obama分别登录测试,发现student/query等能访问,teacher/query 不能访问
2.原因:发现用户没有权限,但是/teacher/query 需要访问权限
四、基于数据库的授权
①:创建实体类、Mapper、service
1.创建菜单(权限)实体类
SysMenu
@Data
public class SysMenu implements Serializable {
private Integer id;
private Integer pid;
private Integer type;
private String name;
private String code;
}
2.创建mapper
SysMenuMapper
public interface SysMenuMapper {
List<String> queryPermissionsByUserId(@Param("userId") Integer userId);
}
3.创建
SysMenuMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.it.mapper.SysMenuMapper">
<!-- 这里定义SQL语句 -->
<select id="queryPermissionsByUserId" resultType="string">
select distinct sm.code
from sys_role_user sru
join sys_role_menu srm on sru.rid = srm.rid
join sys_menu sm on srm.mid = sm.id where sru.rid = #{userId};
</select>
</mapper>
4.创建service
SysMenuService
public interface SysMenuService {
List<String> queryPermissionsByUserId(Integer userId);
}
5.创建
SysMenuServiceImpl
@Service
@Slf4j
public class SysMenuServiceImpl implements SysMenuService {
@Resource
private SysMenuMapper sysMenuMapper;
@Override
public List<String> queryPermissionsByUserId(Integer userId) {
return sysMenuMapper.queryPermissionsByUserId(userId);
}
}
②: 配置权限
1.修改
SecurityUser
实体类
- 加入一个属性
private List<SimpleGrantedAuthority> simpleGrantedAuthorities;
2.修改方法
getAuthorities
@Override
public Collection<? extends GrantedAuthority> getAuthorities () {
// todo 配置权限
return simpleGrantedAuthorities;
}
3.添加一个set方法
public void setSimpleGrantedAuthorities(List<SimpleGrantedAuthority> simpleGrantedAuthorities) {
this.simpleGrantedAuthorities = simpleGrantedAuthorities;
}
4.修改
SecurityUserDetailsServiceImpl
- 增加设置权限的步骤
@Service
public class SecurityUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysMenuService sysMenuService;
@Override
public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getUserName(username);
// 判断对象是否为空
if (ObjectUtils.isEmpty(sysUser)){
throw new UsernameNotFoundException("该用户不存在!");
}
// 判断用户是否可用
if (!sysUser.getAccountNoExpired().equals(1)) {
throw new UsernameNotFoundException("该账户已过期!");
}
// 通过用户id获取用户的所有权限
List<String> permissions = sysMenuService.queryPermissionsByUserId(sysUser.getUserId());
// 使用Stream流将 List<String> permissions 转换为 List<SimpleGrantedAuthority> grantedAuthorities
List<SimpleGrantedAuthority> grantedAuthorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
// 创建一个用户权限对象
SecurityUser securityUser = new SecurityUser(sysUser);
// 将权限设置到用户对象中
securityUser.setSimpleGrantedAuthorities(grantedAuthorities);
// 返回
return securityUser;
}
}
③:启动程序测试
- 使用thomas和obama分别登录测试,发现已经有权限功能了
④:手动擦除密码防止传到前端
- 擦除密码前
1. 修改
SecurityUser
类中的getPassword
方法
@Override
public String getPassword () {
String myPassword = this.sysUser.getPassword();
// 手动擦除密码防止传到前端
this.sysUser.setPassword(null);
return myPassword;
}
- 重启服务再次登录查看