--- dir: order: 2 --- # 之前SpringSecurity **官方文档:**[https://docs.spring.io/spring-security/reference/index.html](https://docs.spring.io/spring-security/reference/index.html) **功能:** + 身份认证(authentication) + 授权(authorization) + 防御常见攻击(protection against common attacks) **身份认证:** + 身份认证是验证`谁正在访问系统资源`,判断用户是否为合法用户。认证用户的常见方式是要求用户输入用户名和密码。 **授权:** + 用户进行身份认证后,系统会控制`谁能访问哪些资源`,这个过程叫做授权。用户无法访问没有权限的资源。 ## 身份认证 **官方代码示例:**[GitHub - spring-projects/spring-security-samples](https://github.com/spring-projects/spring-security-samples/tree/main) ### 项目的基本搭建 项目搭建完成后,默认端口是8080,直接访问`localhost:8080`即可。 **浏览器自动跳转到登录页面:**[http://localhost:8080/login](http://localhost:8080/login) 项目结构 ![](./images/1739884981997-1f74d00b-4ba8-4716-927c-91b26aee6d16.png) #### 基本包 这里用到了数据库但是在项目刚开始启动时,是没有配置数据库的,这时候启动肯定会报错,所以我们现在启动类上排出连接数据库的类。 ```xml 4.0.0 org.springframework.boot spring-boot-starter-parent 3.2.0 com.atguigu security-demo 1.0-SNAPSHOT jar security-demo https://maven.apache.org UTF-8 21 21 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security org.thymeleaf.extras thymeleaf-extras-springsecurity6 org.springframework.boot spring-boot-starter-test org.springframework.security spring-security-test org.springframework.boot spring-boot-starter-thymeleaf mysql mysql-connector-java 8.0.30 com.baomidou mybatis-plus-boot-starter 3.5.4.1 org.mybatis mybatis-spring org.mybatis mybatis-spring 3.0.3 org.projectlombok lombok ``` #### 创建启动类 在启动类上排出数据库的类。 ```java import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) public class SecurityDemoApplication { public static void main(String[] args) { SpringApplication.run(SecurityDemoApplication.class, args); } } ``` #### 创建IndexController ```java import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class IndexController { @GetMapping("/") public String index() { return "index"; } } ``` #### 创建index.html 这里使用的动态标签`th:href="@{/logout}"`目的是可以自动检测路径变化,比如我们在配置文件中配置了全局路径参数。这时候动态标签会自动匹配。 这时需要访问根路径`localhost:8080/demo` ```yaml server: servlet: context-path: /demo ``` HTML模板 ```html Hello Security!

Hello Security

Log Out ``` 比如点击下面按钮会自动匹配路径并退出。 ![](./images/1739884994974-19080b87-0be5-4d8b-a2b4-e57e996da90b.png) ## 自定义Security配置 SecurityProperties修改默认用户和密码。 ```yaml spring: security: user: name: user password: admin123 ``` ### 使用配置类 ```java @Configuration @EnableWebSecurity//Spring项目总需要添加此注解,SpringBoot项目中不需要 public class WebSecurityConfig { @Bean public UserDetailsService userDetailsService() { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser( //此行设置断点可以查看创建的user对象 User .withDefaultPasswordEncoder() .username("huan") //自定义用户名 .password("password") //自定义密码 .roles("USER") //自定义角色 .build() ); return manager; } ``` ## 基于数据库的数据源 ### 环境准备 创建三个数据库表并插入测试数据 ```sql -- 创建数据库 CREATE DATABASE `security-demo`; USE `security-demo`; -- 创建用户表 CREATE TABLE `user`( `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, `username` VARCHAR(50) DEFAULT NULL , `password` VARCHAR(500) DEFAULT NULL, `enabled` BOOLEAN NOT NULL ); -- 唯一索引 CREATE UNIQUE INDEX `user_username_uindex` ON `user`(`username`); -- 插入用户数据(密码是 "abc" ) INSERT INTO `user` (`username`, `password`, `enabled`) VALUES ('admin', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE), ('Helen', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE), ('Tom', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE); ``` #### 引入依赖 这个在之前也引用过了。 ```xml mysql mysql-connector-java 8.0.30 com.baomidou mybatis-plus-boot-starter 3.5.4.1 org.mybatis mybatis-spring org.mybatis mybatis-spring 3.0.3 org.projectlombok lombok ``` #### 配置文件中 ```yaml datasource: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://${bunny.datasource.host}:${bunny.datasource.port}/${bunny.datasource.sqlData}?serverTimezone=GMT%2B8&useSSL=false&characterEncoding=utf-8&allowPublicKeyRetrieval=true username: ${bunny.datasource.username} password: ${bunny.datasource.password} ``` #### 启动类 启动类记得删除排出数据库源的类。 ```java @SpringBootApplication public class SecurityDemoApplication { public static void main(String[] args) { SpringApplication.run(SecurityDemoApplication.class, args); } } ``` ### 配置数据库查询 创建`DBUserDetailsManager`类实现UserDetailsManager, UserDetailsPasswordService方法。 查询数据库字段进行匹配,如果查询到并且密码正确就可以放行。记得在方法上加上`@Configuration`注解。 ```java @Configuration public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService { @Resource private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); User user = userMapper.selectOne(queryWrapper); if (user == null) { throw new UsernameNotFoundException(username); } else { Collection authorities = new ArrayList<>(); return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), user.getEnabled(), true, // 用户账号是否过期 true, // 用户凭证是否过期 true, // 用户是否未被锁定 authorities); // 权限列表 } } @Override public UserDetails updatePassword(UserDetails user, String newPassword) { return null; } @Override public void createUser(UserDetails user) { } @Override public void updateUser(UserDetails user) { } @Override public void deleteUser(String username) { } @Override public void changePassword(String oldPassword, String newPassword) { } @Override public boolean userExists(String username) { return false; } } ``` ## 配置Security默认配置 + `formLogin(withDefaults())`提供默认的登录模拟页面。 + 如果开启了`formLogin(withDefaults())`可以`httpBasic(withDefaults())`屏蔽。 ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; import static org.springframework.security.config.Customizer.withDefaults; @Configuration @EnableWebSecurity// Spring项目总需要添加此注解,SpringBoot项目中不需要 public class WebSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { // authorizeRequests():开启授权保护 // anyRequest():对所有请求开启授权保护 // authenticated():已认证请求会自动被授权 httpSecurity .authorizeRequests(authorize -> authorize .anyRequest() .authenticated()) // .formLogin(withDefaults())// 表单授权方式 .httpBasic(withDefaults());// 基本授权方式 return httpSecurity.build(); } } ``` ## 添加用户 在Controller层添加接口,写入添加用户的方法,之后在实现接口中添加这个方法接口。 ```java @RestController @RequestMapping("/user") @Tag(name = "用户请求接口") public class UserController { @Autowired private UserService userService; @Operation(summary = "添加用户") @PostMapping("/add") public void addUser(@RequestBody User user) { userService.addUserDetails(user); } } ``` 在实现接口中实现这个方法。 1. 注入`DBUserDetailsManager`之后创建这个user。 ```java import com.atguigu.security.config.DBUserDetailsManager; import com.atguigu.security.entity.User; import com.atguigu.security.mapper.UserMapper; import com.atguigu.security.service.UserService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @Service public class UserServiceImpl extends ServiceImpl implements UserService { @Autowired private DBUserDetailsManager manager; /** * 添加用户 * * @param user 用户信息 */ @Override public void addUserDetails(User user) { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); // 这里的User使用的是springSecurity中的User UserDetails userDetails = org.springframework.security.core.userdetails.User .withDefaultPasswordEncoder() .username(user.getUsername()) .password(user.getPassword()) .build(); manager.createUser(userDetails); } } ``` 在方法中添加以下内容`createUser`这个方法 1. 导入了一些需要使用的类,包括`User`实体类和`UserMapper`接口。 2. 声明了一个`UserMapper`类型的字段`userMapper`。 3. 实现了`createUser`方法,该方法用于创建用户。在这个示例中,该方法将传入的`UserDetails`对象中的用户名和密码插入到数据库中。 4. 实现了`userExists`方法,该方法用于检查用户是否存在。在这个示例中,该方法始终返回`false`。 ```java import com.atguigu.security.entity.User; import com.atguigu.security.mapper.UserMapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import jakarta.annotation.Resource; import org.springframework.context.annotation.Configuration; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsPasswordService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.provisioning.UserDetailsManager; import java.util.ArrayList; import java.util.Collection; @Configuration public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService { @Resource private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); User user = userMapper.selectOne(queryWrapper); if (user == null) { throw new UsernameNotFoundException(username); } else { Collection authorities = new ArrayList<>(); return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), user.getEnabled(), true, // 用户账号是否过期 true, // 用户凭证是否过期 true, // 用户是否未被锁定 authorities); // 权限列表 } } @Override public void createUser(UserDetails userDetails) { // 插入数据库方法 User user = new User(); user.setUsername(userDetails.getUsername()); user.setPassword(userDetails.getPassword()); user.setEnabled(true); userMapper.insert(user); } @Override public boolean userExists(String username) { return false; } // 略... } ``` > 为了方便调试,springSecurity默认开启了csrf(这要求请求参数中必须有一个隐藏的**_csrf**字段),为了测试这里就暂时关闭。 > > 在filterChain方法中添加如下代码,关闭csrf攻击防御 > > 代码示例: > ```java //关闭csrf攻击防御 http.csrf((csrf) -> { csrf.disable(); }); ``` ```java @Configuration @EnableWebSecurity// Spring项目总需要添加此注解,SpringBoot项目中不需要 public class WebSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { // authorizeRequests():开启授权保护 // anyRequest():对所有请求开启授权保护 // authenticated():已认证请求会自动被授权 httpSecurity .authorizeRequests(authorize -> authorize .anyRequest() .authenticated()) .formLogin(withDefaults())// 表单授权方式 .httpBasic(withDefaults());// 基本授权方式 // 关闭csrf攻击 httpSecurity.csrf(AbstractHttpConfigurer::disable); return httpSecurity.build(); } } ``` ## 密码加密测试 创建测试方法: ```java @Slf4j public class PasswordTest { @Test void testPassword() throws Exception { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String password = encoder.encode("password"); log.info("Password===>:{}", password); // 密码校验 Assert.isTrue(encoder.matches("password", password), "密码不一致"); } } ``` ## 自定义登录页面 ### 创建登录页 ![](./images/1739885018440-f282a25f-0223-4c1f-9ae9-fa67b4f6dfa1.png) #### 第一步:创建Controller ```java import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class IndexController { @GetMapping("/login") public String login() { return "login"; } } ``` #### 第二步:创建HTML ```html 登录

登录

错误的用户名和密码.
``` #### 第三步:配置Security ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.SecurityFilterChain; import static org.springframework.security.config.Customizer.withDefaults; @Configuration @EnableWebSecurity// Spring项目总需要添加此注解,SpringBoot项目中不需要 public class WebSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { // authorizeRequests():开启授权保护 // anyRequest():对所有请求开启授权保护 // authenticated():已认证请求会自动被授权 httpSecurity .authorizeRequests(authorize -> authorize .anyRequest() .authenticated()) .formLogin(withDefaults())// 表单授权方式 .httpBasic(withDefaults());// 基本授权方式 // 关闭csrf攻击 httpSecurity.csrf(AbstractHttpConfigurer::disable); // 配置SecurityFilterChain-自定义登录页 httpSecurity.formLogin(form -> { form.loginPage("/login").permitAll()// 登录页面无需授权即可访问 .usernameParameter("username")// 自定义表单用户名参数,默认是username .passwordParameter("password")// 自定义表单密码参数,默认是password .failureUrl("/login?error"); // 登录失败的返回地址 }); return httpSecurity.build(); } } ``` ![](./images/1739885055597-43b7107f-a1b2-462a-996b-fd3b4d66ccb0.png) ### 登录页的细节 在`WebSecurityConfig`中自定义前端传递值,默认传递用户名和密码为`username`和`password`,在下面示例中可以修改为自定义的用户名和密码参数。 ```java // 配置SecurityFilterChain-自定义登录页 httpSecurity.formLogin(form -> { form.loginPage("/login").permitAll()// 登录页面无需授权即可访问 .usernameParameter("username")// 自定义表单用户名参数,默认是username .passwordParameter("password")// 自定义表单密码参数,默认是password .failureUrl("/login?error"); // 登录失败的返回地址 }); ``` #### 自定义前端传递参数 ```java // 配置SecurityFilterChain-自定义登录页 httpSecurity.formLogin(form -> { form.loginPage("自定义登录页").permitAll()// 登录页面无需授权即可访问 .usernameParameter("自定义用户名")// 自定义表单用户名参数,默认是username .passwordParameter("自定义密码")// 自定义表单密码参数,默认是password .failureUrl("自定义错误页"); // 登录失败的返回地址 }); ``` ## 认证响应结果 移入fastjson ```xml com.alibaba.fastjson2 fastjson2 2.0.37 ``` ### 认证成功返回 ```java import com.alibaba.fastjson2.JSON; import com.atguigu.security.result.Result; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import java.io.IOException; public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // 获取用户身份信息 Object principal = authentication.getPrincipal(); // 获取用户凭证信息 // Object credentials = authentication.getCredentials(); // 获取用户权限信息 // Collection authorities = authentication.getAuthorities(); Result result = Result.success(principal); // 返回 response.setContentType("application/json;charset=UTF-8"); response.getWriter().println(JSON.toJSON(result)); } } ``` ![](./images/1739885100104-b414e402-cf91-4713-8789-9570a53721c5.png) 在`WebSecurityConfig`类中添加以下`.successHandler(new MyAuthenticationSuccessHandler());`表示成功的返回结果,自定义结果。 如果想看Result的类翻到最下面附录。 ```java // 配置SecurityFilterChain-自定义登录页 httpSecurity.formLogin(form -> { form.loginPage("/login").permitAll()// 登录页面无需授权即可访问 .usernameParameter("username")// 自定义表单用户名参数,默认是username .passwordParameter("password")// 自定义表单密码参数,默认是password .failureUrl("/login?error") // 登录失败的返回地址 .successHandler(new MyAuthenticationSuccessHandler());// 认证成功时的处理 }); ``` ### 认证失败返回 和成功的返回相似,只需要修改两个地方即可。 认证失败的类 ```java import com.alibaba.fastjson2.JSON; import com.atguigu.security.result.Result; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import java.io.IOException; public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { // 错误消息 String localizedMessage = exception.getLocalizedMessage(); Result result = Result.error(localizedMessage); // 转成JSON Object json = JSON.toJSON(result); // 返回响应 response.setContentType("application/json;charset=UTF-8"); response.getWriter().println(json); } } ``` 在WebSecurityConfig类中添加`failureHandler`即可`.failureHandler(new MyAuthenticationFailureHandler());` ```java // 配置SecurityFilterChain-自定义登录页 httpSecurity.formLogin(form -> { form.loginPage("/login").permitAll()// 登录页面无需授权即可访问 .usernameParameter("username")// 自定义表单用户名参数,默认是username .passwordParameter("password")// 自定义表单密码参数,默认是password .failureUrl("/login?error") // 登录失败的返回地址 .successHandler(new MyAuthenticationSuccessHandler())// 认证成功时的处理 .failureHandler(new MyAuthenticationFailureHandler());// 认证失败的处理 }); ``` ![](./images/1739885069146-d3cae399-23b5-447a-a025-fea765c1d8be.png) ### 注销响应 和前面成功和失败过程相似,只需要在`from`中再添加即可。 ```java import com.alibaba.fastjson2.JSON; import com.atguigu.security.result.Result; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import java.io.IOException; public class MyLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // 创建结果对象 Result result = Result.success(); // 转为JSON Object json = JSON.toJSON(result); // 返回响应 response.setContentType("application/json;charset=UTF-8"); response.getWriter().println(json); } } ``` 在`WebSecurityConfig`类中添加以下。 ```java // 注销响应 httpSecurity.logout(logout -> { logout.logoutSuccessHandler(new MyLogoutSuccessHandler());// 注销成功时的处理 }); ``` ![](./images/1739885117054-f7b847c2-7948-485f-bbc4-b53da35671dc.png) 之后访问:[http://localhost/demo](http://localhost/demo) ### 请求未认证接口 ```java import com.alibaba.fastjson2.JSON; import com.atguigu.security.result.Result; import com.atguigu.security.result.ResultCodeEnum; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import java.io.IOException; /** * 请求未认证接口 */ @Slf4j public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { // 错误信息 String localizedMessage = authException.getLocalizedMessage(); log.error("请求未认证接口:{}", localizedMessage); // 创建结果对象 Result result = Result.error(ResultCodeEnum.FAIL_NEED_LOGIN); // 返回响应 response.setContentType("application/json;charset=UTF-8"); response.getWriter().println(JSON.toJSON(result)); } } ``` 在`WebSecurityConfig`类中添加以下。 ```java // 请求未认证接口 httpSecurity.exceptionHandling(exception -> { // 请求未认证的接口 exception.authenticationEntryPoint(new MyAuthenticationEntryPoint()); }); ``` 访问:[http://localhost/demo/](http://localhost/demo/) ![](./images/1739885133278-ef71eb9a-48e4-423a-96b5-ccfe68b07a8f.png) 登录后 ![](./images/1739885137700-67858269-4190-4596-a4be-7227280220f1.png) ![](./images/1739885142319-b86e0961-1eef-4ef9-b42a-f7c9d9335ff7.png) ### 跨域访问 在`WebSecurityConfig`类中添加以下。 ```java // 跨域访问权限 httpSecurity.cors(withDefaults()); ``` ## 获取用户认证信息 USerVo类见附录。 ```java @RestController public class IndexController { @GetMapping("/") public Result index() { SecurityContext context = SecurityContextHolder.getContext(); Authentication authentication = context.getAuthentication(); // 用户名 String username = authentication.getName(); // 身份 Object principal = authentication.getPrincipal(); // 凭证(脱敏) Object credentials = authentication.getCredentials(); // 权限 Collection authorities = authentication.getAuthorities(); // 整理返回参数 UserVo userVo = UserVo.builder().authorities(authorities).credentials(credentials) .principal(principal).username(username).build(); return Result.success(userVo); } } ``` ## 会话并发处理 后登录的账号会使先登录的账号失效。实现方式和之前的差不多也是实现一个接口。 ```java import com.alibaba.fastjson2.JSON; import com.atguigu.security.result.Result; import com.atguigu.security.result.ResultCodeEnum; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.web.session.SessionInformationExpiredEvent; import org.springframework.security.web.session.SessionInformationExpiredStrategy; import java.io.IOException; public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy { @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException { // 创建结果对象 Result result = Result.error(ResultCodeEnum.LOGGED_IN_FROM_ANOTHER_DEVICE); // 转为JSON Object json = JSON.toJSON(result); // 返回响应 HttpServletResponse response = event.getResponse(); response.setContentType("application/json;charset=UTF-8"); response.getWriter().println(json); } } ``` 在`WebSecurityConfig`类中添加以下。 ```java // 后登录的账号会使先登录的账号失效 httpSecurity.sessionManagement(session -> { session.maximumSessions(1) .expiredSessionStrategy(new MySessionInformationExpiredStrategy()); }); ``` ## 授权访问 授权管理的实现在SpringSecurity中非常灵活,可以帮助应用程序实现以下两种常见的授权需求: + 用户-权限-资源:例如张三的权限是添加用户、查看用户列表,李四的权限是查看用户列表 + 用户-角色-权限-资源:例如 张三是角色是管理员、李四的角色是普通用户,管理员能做所有操作,普通用户只能查看信息 ### 基于request的授权 **需求:** + 具有USER_LIST权限的用户可以访问/user/list接口 + 具有USER_ADD权限的用户可以访问/user/add接口 在`WebSecurityConfig`添加下面内容。 ```java // authorizeRequests():开启授权保护 httpSecurity.authorizeRequests(authorize -> { // 具有USER_LIST权限的用户可以访问/user/list,访问路径是Controller中的路径 authorize.requestMatchers("/user/list").hasAuthority("USER_LIST") // 具有USER_ADD权限的用户可以访问/user/add .requestMatchers("/user/add").hasAuthority("USER_ADD") // 对所有请求开启授权保护 .anyRequest() // 已认证请求会自动被授权 .authenticated(); }); ``` 之后在`DBUserDetailsManager`中授予访问权限。 在其中注释`authorities.add(() -> "USER_LIST")`会发现没有权限访问。 ```java // 授予访问权限 authorities.add(() -> "USER_LIST"); authorities.add(() -> "USER_ADD"); ``` 完整代码 ```java import com.atguigu.security.mapper.UserMapper; import com.atguigu.security.model.entity.User; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import jakarta.annotation.Resource; import org.springframework.context.annotation.Configuration; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsPasswordService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.provisioning.UserDetailsManager; import java.util.ArrayList; import java.util.Collection; @Configuration public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService { @Resource private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); User user = userMapper.selectOne(queryWrapper); if (user == null) { throw new UsernameNotFoundException(username); } else { Collection authorities = new ArrayList<>(); // 授予访问权限 authorities.add(() -> "USER_LIST"); authorities.add(() -> "USER_ADD"); return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), user.getEnabled(), true, // 用户账号是否过期 true, // 用户凭证是否过期 true, // 用户是否未被锁定 authorities); // 权限列表 } } } ``` 请求未授权的接口 创建未授权访问类,返回对象。 ```java import com.alibaba.fastjson2.JSON; import com.atguigu.security.result.Result; import com.atguigu.security.result.ResultCodeEnum; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import java.io.IOException; public class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { Result result = Result.error(ResultCodeEnum.FAIL_NO_ACCESS_DENIED); Object json = JSON.toJSON(result); // 返回响应 response.setContentType("application/json;charset=UTF-8"); response.getWriter().println(json); } } ``` 在`WebSecurityConfig`添加下面内容。 ```java // 请求未授权的接口 httpSecurity.exceptionHandling(exception -> { exception.authenticationEntryPoint(new MyAuthenticationEntryPoint()); // 没有权限访问 exception.accessDeniedHandler(new MyAccessDeniedHandler()); }); ``` **更多的例子:**[Method Security :: Spring Security](https://docs.spring.io/spring-security/reference/servlet/authorization/method-security.html) #### 用户-角色-资源 **需求:**角色为ADMIN的用户才可以访问/user/**路径下的资源 ##### 配置角色 在`filterChain`方法中配置;将之前的手动添加的路径注释。 `hasRole("ADMIN")`可以自定义内容。 ```java // authorizeRequests():开启授权保护 httpSecurity.authorizeRequests(authorize -> { // 具有USER_LIST权限的用户可以访问/user/list,访问路径是Controller中的路径 authorize // 具有管理员角色的用户可以访问/user/** .requestMatchers("/user/**").hasRole("ADMIN") // .requestMatchers("/user/list").hasAuthority("USER_LIST") // 具有USER_ADD权限的用户可以访问/user/add // .requestMatchers("/user/add").hasAuthority("USER_ADD") // 对所有请求开启授权保护 .anyRequest() // 已认证请求会自动被授权 .authenticated(); }); ``` 配置DBUserDetailsManager,改变`roles("USER")` + 如果为`roles("ADMIN")`表示都可以访问 + 如果为`roles("USER")`表示只是普通用户。 ```java @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); User user = userMapper.selectOne(queryWrapper); if (user == null) { throw new UsernameNotFoundException(username); } else { // Collection authorities = new ArrayList<>(); // // 授予访问权限 // authorities.add(() -> "USER_LIST"); // authorities.add(() -> "USER_ADD"); // return new org.springframework.security.core.userdetails.User( // user.getUsername(), // user.getPassword(), // user.getEnabled(), // true, // 用户账号是否过期 // true, // 用户凭证是否过期 // true, // 用户是否未被锁定 // authorities); // 权限列表 return org.springframework.security.core.userdetails.User .withUsername(user.getUsername()) .password(user.getPassword()) .disabled(!user.getEnabled()) .credentialsExpired(false) .accountLocked(false) // SecurityFilterChain中配置hasRole("ADMIN")来判断的 .roles("USER") .build(); } } ``` 为`roles("ADMIN")`时全部可以访问。 ![](./images/1739885159429-1ba4d90f-d7c2-4d48-9da1-2c11a9252233.png) 为普通用户时。 ![](./images/1739885163072-25c78a6a-56dc-46cf-9fc7-cbc36b2fe155.png) ### RBAC设置权限表方式 RBAC(Role-Based Access Control,基于角色的访问控制)是一种常用的数据库设计方案,它将用户的权限分配和管理与角色相关联。以下是一个基本的RBAC数据库设计方案的示例: 1. 用户表(User table):包含用户的基本信息,例如用户名、密码和其他身份验证信息。 | 列名 | 数据类型 | 描述 | | --- | --- | --- | | user_id | int | 用户ID | | username | varchar | 用户名 | | password | varchar | 密码 | | email | varchar | 电子邮件地址 | | ... | ... | ... | 2. 角色表(Role table):存储所有可能的角色及其描述。 | 列名 | 数据类型 | 描述 | | --- | --- | --- | | role_id | int | 角色ID | | role_name | varchar | 角色名称 | | description | varchar | 角色描述 | | ... | ... | ... | 3. 权限表(Permission table):定义系统中所有可能的权限。 | 列名 | 数据类型 | 描述 | | --- | --- | --- | | permission_id | int | 权限ID | | permission_name | varchar | 权限名称 | | description | varchar | 权限描述 | | ... | ... | ... | 4. 用户角色关联表(User-Role table):将用户与角色关联起来。 | 列名 | 数据类型 | 描述 | | --- | --- | --- | | user_role_id | int | 用户角色关联ID | | user_id | int | 用户ID | | role_id | int | 角色ID | | ... | ... | ... | 5. 角色权限关联表(Role-Permission table):将角色与权限关联起来。 | 列名 | 数据类型 | 描述 | | --- | --- | --- | | role_permission_id | int | 角色权限关联ID | | role_id | int | 角色ID | | permission_id | int | 权限ID | | ... | ... | ... | 在这个设计方案中,用户可以被分配一个或多个角色,而每个角色又可以具有一个或多个权限。通过对用户角色关联和角色权限关联表进行操作,可以实现灵活的权限管理和访问控制。 当用户尝试访问系统资源时,系统可以根据用户的角色和权限决定是否允许访问。这样的设计方案使得权限管理更加简单和可维护,因为只需调整角色和权限的分配即可,而不需要针对每个用户进行单独的设置。 ```sql CREATE TABLE admin_power ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '权限ID', power_name VARCHAR(50) NOT NULL COMMENT '权限名称', power_code VARCHAR(255) NOT NULL COMMENT '权限编码', description VARCHAR(100) COMMENT '描述', create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', update_user VARCHAR(50) COMMENT '更新用户', is_delete TINYINT(1) DEFAULT 0 COMMENT '是否删除,0-未删除,1-已删除' ); CREATE TABLE admin_user_role ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID', user_id VARCHAR(50) NOT NULL COMMENT '用户id', role_id VARCHAR(255) NOT NULL COMMENT '角色id', description VARCHAR(100) COMMENT '描述', create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', update_user VARCHAR(50) COMMENT '更新用户', is_delete TINYINT(1) DEFAULT 0 COMMENT '是否删除,0-未删除,1-已删除' ); CREATE TABLE admin_role_power ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 'ID', role_id VARCHAR(50) NOT NULL COMMENT '角色id', power_id VARCHAR(255) NOT NULL COMMENT '权限id', description VARCHAR(100) COMMENT '描述', create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', update_user VARCHAR(50) COMMENT '更新用户', is_delete TINYINT(1) DEFAULT 0 COMMENT '是否删除,0-未删除,1-已删除' ); ``` ### 基于方法的授权 #### 开启方法授权 在配置文件中添加如下注解,或者在启动类上添加都可以。 + 默认如果开启了方法授权访问同时也配置了全局角色为`roles("ADMIN")`那么所有接口都是可以访问的。 ```java @EnableMethodSecurity ``` #### 给用户授予角色和权限 DBUserDetailsManager中的loadUserByUsername方法,添加`authorities("USER_ADD", "USER_UPDATE")` + `roles("ADMIN")`:与authorities不能同时使用 ```java @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); User user = userMapper.selectOne(queryWrapper); if (user == null) { throw new UsernameNotFoundException(username); } else { // Collection authorities = new ArrayList<>(); // // 授予访问权限 // authorities.add(() -> "USER_LIST"); // authorities.add(() -> "USER_ADD"); // return new org.springframework.security.core.userdetails.User( // user.getUsername(), // user.getPassword(), // user.getEnabled(), // true, // 用户账号是否过期 // true, // 用户凭证是否过期 // true, // 用户是否未被锁定 // authorities); // 权限列表 return org.springframework.security.core.userdetails.User .withUsername(user.getUsername()) .password(user.getPassword()) .disabled(!user.getEnabled()) .credentialsExpired(false) .accountLocked(false) // SecurityFilterChain中配置hasRole("ADMIN")来判断的 // .roles("ADMIN")// 与authorities不能同时使用 // 给用户授予角色和权限 .authorities("USER_ADD", "USER_UPDATE") .build(); } } ``` #### 常用授权注解 + `hasAnyRole('ADMIN')`当前角色权限为ADMIN同时访问用户也要为`authentication.name == 'admin'` ```java @RestController @RequestMapping("/user") @Tag(name = "用户请求接口") public class UserController { @Autowired private UserService userService; @Operation(summary = "查询所有用户") // @PreAuthorize("hasAnyRole('ADMIN')") @PreAuthorize("hasAnyRole('ADMIN') and authentication.name == 'admin'")// 编写逻辑表达式 @GetMapping("/list") public List getList() { return userService.list(); } // 用户必须有 USER_ADD 权限 才能访问此方法 @Operation(summary = "添加用户") @PreAuthorize("hasAuthority('USER_ADD')") @PostMapping("/add") public void addUser(@RequestBody User user) { userService.addUserDetails(user); } } ``` > 如果当前用户不是admin用户访问不了。 > ![](./images/1739885175291-ac74528f-137c-4021-bd8d-4d925ff98d82.png)