SpringSercuiry(借助Redis项目授权)
一、添加redis的依赖!--Spring Boot-redis的依赖包-- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency二、yml配置#redis配置 spring: data: redis: host: 127.0.0.1 database: 0 password: 123456 port: 6379三、在认证成功处理器中使用Data Component ConfigurationProperties(prefix jwt) // 获取yml配置文件中前缀为jwt下对应的属性 public class LoginSuccessHandler implements AuthenticationSuccessHandler { Autowired private RedisTemplate redisTemplate; // 获取配置文件的属性 private String secret; Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // 解决请求和响应乱码问题 request.setCharacterEncoding(utf-8); response.setContentType(application/json;charsetutf-8); // 获取用户信息 TUser tUser (TUser) authentication.getPrincipal(); String jsonTUser JSONUtil.toJsonStr(tUser); // 定义负载 MapString, Object map new HashMap(); map.put(loginToken, jsonTUser); // 生成jwt的 token String token JWTUtil.createToken(map, secret.getBytes(StandardCharsets.UTF_8)); // System.out.println(token); // 将token 保存到redis中 hash类型 大key 公司名/项目名:模块名:功能名[:唯一标识的参数] // 将 user:login:token 这部分抽取成一个常量 redisTemplate.opsForHash().put(Const.LOGIN_TOKEN_KEY, String.valueOf(tUser.getId()), token); // 统一处理登录成功 Result r Result.success(token); // 将统一处理登录成功的对象转成json格式响应给前端 ObjectMapper om new ObjectMapper(); String json om.writeValueAsString(r); response.getWriter().print(json); } }package com.sy.utils; public class Const { /** * 登录的token */ public static final String LOGIN_TOKEN_KEY user:login:token; }四、处理redis中乱码但是我们发现认证成功后存储到redis中的值有乱码不方便我们查看这是因为存储的时候默认以jdk的Key序列化器JdkSerializationRedisSerializer后存储的看到是二进制数据但是不影响我们读取。怎么处理这样的数据呢只需要默认更改序列化的方式就可以了添加一个redis的配置类Configuration public class RedisConfig { // 更改默认的序列化方式 Bean public RedisTemplateObject, Object redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplateObject, Object redisTemplate new RedisTemplate(); // 默认的Key序列化器为JdkSerializationRedisSerializer // 给字符串的key序列化 redisTemplate.setKeySerializer(RedisSerializer.string()); // 给hash的key序列化 redisTemplate.setHashKeySerializer(RedisSerializer.string()); // 连接工厂 redisTemplate.setConnectionFactory(connectionFactory); return redisTemplate; } }解决完成后的效果五、请求携带token怎么能够保证每次请求携带这个服务器生成的token呢这个时候我们立马就想到了axios的拦截器该拦截器包括请求拦截器和响应拦截器我们在请求拦截器中获取之前在浏览器上存储的token然后加入到对应的请求头中就好了这样就避免了我们每次发送请求还需要手动的添加这个请求头。在axios实例中实现: /utils/axios.js//定义axios实例 import axios from axios; const request axios.create({ baseURL: http://localhost:8080/, timeout: 5000, headers: {X-Custom-Header: foobar} }); // 添加请求拦截器 request.interceptors.request.use(function (config) { // 在发送请求之前做些什么 // 添加请求头 const token sessionStorage.getItem(loginToken); console.log(token); if (token) { config.headers[authorization] Bearer${token}; } return config; }, function (error) { // 对请求错误做些什么 return Promise.reject(error); }); // 添加响应拦截器 request.interceptors.response.use(function (response) { // 2xx 范围内的状态码都会触发该函数。 // 对响应数据做点什么 return response; }, function (error) { // 超出 2xx 范围的状态码都会触发该函数。 // 对响应错误做点什么 return Promise.reject(error); }); export default request六、过滤器获取请求头但是又面临一个问题我们应该怎么获取这个请求头呢什么时候获取呢以后每次请求都会发送这个请求头如果我们写在对应的Controller中那么每个controller的每个方法中都需要获取并验证对吧这个时候我们想到了过滤器我们可以定义个过滤器在这个过滤器中来获取请求头验证这个token是否一致同时因为spring security底层也是很多的过滤器链定义完成后加入到安全框架的过滤器链中就好了。需要注意的是登录操作是不需要验证的所以需要直接放行我们这里继承OncePerRequestFilter而不是我们之前学习的实现Filter因为需要类型转换(request,response)package com.sy.filter; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import cn.hutool.jwt.JWTUtil; import com.sy.pojo.TUser; import com.sy.utils.Const; import com.sy.utils.Result; import jakarta.annotation.Resource; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.Data; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; import java.nio.charset.StandardCharsets; Component Data public class JwtTokenFilter extends OncePerRequestFilter { Value(${jwt.secret}) private String secret; Resource private RedisTemplateString, Object redisTemplate; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 解决乱码 request.setCharacterEncoding(utf-8); response.setContentType(application/json;charsetutf-8); // 获取请求路径 因为此时我们项目的上下问名称为 /,所以获取的路径为 /xx/xx String uri request.getRequestURI(); // 如果路径是登录页面的路径直接放行 if (/user/login.equals(uri)) { filterChain.doFilter(request, response); } else { // 否则需要验证了 // 1.获取token String authorization request.getHeader(authorization); // 判断是否为null if (!StringUtils.hasText(authorization)) { // 返回给前端对应的错误信息 response.getWriter().print(JSONUtil.toJsonStr(Result.error(505, token不存在))); } else { String token authorization.substring(6); // 验证token boolean verify false; try { verify JWTUtil.verify(token, secret.getBytes(StandardCharsets.UTF_8)); } catch (Exception e) { e.printStackTrace(); } if (!verify) { // 验证错误返回错误信息 response.getWriter().print(JSONUtil.toJsonStr(Result.error(506, token不合法))); } else { // 否则和redis中的token对比一下 // 获取redis中token // hash中的key我们当时存储的是用户id怎么获取呢 // 通过jwt解析呀 JSONObject payloads JWTUtil.parseToken(token).getPayloads(); String tUser_json payloads.get(loginToken, String.class); // 将json字符串转换为对象 TUser tUser JSONUtil.toBean(tUser_json, TUser.class); String redis_token (String) redisTemplate.opsForHash().get(Const.LOGIN_TOKEN_KEY, String.valueOf(tUser.getId())); if (!token.equals(redis_token)) { response.getWriter().print(JSONUtil.toJsonStr(Result.error(507, token校验错误))); } else { // 同时将认证信息添加到security上下文 UsernamePasswordAuthenticationToken upat new UsernamePasswordAuthenticationToken( tUser, // 这里什么都包含了 null, // 密码null null // 权限null ); SecurityContextHolder.getContext().setAuthentication(upat); // 放行 filterChain.doFilter(request, response); } } } } } }七、添加到过滤器链中再一次认证通过后发送访问Index页面前端修改:script setup import { onMounted, ref } from vue import {hello} from /api/hello.js; import {useRouter} from vue-router const username ref(); // 获取当前路由实例 const router useRouter() // 页面加载事件 onMounted(() { // 向服务端发送请求 hello().then(response { // console.log(response.data) // 判断如何没有登录成功跳转到登录页面 if (response.data.code 200) { username.value response.data.data.username } else { router.push(/login) } }) }) /script template h1欢迎 {{username}} 来到胜雅教育后台管理系统首页/h1 /template style scoped h1{ color: red; font-size: 50px; text-align: center; } /style/api/hello.js// 导入axios实例对象 import axios from /utils/axios.js // 向服务端发送请求 params: 发送请求时携带的参数 data 思考: 登录功能是否可以使用get提交方式? 不可以使用 因为不安全 使用post请求 export const hello () { return axios({ url: /hello, method: post }) }首页向服务端发送请求获取当前认证成功的用户信息package com.sy.controller; import com.sy.pojo.TUser; import com.sy.utils.Result; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * author Mr·Zhai * version 1.0 * description: TODO * date 2026/4/14 20:59 */ RestController public class HelloController { PostMapping(/hello) public ResultTUser hello(Authentication authentication){ TUser tUser (TUser) authentication.getPrincipal(); return Result.success(tUser); } }重启项目如何让token失效因为JWT无状态重启项目后jwt并没有失效依然可以访问后端的接口原因是你重启后端springboot项目后前端sessionStorage中token没有失效后端redis中的token也没有失效解决办法1、把jwt存入redis中并设置一个过期时间到期后jwt自动失效30分钟失效2、实现一个退出功能用户点击退出登录让jwt失效用户如果不点击退出3、服务关闭/重启删除redis的所有jwt而不是某一个用户的这个时候使用监听器解决监听项目的关闭事件就可以了package com.sy.listener; import com.sy.utils.Const; import jakarta.annotation.Resource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextClosedEvent; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; /** * 监听服务器关闭事件 */ Component public class ApplicationShutdownListener implements ApplicationListenerContextClosedEvent { Resource private RedisTemplateString,Object redisTemplate; /** * 监听到服务器关闭的时候, 自动执行该方法 因为我们可以在该方法中删除redis中的token * param event */ Override public void onApplicationEvent(ContextClosedEvent event) { System.out.println(监听到服务器关闭事件...); redisTemplate.delete(Const.LOGIN_TOKEN_KEY); } }设置过期时间用户退出操作:script setup import { onMounted, ref } from vue import {hello, userLogout} from /api/hello.js; import {useRouter} from vue-router const username ref(); // 获取当前路由实例 const router useRouter() // 页面加载事件 onMounted(() { // 向服务端发送请求 hello().then(response { console.log(response.data) // 判断如何没有登录成功跳转到登录页面 if (response.data.code 200) { username.value response.data.data.username } else { router.push(/login) } }) }) // 用户退出操作 function logOut(){ // 发送异步请求 userLogout().then(response { // console.log(response.data) if (response.data.code 200) { // 将本地的token删除 sessionStorage.removeItem(loginToken) // 跳转到登录页面 router.push(/login) } }) } /script template h1 欢迎 {{username}} 来到胜雅教育后台管理系统首页 button clicklogOut退出/button /h1 /template style scoped *{ margin: 0; padding: 0; } h1{ color: red; font-size: 50px; text-align: center; } button{ width: 100px; height: 40px; } /style/api/hello.js// 导入axios实例对象 import axios from /utils/axios.js // 向服务端发送请求 params: 发送请求时携带的参数 data 思考: 登录功能是否可以使用get提交方式? 不可以使用 因为不安全 使用post请求 export const hello () { return axios({ url: /hello, method: post }) } // 用户退出操作 export const userLogout () { return axios({ url: /user/logout, method: get }) }后端实现:RestController public class HelloController { Autowired private RedisTemplate redisTemplate; PostMapping(/hello) public ResultTUser hello(Authentication authentication){ TUser tUser (TUser) authentication.getPrincipal(); return Result.success(tUser); } GetMapping(/user/logout) public Result logout(Authentication authentication){ TUser tUser (TUser) authentication.getPrincipal(); // 删除redis中指定的token Long count redisTemplate.opsForHash().delete(Const.LOGIN_TOKEN_KEY, String.valueOf(tUser.getId())); return Result.success(count); } }八、权限授予和我们之前讲的一样现在这里就说一个万一没有权限怎么办呢 提示暂无权限操作前端实现:script setup import { onMounted, ref } from vue import {hello, userLogout} from /api/hello.js; import {list, add, del} from /api/product.js; import {useRouter} from vue-router const username ref(); // 获取当前路由实例 const router useRouter() // 页面加载事件 onMounted(() { // 向服务端发送请求 hello().then(response { console.log(response.data) // 判断如何没有登录成功跳转到登录页面 if (response.data.code 200) { username.value response.data.data.username } else { router.push(/login) } }) }) // 用户退出操作 function logOut(){ // 发送异步请求 userLogout().then(response { // console.log(response.data) if (response.data.code 200) { // 将本地的token删除 sessionStorage.removeItem(loginToken) // 跳转到登录页面 router.push(/login) } }) } // 商品列表 function productList(){ list().then(response { console.log(response.data) }) } // 商品添加 function productAdd(){ add().then(response { console.log(response.data) }) } // 商品删除 function productDel(){ del().then(response { console.log(response.data) }) } /script template h1 欢迎 {{username}} 来到胜雅教育后台管理系统首页 button clicklogOut退出/button /h1 p button clickproductList a hrefjavascript:void(0)产品管理-列表/a /button /p p button clickproductAdd a hrefjavascript:void(0)产品管理-录入/a /button /p p button clickproductDel a hrefjavascript:void(0)产品管理-删除/a /button /p /template style scoped *{ margin: 0; padding: 0; } h1{ color: red; font-size: 50px; text-align: center; } button{ width: 100px; height: 40px; } p{ margin-bottom: 20px; margin-left: 50px; } p a{ text-decoration: none; } /style// 导入axios实例对象 import axios from /utils/axios.js // 商品列表 export const list () { return axios({ url: /product/list, method: get }) } // 商品添加 export const add () { return axios({ url: /product/add, method: get }) } // 商品删除 export const del () { return axios({ url: /product/del, method: get }) }后端实现:package com.sy.controller; import com.sy.utils.Result; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; RestController RequestMapping(/product) public class ProductController { /** * 获取产品列表 * return */ GetMapping(/list) PreAuthorize(hasAuthority(product:list)) public Result ProductList(){ return Result.success(产品列表); } /** * 新增 产品信息 * return */ GetMapping(/add) PreAuthorize(hasAuthority(product:add)) public Result ProductAdd(){ return Result.success(添加产品); } /** * 删除产品信息 * return */ GetMapping(/del) PreAuthorize(hasAuthority(product:delete)) public Result ProductDel(){ return Result.success(删除产品); } }修改JwtTokenFilter过滤器中将 权限信息 添加到 添加到security上下文对象中修改为以下内容:我们去配置一个无权限配置的提示信息和我们之前的成功失败的处理器是一样的也可以或者使用以下方式即可package com.sy.config; import cn.hutool.json.JSONUtil; import com.sy.filter.JwtTokenFilter; import com.sy.handle.LoginFailHandler; import com.sy.handle.LoginSuccessHandler; import com.sy.utils.Result; 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.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.Arrays; // 配置类 Configuration EnableMethodSecurity public class SecurityConfig { Autowired private LoginSuccessHandler loginSuccessHandler; Autowired private LoginFailHandler loginFailHandler; Autowired private JwtTokenFilter jwtTokenFilter; Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } /** * 跨域配置源 */ Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration new CorsConfiguration(); // 允许的前端域名,将我们的前端域名写道这里就好了 configuration.setAllowedOrigins(Arrays.asList(http://localhost:5173)); // 允许的请求方法 configuration.setAllowedMethods(Arrays.asList(GET, POST, PUT, DELETE, OPTIONS)); // 允许的请求头 configuration.setAllowedHeaders(Arrays.asList(*)); // 允许携带凭证如果这个属性为true,setAllowedOrigins这个一定是一个具体的域名 configuration.setAllowCredentials(true); // 预检请求缓存时间 configuration.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); // 将定义好的跨域规则configuration应用到所有请求路径/**上。 // registerCorsConfiguration(/**, configuration); 中 // 双星号 /** 是用来匹配所有的URL路径的通配符。 // 这意味着 CorsConfiguration 配置 将应用于应用程序中的所有路径允许或拒绝跨域请求的访问。 source.registerCorsConfiguration(/**, configuration); return source; } Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { return httpSecurity .formLogin(formLogin - formLogin .loginProcessingUrl(/user/login) .successHandler(loginSuccessHandler) // 登录成功后的处理 .failureHandler(loginFailHandler) // 登录失败后的处理 ) .csrf(csrf-{ // 禁用csrf防御 因为页面获取不到这个值了, 后续可以使用jwt进行防御 csrf.disable(); }) // 解决跨域问题 .cors(cors-{ // 解决跨域问题 cors.configurationSource(corsConfigurationSource()); }) // 添加自定义过滤器 在用户名和密码认证之前添加 .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class) // 设置无权限配置 .exceptionHandling(exceptionHandling - exceptionHandling .accessDeniedHandler((request, response, accessDeniedException) - { // 响应给前端 response.getWriter().print(JSONUtil.toJsonStr(Result.error(500, 暂无权限))); }) ) .authorizeHttpRequests(authorizeHttpRequests - authorizeHttpRequests //拦截所有的请求要求经过认证 .anyRequest().authenticated()) .build(); } }