SpringSecurity

1.认证授权的概述

1.1什么是认证?

进入移动互联网时代,大家每天都在刷手机,常用的软件有微信、支付宝、头条,抖音等,下边拿微信来举例子说明认证相关的基本概念,在初次使用微信前需要注册成为微信用户,然后输入账号和密码即可登录微信,==输入账号和密码登录微信的过程就是认证==。

系统为什么要认证?

认证是为了保护系统的隐私数据与资源,用户的身份合法,方可访问该系统的资源。

认证︰用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统==资源==时系统要求验证用户的身份信息,身份合法 方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:==用户名密码登录,二维码登录,手机短信登录,指纹认证等方式==。

1.2什么是会话?

用户认证通过后,为了避免用户的每次操作都进行认证,可将用户的信息保证在会话中。会话就是系统为了保持当前用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等。

1.2.1基于session的认证

它的交互流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发送给客户端的session_id存放到cookie中,这样用户客户端请求时带上session_id就可以验证服务器端是否存在session数据,以此完成瀛湖的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了。

1.2.2基于Token的认证

它的交互流程是,用户认证成功后,服务端生成一个token【令牌】(唯一字符串)【uuid,jwt】发送给客户端,客户端可以放在token通过验证后即可确认用户身份。

基于session的认证方式,由servlet规范制定,服务端要存储session信息需要占用内存资源,客户端需要支持cookie;基于token的方式,则一般不需要服务端存储token,并且不限制客户端的存储方式并且不限制客户端的存储方式cookie sessionStorage LocalStorage Vuex。如今移动互联网时代更多类型的客户端[pC ,android,IOS,]需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。

  1. 使用前后端分离或后台使用了集群--一定采用token模式。--一般线上的都是这种
  2. 传统的项目前端和后端都在一个工程下--基于session模式。--公司的系统(只需要很少的人登录)、仓库管理。。。

1.3什么是授权

还拿微信来举例子,微信登录成功后用户即可使用微信的功能,比如,发红包、发朋友圈、添加好友等,没有绑定银行卡的用户是无法发送红包的,绑定银行卡的用户才可以发红包,发红包功能、发朋友圈功能都是微信的资源即功能资源,用户拥有发红包功能的==权限==才可以正常使用发送红包==功能==,拥有发朋友圈功能的权限才可以便用发朋友圈功能,这个根据用户的权限来控制用户使用资源的过程就是授权。

1.权限【权限表】--资源【接口】

(一期项目的权限管理--不同的角色有不同的功能)

1.3.1为什么要授权

认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,==授权是在认证通过后发生的==,控制不同的用户能够访问不同的资源。授权:授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。

认证授权的框架:

1\]shiro 轻量级的认证授权 它可以整合任意框架 它支持javase和javaee \[2\]springsecurity 重量级的认证授权框架。它只能和spring整合,只支持javaee web框架。 spring非常麻烦,但是现在和springboot整合就很简单了。 ## 3.概述springsecurity [Spring Security](https://spring.io/projects/spring-security "Spring Security") ### 3.1什么是SpringSecurity? 百度百科:Spring Security是一个能够为基于Spring的企业应用系统提供==声明式的安全访问控制解决方案的安全框架==。它提供了一组可以在Sprirg应用上下文中配置的Bean,充分利用了Spring IOC,DI(控制反转Inversion of Control ,DI:Dependency Injection依赖主入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。以上解释来源于百度白科。 可以一句话来概括,**SpringSecurity 是一个安全框架。可以帮我们完成认证,密码加密,授权,rememberme的功能。** ## 4.快速入门SpringSecurity 1.导入依赖--也可以在创建项目时就选中添加 org.springframework.boot spring-boot-starter-security 2.创建接口资源----controller层 package com.wjy.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("/hello") public String hello(){ return "hello"; } } 3.访问启动项目并访问资源:[http://localhost:8080/hello](http://localhost:8080/hello "http://localhost:8080/hello") ![](https://img-blog.csdnimg.cn/img_convert/b3ef3ae54e78dc54119e7d7b4381fc3b.png) ![](https://img-blog.csdnimg.cn/img_convert/415373c149f30c4f2eb31712945c7193.png) ![](https://img-blog.csdnimg.cn/img_convert/5d51ff541118ea56ea43f90aca7d83bc.png) 发现 帮你跳转到登录页面。 因为springsecurity包含了很多过滤器,认证过滤器发现你没有登录就访问资源。默认调整到它内置的登录页面 ![](https://img-blog.csdnimg.cn/img_convert/4078ce45c178c559cb0e9053b3352e44.png) ### 4.1自定义账号和密码 ![](https://img-blog.csdnimg.cn/img_convert/4fd7b7a5e750b8aad92a941214058ac0.png) #### 4.1.1设置多个用户--定义一个配置类--基于内存 package com.wjy.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration //如果springboot的版本2.7.0以上的会有过期的波浪线 public class MySercurityConfig extends WebSecurityConfigurerAdapter { //密码编码器--会自动加密 @Bean public PasswordEncoder passwordEncoder() { BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); return passwordEncoder; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth //内存中认证和授权 .inMemoryAuthentication() //用户名 .withUser("wjy") //密码---加密和密码编辑器保持一致 .password(passwordEncoder().encode("123456")) //角色 .roles("admin") //具有的权限 .authorities("admin:select", "admin:update","admin:insert") //多个用户之间用and连接 .and() .withUser("wjy1") .password(passwordEncoder().encode("123456")) .roles("admin"); } } ![](https://img-blog.csdnimg.cn/img_convert/ad640d013317a5ebae431d4b62787b4a.png) 此时登录会报错,使用密码加密器。 修改配置类 ![](https://img-blog.csdnimg.cn/img_convert/4186a32d062951d14fa129a8c4f64586.png) ![](https://img-blog.csdnimg.cn/img_convert/ec6e5700661e587e461b683e77fd6c5a.png) #### 4.1.2密码加密器 分成两种类型:对称加密和非对称加密 对称加密:表示加密和解密使用同一把密钥。 非对称加密:表示加密和解密不是使用同一把密钥。md5 hash package com.wjy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; public class Test { public static void main(String[] args) { PasswordEncoder passwordEncoder=new BCryptPasswordEncoder(); //用于加密 String encode = passwordEncoder.encode("123456"); String encode2 = passwordEncoder.encode("123456"); String encode3 = passwordEncoder.encode("123456"); System.out.println(encode); System.out.println(encode2); System.out.println(encode3); //安全. boolean matches = passwordEncoder.matches("123456", encode2); System.out.println("是否密码正确:"+matches); } } 加密后无法解密--通过hash随机生成的--不用解密会自动比对 使用**new BCryptPasswordEncoder()对象的encode()方法**进行加密--安全的 MD5每次生成的加密都会相同--安全性低 #### 4.1.3上面测试的完整代码流程 package com.wjy.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration //如果springboot的版本2.7.0以上有其他的写法 public class MySercurityConfig extends WebSecurityConfigurerAdapter { //密码编码器--会自动加密 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } //用户认证和授权 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth //内存中认证和授权 .inMemoryAuthentication() //用户名 .withUser("wjy") //密码---加密和密码编辑器保持一致 .password(passwordEncoder().encode("123456")) //角色 .roles("admin") //具有的权限 .authorities("admin:select", "admin:update","admin:insert") .and() .withUser("wjy1") .password(passwordEncoder().encode("123456")) .roles("admin"); } //登录前后的权限设置 @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() //登录页面 .loginPage("/login.html") //登录成功后跳转的页面 .loginProcessingUrl("/login") //登录成功后跳转的页面--必须都是Post请求 .successForwardUrl("/success") //登录失败后跳转的页面 .failureForwardUrl("/error") //上面的页面请求无需认证--无需权限认证特权 .permitAll(); //如果使用的登录页面不是自己的--禁止跨越伪造请求的过滤器 http.csrf().disable(); //其他所有请求都需要认证 http.authorizeRequests() .anyRequest() .authenticated(); } } package com.wjy.controller.config; 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.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity//开启Spring Security的功能 public class SecurityConfig { //加密器 @Bean public PasswordEncoder passwordEncoder(){ PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); return passwordEncoder; } //设置用户 @Bean public UserDetailsService myconfigure() throws Exception{ return new InMemoryUserDetailsManager( User.withUsername("wjy") .password(passwordEncoder().encode("123456")) .roles("admin") .authorities("user:list","user:add") .build() ); } //登录前后的权限设置 @Bean public SecurityFilterChain myconfigure2(HttpSecurity http)throws Exception{ http.formLogin( //登录页面 form->form.loginPage("/login.html") //要处理请求的路径 //登录处理 .loginProcessingUrl("/login") .successForwardUrl("/success") .permitAll() ); //如果使用的登录页面不是自己的--禁止跨越伪造请求的过滤器 http.csrf(a->a.disable()); //其他的所有请求都需要认证 http.authorizeHttpRequests( auth->auth.anyRequest() .authenticated() ); return http.build(); } } package com.wjy.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; //@RestController//-->返回字符串 @Controller public class HelloController { @GetMapping("/hello") public String hello(){ return "hello"; } @PostMapping("/success") public String success(){ return "redirect:success.html"; } } 登录

账号登录

成功登录后跳转的页面 成功登录 ### 4.2获取当前登录者的信息(重要) SpringSecurity默认把当前用户的信息保存**在SecurityContext上下文**中了--如同之前的session package com.wjy.config; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user") public class UserController { @GetMapping("/info") public Authentication info() { //1. 获取SecurityContext对象 SecurityContext context = SecurityContextHolder.getContext(); //2.把用户得到信息封装到Authentication对象中(包含了用户名--角色以及权限--状态(是否过期到期了)) Authentication authentication = context.getAuthentication(); //3.获取所有的信息--getPrincipal() UserDetails principal = (UserDetails) authentication.getPrincipal(); //4.获取用户名--getUsername()具体的信息 System.out.println(principal.getUsername()); return authentication; } } ![](https://img-blog.csdnimg.cn/img_convert/86176f71b01a27ba3be16f0a1cde78e6.png) ## 5.security完成授权 授权:把当前用户具有的**权限** 和对应的**资源** **绑定的过程**。 -----授权一定发生在认证后 定义一些资源接口 @GetMapping("select") public String select(){ System.out.println("查询用户"); return "查询用户"; } @GetMapping("insert") public String insert(){ System.out.println("添加用户"); return "添加用户"; } @GetMapping("update") public String update(){ System.out.println("修改用户"); return "修改用户"; } @GetMapping("delete") public String delete(){ System.out.println("删除用户"); return "删除用户"; } @GetMapping("export") public String export(){ System.out.println("导出用户"); return "导出用户"; } 修改配置类 2.7版本之前的 ![](https://img-blog.csdnimg.cn/img_convert/5e0baa06485987dc20a56e17638b7141.png) 2.7版本之后的 ![](https://img-blog.csdnimg.cn/img_convert/9328dfbc6a5d190a239cd1185e8ca906.png) ### 5.1使用注解完成授权 上面的授权代码比较麻烦,我们可以使用注解完成授权的过程。 #### 1.开启security授权的注解驱动 **2.7版本之前的** ![](https://img-blog.csdnimg.cn/img_convert/f7d4a923f6657e479d15fb66b9095ad4.png) 2.7版本之后的 ![](https://img-blog.csdnimg.cn/img_convert/b2333be379b7de516dec8b37e431f3b1.png) #### 2.在资源上使用注解 @GetMapping("/select") @PreAuthorize("hasAnyAuthority('user:select')") public String select(){ return "select"; } ### 5.2权限不足跳转指定页面 ![](https://img-blog.csdnimg.cn/img_convert/364dc701b0f4fedf7e4d7783e94b75d6.png) **修改配置类** ![](https://img-blog.csdnimg.cn/img_convert/817252d24a5b70379cf7a730e3378c70.png) ## 6.【源码分析】SpringSecurity认证授权(了解) 为什么需要分析认证得流程? 我们要通过数据库进行认证和授权。 ### 6.1结构总览 Spring security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AoP等技术来实现,SpringSecurity对web资源的保护是靠==Filter=='实现的,所以从这个Filter来入手,逐步深入Spring Security 原理。当初始化Spring Security时,会创建一个名为SpringSecurityFilterChain的 Servlet过滤器,类型为org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类,下图是 Spring Security过虑器链结构图: ### 6.2过滤器中主要的几个过滤器及其作用 #### 1.SecurityContextPersistenceFi1ter 这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepesitory 中获取SecurityContext,然后把它设置给securityContextHolder.在请求完成后将SecurityContextHolder持有的SecurityContext再保存到配置好的SecurityContextRepository ,同时清除securityContextHolder所持有的SecurityContext; #### 2. UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的AuthenticationSuccessHandler和AuthenticationFailureHandler,这些都可以根据需求做相关改变; #### 3. FilterSecurityInterceptor 是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问; #### 4 .ExceptionTranslationFilter 能够捕获来自Filterchain所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException和 AccessDeniedException,其它的异常它会继续抛出。 ### 6.3【源码分析】Spring security 认证工作流程 UsernamePasswordAuthenticationFilter (attemptAuthentication) ProviderManager (authenticate)DaoAuthenticationProvider (retrieveUser)AbstractUserDetailsAuthenticationProvider (authenticate) #### 1 认证流程图 ![](https://img-blog.csdnimg.cn/img_convert/1c2d61f00417e63359f7aecfc9f5fc92.png) ![](https://img-blog.csdnimg.cn/img_convert/34afeba7d2c49c42f5586340153d8340.png) 查询数据库进行比对。 查询用户信息的代码---\>UserDetailsService中loadUserByUsername该方法。 我们只需要重写该方法即可。 ## 7. 自定义UserDetailsService接口类 **1.自定义service** package com.wjy.service; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Collection; @Service public class MyUserDetailService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //自己的业务--表示数据库中存在该用户--获取到权限 if ("wjy".equals(username)){ Collection authorities = new ArrayList<>(); //从数据库中可以获取用户的权限 authorities.add(new SimpleGrantedAuthority("user:select")); authorities.add(new SimpleGrantedAuthority("user:update")); authorities.add(new SimpleGrantedAuthority("user:insert")); return new User("wjy","$2a$10$NZ.q7Y.fJJ/5YxvqYXJ1Auq04JYxrZ7YQ5/x3XKZ7YQ5/x3XKZ7YQ5",authorities); }else if ("wjy1".equals(username)){ Collection authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("user:delete")); return new User("wjy1","$2a$10$NZ.q7Y.fJJ/5YxvqYXJ1Auq04JYxrZ7YQ5/x3XKZ7YQ5/x3XKZ7YQ5",authorities); } return null; } } **2.修改配置类** **2.7之前的版本** ![](https://img-blog.csdnimg.cn/img_convert/91ee81507cc492af5e879500e8b0d083.png) **2.7之后的版本** ![](https://img-blog.csdnimg.cn/img_convert/ea00110940c86bc928f02489737f1763.png) ## 8.SpringSecurity整合thymeleaf 模板引擎等价于jsp. /* Navicat Premium Data Transfer Source Server : gz02 Source Server Type : MySQL Source Server Version : 80032 Source Host : localhost:3306 Source Schema : security Target Server Type : MySQL Target Server Version : 80032 File Encoding : 65001 Date: 24/11/2023 11:16:37 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for sys_permission -- ---------------------------- DROP TABLE IF EXISTS `sys_permission`; CREATE TABLE `sys_permission` ( `perid` int(0) NOT NULL AUTO_INCREMENT, `pername` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL, `percode` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL, PRIMARY KEY (`perid`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of sys_permission -- ---------------------------- INSERT INTO `sys_permission` VALUES (1, '用户查询', 'user:query'); INSERT INTO `sys_permission` VALUES (2, '用户添加', 'user:add'); INSERT INTO `sys_permission` VALUES (3, '用户修改', 'user:update'); INSERT INTO `sys_permission` VALUES (4, '用户删除', 'user:delete'); INSERT INTO `sys_permission` VALUES (5, '用户导出', 'user:export'); -- ---------------------------- -- Table structure for sys_role -- ---------------------------- DROP TABLE IF EXISTS `sys_role`; CREATE TABLE `sys_role` ( `roleid` int(0) NOT NULL AUTO_INCREMENT, `rolename` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL, PRIMARY KEY (`roleid`) 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, '管理员'); INSERT INTO `sys_role` VALUES (2, '测试人员'); INSERT INTO `sys_role` VALUES (3, '普通用户'); -- ---------------------------- -- Table structure for sys_role_permission -- ---------------------------- DROP TABLE IF EXISTS `sys_role_permission`; CREATE TABLE `sys_role_permission` ( `perid` int(0) NULL DEFAULT NULL, `roleid` int(0) NULL DEFAULT NULL ) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of sys_role_permission -- ---------------------------- INSERT INTO `sys_role_permission` VALUES (2, 1); INSERT INTO `sys_role_permission` VALUES (1, 1); INSERT INTO `sys_role_permission` VALUES (3, 1); INSERT INTO `sys_role_permission` VALUES (4, 1); INSERT INTO `sys_role_permission` VALUES (2, 2); INSERT INTO `sys_role_permission` VALUES (1, 2); INSERT INTO `sys_role_permission` VALUES (3, 2); INSERT INTO `sys_role_permission` VALUES (1, 3); INSERT INTO `sys_role_permission` VALUES (5, 3); -- ---------------------------- -- Table structure for sys_user -- ---------------------------- DROP TABLE IF EXISTS `sys_user`; CREATE TABLE `sys_user` ( `userid` int(0) NOT NULL AUTO_INCREMENT, `username` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL, `userpwd` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL, `sex` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL, `address` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL, PRIMARY KEY (`userid`) 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, '张三', '$2a$10$cI7e7bgSs9.9nNHhxKO9LuK/Ll.AeZwgUyZb77oD2y3UwwZyZhWG6', '男', '郑州'); INSERT INTO `sys_user` VALUES (2, '李四', '$2a$10$cI7e7bgSs9.9nNHhxKO9LuK/Ll.AeZwgUyZb77oD2y3UwwZyZhWG6', '男', '北京'); INSERT INTO `sys_user` VALUES (3, '王五', '$2a$10$cI7e7bgSs9.9nNHhxKO9LuK/Ll.AeZwgUyZb77oD2y3UwwZyZhWG6', '女', '杭州'); -- ---------------------------- -- Table structure for sys_user_role -- ---------------------------- DROP TABLE IF EXISTS `sys_user_role`; CREATE TABLE `sys_user_role` ( `userid` int(0) NOT NULL, `roleid` int(0) NULL DEFAULT NULL ) ENGINE = InnoDB CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of sys_user_role -- ---------------------------- INSERT INTO `sys_user_role` VALUES (1, 1); INSERT INTO `sys_user_role` VALUES (2, 2); INSERT INTO `sys_user_role` VALUES (3, 3); INSERT INTO `sys_user_role` VALUES (1, 2); SET FOREIGN_KEY_CHECKS = 1; **pom依赖** org.thymeleaf.extras thymeleaf-extras-springsecurity5 org.thymeleaf.extras thymeleaf-extras-springsecurity6 spring.application.name=spring_security spring.datasource.url=jdbc:mysql://localhost:3306/tb_security?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false&allowPublicKeyRetrieval=true spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver mybatis-plus.mapper-locations=classpath*:mapper/*.xml mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl **实体类用快速生成--加上mybatis-plus** **生成实体类、mapper操作数据库、mapper.xml映射文件** mapper--需要自己定义的数据库查询 (根据数据库中已经存在的用户名匹配对应的密码判断是否登录成功) public interface PermissionMapper extends BaseMapper { public List selectByUserId(Integer userId); } **serivce类** 条件查找,判断用户是否存在: 存在--查询具有的权限(链表条件查询--自定义) package com.wjy.service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.wjy.entity.User; import com.wjy.mapper.PermissionMapper; import com.wjy.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @Service public class MyUserDetailService implements UserDetailsService { @Autowired private UserMapper userMapper; @Autowired private PermissionMapper permissionMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 根据用户名查询用户信息--username必须唯一 QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("username",username); User user = userMapper.selectOne(wrapper); //判断用户是否存在 if (Objects.nonNull(user)){ //查询当前用户具有的权限 List collection = permissionMapper.selectByUserId( user.getUserid())//根据用户id查询权限 .stream()//转换流 .map(p -> new SimpleGrantedAuthority(p.getPercode()))//把每次查询的权限码封装成SimpleGrantedAuthority .collect(Collectors.toList());//转换为集合 return new org.springframework.security.core.userdetails.User(user.getUsername(),user.getUserpwd(),collection); } return null; } } **配置类** package com.wjy.config; import com.wjy.service.MyUserDetailService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration //如果springboot的版本2.7.0以上有其他的写法 public class MySercurityConfig extends WebSecurityConfigurerAdapter { //密码编码器--会自动加密 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Autowired private MyUserDetailService myUserDetailService; //用户认证和授权 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(myUserDetailService); } //登录前后的权限设置 @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() //登录页面 .loginPage("/login.html") //登录成功后跳转的页面 .loginProcessingUrl("/login") //登录成功后跳转的页面--必须都是Post请求 .successForwardUrl("/success") //登录失败后跳转的页面 .failureForwardUrl("/error") //上面的页面请求无需认证--无需权限认证特权 .permitAll(); //权限不足跳转的页面 http.exceptionHandling().accessDeniedPage("/error.html"); //如果使用的登录页面不是自己的--禁止跨越伪造请求的过滤器 http.csrf().disable(); //其他所有请求都需要认证 http.authorizeRequests() .anyRequest() .authenticated(); } } **controller控制层** package com.wjy.controller; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user") public class UserController { @GetMapping("/info") public Authentication info() { //1. 获取SecurityContext对象 SecurityContext context = SecurityContextHolder.getContext(); //2.把用户得到信息封装到Authentication对象中(包含了用户名--角色以及权限--状态(是否过期到期了)) Authentication authentication = context.getAuthentication(); //3.获取所有的信息--getPrincipal() UserDetails principal = (UserDetails) authentication.getPrincipal(); //4.获取用户名--getUsername()具体的信息 System.out.println(principal.getUsername()); return authentication; } @GetMapping("/select") @PreAuthorize("hasAnyAuthority('user:query')") public String select(){ return "select"; } @GetMapping("/insert") @PreAuthorize("hasAnyAuthority('user:add')") public String insert(){ return "insert"; } @GetMapping("/update") @PreAuthorize("hasAnyAuthority('user:update')") public String update(){ return "update"; } @GetMapping("/delete") @PreAuthorize("hasAnyAuthority('user:delete')") public String delete(){ return "delete"; } @GetMapping("/export") @PreAuthorize("hasAnyAuthority('user:export')") public String export(){ return "export"; } } package com.wjy.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; //@RestController//-->返回字符串 @Controller public class HelloController { @GetMapping("/hello") public String hello(){ return "hello"; } @PostMapping("/success") public String success(){ return "success";//这里不能是redirect重定向 } } **跳转的权限网页** ![](https://img-blog.csdnimg.cn/img_convert/14a3fc68acaf4057111e5d6bbe85499a.png) \ 成功登录后跳转的页面 查询用户
删除用户
添加用户
修改用户
导出用户 **入口函数** ![](https://img-blog.csdnimg.cn/img_convert/a88449d1cc0dcd0d2b183ffc79dc8371.png) ## 9.SpringSecurity完成前后端完全分离 ![](https://img-blog.csdnimg.cn/img_convert/12633dc41c845aec397fd6324e6fbfdb.png) 前后端的分离:**响应的数据** 必须**为JSON数据**。 需要修改的代码有哪些? 1.登录成功需要返回json数据 2.登录失败需要返回json数据 3.权限不足时返回json数据 4.未登录访问资源返回json数据 ### 9.1JWT的概述 #### 1.什么是Jwt Json web token (JWT),是为了在网络应用环境间**传递声明** 而执行的一种基于**JSON的开放**标准((RFC7519).该token被设计为紧凑且==安全==的,特别适用于==分布式站点的单点登录(SSO)场景==。 JWT的声明一般被用来在**身份提供者** 和**服务提供者** 间传递**被认证** 的**用户身份信息** ,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也**可直接被用于认证,也可被加密**。 官网: [JSON Web Token Introduction - jwt.io](https://jwt.io/introduction/ "JSON Web Token Introduction - jwt.io") #### 2 前后端完全分离认证问题 互联网服务离不开用户认证。一般流程是下面这样。 1、用户向服务器发送用户名和密码。 2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。3、服务器向用户返回一个session_id,写入用户的Cookie。 4、用户随后的每一次请求,都会通过Cookie,将session_id传回服务器。 5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。 这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是前后端分离的服务导向架构,就要求session 数据共享,每台服务器都能够读取session。 举例来说,A网站和B网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现? 一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。 另一种方案是服务器索性不保存 session 数据了,所有数据都保存在==客户端==,每次请求都发回服务器。JWT就是这种方案的一个代表。 **JWT: 影响了网络带宽。** #### 3.JWT的原理 JWT的原理是,服务器认证以后,生成一个==JSON对象==,发回给用户,就像下面这样。 { "姓名":"张三", "角色":"管理员", "到期时间":"2022年8月1日0点0分" } 以后,用户与服务端通信的时候,都要发回这个JSON对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。 #### 4.JWT的数据结构 它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT内部是没有换行的,这里只是为了便于展示,将它写成了几行。JWT的三个部分: Header (头部) Payload(负载 载荷) Signature(签名) 写成一行,就是下面的样子。 Header.Payload.Signature #### 5.Header Header 部分是一个JSON对象,描述JWT的元数据,通常是下面的样子。 ![](https://img-blog.csdnimg.cn/img_convert/7341e3fea68ef405bf2d0108b603e551.png) 上面代码中, **alg属性** 表示**签名的算法(** algorithm),**默认是** HMAC SHA256 (写成 **HS256**) ; **typ属性** 表示这个**令牌(token)的类型** (type), JWT令牌统一写**为JWT**。 最后,将上面的JSON对象使用Base64URL算法转成字符串。 #### 6.Payload Payload 部分也是一个JSON对象,==用来存放实际需要传递的数据==。JWT规定了7个官方字段,供选用。iss (issuer):签发人exp (expiration time):过期时间 sub (subject):主题 aud (audience):受众 nbf (Not Before):生效时间 iat (lssued At):签发时间 jti (JWT ID):编号除了官方字段,你还可以在这个部分**定义自己的字段**,下面就是一个例子。 ![](https://img-blog.csdnimg.cn/img_convert/11d107e87d235e775e779d19fdf52956.png) 注意,**JWT 默认** 是**不加密** 的,任何人都可以读到,所以**不要把** ==**秘密信息【密码】** ==**放在这个部分**。这个JSON 对象也要使用Base64URL 算法转成字符串。 #### 7.Sinature Signature部分是**对前两部分的签名** ,**防止数据篡改**。 首先,需要指定一个**密钥(secret)** 。这个密钥只有**服务器才知道** ,**不能泄露给用户**。然后,使用Header里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。HMACSHA256(base64UrlEncode(header) + ".""+base64UrlEncode(payload),secret)算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。 ### 9.2JWT的使用方式 客户端收到服务器返回的JWT,可以存储在Cookie里面,也可以存储在localStorage。SessionStorage此后,**客户端** **每次与** **服务器** **通信** ,**都要带上这个** **JWT**。 把它放在Cookie里面自动发送,但是这样不能跨域,所以更好的做法是**放在** **HTTP请求的头信息**Authorization字段里面。 #### 1.引入jar com.auth0 java-jwt 4.4.0 #### 2.创建jwt的工具类 ①创建令牌---②校验令牌---③根据token获取自定义的信息 package com.wjy.util; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.interfaces.Verification; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Map; public class JWTUtil { private static final String SECRET = "wjyGyh"; //通过jwt创建token令牌 public static String createToken(Map map){ Map head = new HashMap<>(); head.put("alg", "HS256"); head.put("typ", "JWT"); Date date = new Date();//当前时间--发布时间 Calendar instance = Calendar.getInstance();//获取当前时间 instance.set(Calendar.SECOND,7200);//当前时间的基础上添加2个小时 Date time = instance.getTime(); return JWT.create() .withHeader(head)//头部信息 .withIssuedAt(date)//发布日期 .withExpiresAt(time)//过期时间 .withClaim("userinfo",map)//存放用户信息--设置个人信息 .sign(Algorithm.HMAC256(SECRET));//签名 } //校验token public static boolean verify(String token){ Verification require = JWT.require(Algorithm.HMAC256(SECRET)); try { require.build().verify(token); return true; } catch (JWTVerificationException e) { System.out.println("token错误,校验失败"); return false; } } //根据token获取自定义的信息 public static Map getTokenInfo(String token,String key){ return JWT.require(Algorithm.HMAC256(SECRET)) .build() .verify(token) .getClaim(key) .asMap(); } } ### 9.3登录成功需要返回json数据 **第一种方案:基于redis(集群)--相当于之前的session共享(单例)** 缺点:1.redis压力太大。2.项目依赖于第三方组件,存在redis第三方组件宕机后的影响。 ![](https://img-blog.csdnimg.cn/img_convert/62c2f0d2f909d0a8922c269ce480499d.png) **第二种:基于JWT** JWT会生成唯一标识,而且校验唯一标识。信息会存放在JWT中,同时也可以从JWT中获取。 ![](https://img-blog.csdnimg.cn/img_convert/6e567e5e57312e6851efb10ebf3007fd.png) **1.修改权限设置** ![](https://img-blog.csdnimg.cn/img_convert/ca31def816f027652bb52b0000bdac91.png) **2.书写登录成功后跳转的页面所携带给前端的数据** com.alibaba fastjson 2.0.22 //登录成功后跳转的页面--必须都是Post请求 private AuthenticationSuccessHandler successHandler() { return (httpServletRequest, httpServletResponse, authentication)->{ //设置响应的编码 httpServletResponse.setContentType("application/json;charset=utf-8"); //获取输出对象 PrintWriter writer = httpServletResponse.getWriter(); //封装map数据 Map map = new HashMap<>(); //①存放用户名 map.put("username", authentication.getName()); //获取权限 List collect = authentication.getAuthorities() .stream() .map(item -> item.getAuthority()).collect(Collectors.toList()); //②存放权限码 map.put("permissions", collect); //1.生成token String token = JWTUtil.createToken(map); //2.放入token,返回json数据 R r = new R(200, "登录成功", token); //转为json数据 String jsonString = JSON.toJSONString(r); //给前端判断的依据 writer.println(jsonString); //刷新流 writer.flush(); //关闭流 writer.close(); }; } ### 9.4登录失败返回json数据给前端 **1.修改配置类** ![](https://img-blog.csdnimg.cn/img_convert/dd22af72acf97ae647e88942d56278ec.png) **2.书写登录失败处理的函数** //登录失败后给前端返回的json数据 private AuthenticationFailureHandler failureHandler() { return (httpServletRequest, httpServletResponse, e)->{ //设置响应的编码--->获取输出对象 httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter writer = httpServletResponse.getWriter(); R r = new R(500, "登录失败", e.getMessage()); //转为json数据-->给前端判断的依据-->刷新流-->关闭流 String jsonString = JSON.toJSONString(r); writer.println(jsonString); writer.flush(); writer.close(); }; } ### 9.5权限不足返回给前端的json数据 **1.修改配置类** ![](https://img-blog.csdnimg.cn/img_convert/ca596e45b99f21f2d7405bab1bb01e84.png) **2.书写权限不足要处理的类** //权限不足后给前端返回的json数据 private AccessDeniedHandler accessDeniedHandler() { return (httpServletRequest, httpServletResponse, e)->{ //设置响应的编码--->获取输出对象 httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter writer = httpServletResponse.getWriter(); //创建R对象 并转换成json数据给前端 判断作为的依据-->刷新流-->关闭流 R r = new R(403, "权限不足", e.getMessage()); String jsonString = JSON.toJSONString(r); writer.println(jsonString); writer.flush(); writer.close(); }; } ### 9.6未登录返回json数据 **1.需要自定义一个过滤器** package com.wjy.filter; import com.alibaba.fastjson.JSON; import com.wjy.util.JWTUtil; import com.wjy.vo.R; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @Component//交于spring容器管理 public class LoginFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { response.setContentType("application/json;charset=utf-8"); //获取请求路径 String path = request.getRequestURI(); //获取请求方式 String method = request.getMethod(); //判断请求路径是否为登录路径--是的话放行 if ("/login".equals(path) && "POST".equals(method)){ filterChain.doFilter(request,response); return; } //1.获取token令牌--从请求头中获取 String token = request.getHeader("token"); //2.判断token是否为空 //①token为空 if (StringUtils.isEmpty(token)){ R r = new R(500, "未登录", null); PrintWriter writer = response.getWriter(); String jsonString = JSON.toJSONString(r); writer.write(jsonString); writer.flush(); writer.close(); return; } //②token不为空--验证token失败的情况(token不正确) if (!JWTUtil.verify(token)){ R r = new R(500, "token失效,登录失败", null); PrintWriter writer = response.getWriter(); String jsonString = JSON.toJSONString(r); writer.write(jsonString); writer.flush(); writer.close(); return; } //③token不为空--验证token成功--获取token中的信息 //获取token中的信息 SecurityContext context = SecurityContextHolder.getContext(); //--从JWT的工具类中获取 Map userinfo = JWTUtil.getTokenInfo(token, "userinfo"); //获取用户名 Object username = userinfo.get("username"); //获取权限码-->并转成需要的集合格式类型 List permissions = (List) userinfo.get("permissions"); List collect = permissions.stream() .map(item -> new SimpleGrantedAuthority(item)) .collect(Collectors.toList()); //创建token UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, collect); //将token放入上下文 context.setAuthentication(authenticationToken); //④验证token成功--放行 filterChain.doFilter(request,response); } } **2.修改配置类** ![](https://img-blog.csdnimg.cn/img_convert/8449b78d2c6513a9278929e2d55cad1f.png)

相关推荐
在京奋斗者11 分钟前
深入源码级别看spring bean创建过程
java·spring
大萌神Nagato28 分钟前
Johnson算法 流水线问题 java实现
java·算法
来自星星的坤34 分钟前
Spring Boot 邮件发送配置遇到的坑:解决 JavaMailSenderImpl 未找到的错误
java·开发语言·spring boot·后端·spring
F_lander39 分钟前
蓝桥杯冲刺题单--二分
java·算法·蓝桥杯
PureWT1 小时前
Springboot----@Role注解的作用
java
爱的叹息1 小时前
关于Spring MVC在无注解情况下通过参数名匹配获取请求参数的详细说明,包含代码示例和总结表格
java·spring·mvc
橘子青衫1 小时前
掌握HttpClient技术:从基础到实战(java.net.http)
java·后端·架构
缘友一世2 小时前
解决Spring Boot上传默认限制文件大小和完善超限异常(若依框架)
java·spring boot·后端
FixBug_Nick2 小时前
Jenkins配置的JDK,Maven和Git
java·git·jdk·jenkins·maven
青春不流名2 小时前
flink iceberg写数据到hdfs,hive同步读取
java·大数据