SpringSecurity——前后端分离登录状态如何保持

embedded/2025/3/26 6:02:17/

目录

设计思路 

登录成功后生成 JWT 并存入 Redis

🧾 前端如何处理 JWT?

🛡️ JWT 拦截器校验(自定义过滤器)

🧠为什么需要手动设置用户登录状态

① 创建认证对象

② 设置到 Spring Security 上下文

什么是SecurityContext?

📦 它具体保存了什么?

SecurityContextHolder 是什么?

安全退出:删除 Redis 中的 Token

没有权限访问资源时的回调处理逻辑 

权限认证过程

🟡 步骤 1:前端发起请求(带上 Token)

🟢 步骤 2:JWT过滤器拦截请求,进行认证处理

🔵 步骤 3:Spring Security 检查你有没有权限访问这个接口(授权)

🔴 步骤 4:抛出异常,触发回调

✅ 步骤 5:权限校验通过,放行执行 Controller 方法

🔑 Spring Security 和 MD5 的区别


设计思路 

  • 用户登录成功后,后端生成一个 JWT Token,并将该 Token 返回给前端。
  • 前端将 Token 存储在本地(如 localStorage / sessionStorage)。
  • 每次请求时,前端将 Token 放入 请求头(Authorization) 中,后端通过过滤器拦截请求并解析 Token,获取用户信息并认证。
  • Spring Security 不再使用 Session,而是通过 SecurityContextHolder 维护用户认证信息。

登录成功后生成 JWT 并存入 Redis

用户登录成功后,我们在 MyAuthenticationSuccessHandler 中生成 JWT Token,将其与用户 ID 绑定后存入 Redis,并返回给前端。

@Component
public class MyAuthenticationSuccessHandle implements AuthenticationSuccessHandler {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate RedisTemplate<String, String> redisTemplate;@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {// 获取用户信息(从认证信息中拿到我们自定义的UserDetails对象)TUser user = (TUser) authentication.getPrincipal();String userJson = JSONUtil.toJsonStr(user); // 序列化成 JSON 字符串// 生成 JWT Token,包含用户信息String token = JWTUtil.createToken(Map.of("user", userJson), Constant.JWT_SECRET.getBytes());// Redis Key 设置为用户 ID 的前缀形式String redisKey = Constant.REDIS_TOKEN_PREFIX + user.getId();// 将 Token 存入 Redis,并设置过期时间(单位:秒)redisTemplate.opsForValue().set(redisKey, token, Constant.EXPIRATION_TIME, TimeUnit.SECONDS);// 将 Token 返回给前端R result = R.builder().code(200).msg("登录成功").info(token).build();response.setContentType("application/json;charset=utf-8");response.setStatus(HttpServletResponse.SC_OK);response.getWriter().write(JSONUtil.toJsonStr(result));}
}

💡注意:

  • 存入 Redis 是为了实现 Token 的可控性,比如登出后手动失效、强制下线等。
  • 建议不要直接使用账号名作为 Redis Key,使用 user.getId() 更安全、唯一。

🧾 前端如何处理 JWT?

前端在登录成功后:

  1. 将 Token 存储在 localStoragesessionStorage

    localStorage.setItem("token", token);
    
  2. 每次请求后端接口时,将 Token 放在请求头中:

    axios.get('/api/user/info', {headers: {'token': localStorage.getItem("token")}
    });
    

🛡️ JWT 拦截器校验(自定义过滤器)

JWT 校验是在 Spring Security 的过滤器链中进行的。我们实现一个继承 OncePerRequestFilter 的类 TokenFilter

后端接口接收到前端的请求时,首先都会被jwt的验证过滤器拦截,拦截里面会验证jwt是否合法(是否是空、有没有篡改,和redis是否相等),验证未通过就直接给前端返回一个R对象的json,验证通过了,把spring security上下文中设置用户认证信息,表示该jwt的用户是登录过的,接下来就可以访问具体的后端controller接口了,接口里面执行具体的业务,然后controller接口返回json给前端,前端进行数据显示;

@Component
public class TokenFilter extends OncePerRequestFilter {@Resourceprivate RedisTemplate<String, String> redisTemplate;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {response.setContentType("application/json;charset=UTF-8");String requestURI = request.getRequestURI();if ("/login".equals(requestURI)) {// 登录接口放行filterChain.doFilter(request, response);return;}String token = request.getHeader("token");if (!StringUtils.hasText(token)) {R result = R.builder().code(901).msg("token不能为空").build();response.getWriter().write(JSONUtil.toJsonStr(result));return;}boolean verify = false;try {// 校验 token 是否被篡改verify = JWTUtil.verify(token, Constant.JWT_SECRET.getBytes());} catch (Exception e) {log.error("JWT验证失败", e);}if (!verify) {R result = R.builder().code(902).msg("token无效").build();response.getWriter().write(JSONUtil.toJsonStr(result));return;}// 解析 payload,获取用户信息JSONObject payload = JWTUtil.parseToken(token).getPayloads();String userJson = payload.getStr("user");TUser tUser = JSONUtil.toBean(userJson, TUser.class);// 校验 Redis 中是否存在该 tokenString redisToken = redisTemplate.opsForValue().get(Constant.REDIS_TOKEN_PREFIX + tUser.getId());if (!token.equals(redisToken)) {R result = R.builder().code(903).msg("请求token错误").build();response.getWriter().write(JSONUtil.toJsonStr(result));return;}// 认证通过,手动设置用户登录状态UsernamePasswordAuthenticationToken authToken =new UsernamePasswordAuthenticationToken(tUser, null, tUser.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authToken);filterChain.doFilter(request, response); // 放行}
}

🔍 易错点提示:

  • 没有给 Redis 的 Token 设置过期时间,可能导致旧的 Token 不自动失效。
  • 在手动设置用户登录状态的时候,没有设置用户权限,会导致认证通过但鉴权失败。
  • 如果自定义的 UserDetails 没有实现 getAuthorities(),也会导致授权失败。

🧠为什么需要手动设置用户登录状态

前端每次请求都会携带一个 JWT Token,后端通过过滤器验证这个 Token 是否有效。验证通过后,还需要告诉 Spring Security 当前用户是“已登录状态”,否则它认为是“匿名未认证用户”。就又会跳转到登录的页面。

这时候就需要我们手动创建一个认证对象 Authentication,并设置进 Spring Security 的上下文中(SecurityContext完成认证过程的补全。

① 创建认证对象

UsernamePasswordAuthenticationToken authToken =new UsernamePasswordAuthenticationToken(tUser, null, tUser.getAuthorities());

UsernamePasswordAuthenticationToken 这个类是 Authentication 接口的一个实现,代表一位“已认证的用户”。

  • tUser:这是你从 JWT 中还原出来的用户对象(一般实现了 UserDetails 接口),是登录用户的身份信息。
  • null:代表密码,这里传 null 是因为我们是基于 Token 的认证流程,已经不需要密码校验了
  • tUser.getAuthorities():这是用户的权限信息,决定了这个用户是否有权限访问某些接口。
  • 只有设置了权限信息(GrantedAuthority),Spring Security 才能在后续进行基于角色的访问控制。

② 设置到 Spring Security 上下文

SecurityContextHolder.getContext().setAuthentication(authToken);

Spring Security 维护着当前线程的用户登录状态,这个上下文信息存放在 SecurityContextHolder 中。

  • 通过 getContext() 拿到当前线程的上下文对象
  • 然后调用 setAuthentication() 方法,把我们手动创建的认证对象 authToken 设置进去

💡 设置后,Spring Security 后续的过滤器就认为:用户已经登录,并拥有对应的权限,可以放行访问资源。

什么是SecurityContext?

SecurityContext 是 Spring Security 用来 保存当前用户登录信息(认证信息)的一个对象

换句话说,它就像一个“身份信息仓库”,记录了当前用户是谁、是否登录、有哪些权限。

📦 它具体保存了什么?

主要保存的是一个接口 Authentication 的实现对象(比如 UsernamePasswordAuthenticationToken),这个对象里面包含了这些内容:

属性说明
Principal当前用户的信息(比如你自己的 TUser 对象)
Authorities当前用户的权限集合(角色列表)
Credentials通常是密码(认证后可设为 null
authenticated是否已认证(true 表示登录过)

SecurityContextHolder 是什么?

SecurityContextHolder 是一个工具类,它提供了静态方法让我们可以获取/设置当前线程中的 SecurityContext

它的默认作用范围是 当前线程(ThreadLocal),也就是说:

  • 每个请求线程都有自己的 SecurityContext
  • 不会互相干扰,安全、独立

安全退出:删除 Redis 中的 Token

当用户访问 /logout 接口时,Spring Security 会自动处理退出逻辑,我们只需要提供一个 LogoutSuccessHandler 来清理 Token 即可。

访问退出接口,那就是访问 /logout接口,这个接口是spring security框架提供的,我们不需要写controller,退出的具体操作逻辑是spring security自己实现的(内部把spring security context上下文的登录认证信息Authentication清除了),退出成功了会调用AppLogoutSuccessHandler这个handler,在handler中我们要把Redis中的登录token删除,然后再返回一个R对象的json告诉前端退出成功就可以了;

@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {@Resourceprivate RedisTemplate<String, String> redisTemplate;@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {response.setContentType("application/json;charset=utf-8");// 获取当前登录用户信息TUser tUser = (TUser) authentication.getPrincipal();// 删除 Redis 中的 TokenredisTemplate.delete(Constant.REDIS_TOKEN_PREFIX + tUser.getId());R result = R.builder().code(200).msg("退出成功").build();response.getWriter().write(JSONUtil.toJsonStr(result));}
}

注意:

  • 一定要确保 authentication 不为 null,否则获取用户信息会抛出异常。因此获取token的拦截器需要设定在退出登录拦截器之前
// 配置spring security框架的一些行为(配置我们自己的登录页,不使用默认的登录页)// 但是当配置了SecurityFilterChain这个配置类之后,springsecurity 框架的某些默认行为失效了(例如不拦截未登录的用户)// 此时应该重新配置@Bean// 安全过滤器链Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity ,CorsConfigurationSource configurationSource ) throws Exception { //httpSecurity方法参数注入Beanreturn httpSecurity// 配置自己的登录页面.formLogin( (formLogin) ->{formLogin.loginProcessingUrl("/login") // 登录账户密码往哪个地址提交.successHandler(myAuthenticationSuccessHandler).failureHandler(myAuthenticationFailHandler); // 登录失败的回调}).logout((logout)->{logout.logoutUrl("/logout") // 退出登录的url.logoutSuccessHandler(myLogoutSuccessHandler); // 退出登录成功之后的回调}).authorizeHttpRequests((authorizeHttpRequests)->{authorizeHttpRequests.requestMatchers("/toLogin","/common/captcha").permitAll() // 特殊情况,toLogin不需要登录就可以访问, 验证码不需要登录就可以访问.anyRequest().authenticated(); // 除了上述特殊情况之外,其他任何请求都需要认证之后才能访问}).csrf((csrf)->{// 禁止csrf跨站请求,禁用之后,肯定就不安全了,有csrf网络攻击的风险,后续加入jwt是可以防御的csrf.disable();}).cors((cors)->{ // 允许前端跨域访问cors.configurationSource( configurationSource);}).sessionManagement((sessionManagement)->{// session管理,设置无状态的session,即不创建session,每次请求都要重新认证sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS);})// 在登录filter 之后添加一个token过滤器 确保退出登录的时候可以获取到认证信息.addFilterBefore(tokenFilter, LogoutFilter.class)// 没有权限的回调.exceptionHandling((exceptionHandling)->{exceptionHandling.accessDeniedHandler(myAccessDeniedHandler);}).build();}

没有权限访问资源时的回调处理逻辑 

当一个“已经登录”的用户尝试访问没有权限的接口时,Spring Security 会触发 AccessDeniedHandler 回调,我们可以通过这段配置来告诉它:该怎么处理这种无权访问的情况(比如返回自定义 JSON 而不是默认的403页面)。

@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {// 返回json告诉前端 权限不足 不能请求response.setContentType("application/json;charset=utf-8");R result = R.builder().code(401).msg("权限不足").info(accessDeniedException.getMessage()).build();String json = JSONUtil.toJsonStr(result);response.getWriter().write(json);}
}

权限认证过程

🟡 步骤 1:前端发起请求(带上 Token)

前端访问一个需要登录的接口,比如:

GET /admin/data
Headers:token: eyJhbGciOiJIUzI1NiIsInR5cCI...

🟢 步骤 2:JWT过滤器拦截请求,进行认证处理

你写的 TokenFilter 拦截到了请求 。通过解析出user对象,可以获取到权限列表

// 校验token是否存在 & 合法
// 验证成功后,将认证信息设置到SecurityContext
SecurityContextHolder.getContext().setAuthentication(authToken);

✅ 到这一步,Spring Security 就认为你是“已登录状态”。

🔵 步骤 3:Spring Security 检查你有没有权限访问这个接口(授权)

这个检查过程就是“权限认证”。如果你在 Controller 或配置中做了如下限制:

@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/data")
public String adminData() {return "管理员数据";
}

Spring Security 会读取你认证信息里的权限:

authToken.getAuthorities(); // 获取用户权限

通常TUser这个实现了UserDetails接口并且实现了其中的抽象方法: 

/*** 用户的权限标识符*/@JsonIgnoreprivate List<TPermission> tPermissionsList;//-----------------实现UserDetails当中相关的方法(7个)--------------------@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {Collection<GrantedAuthority>  authorities = new ArrayList<>();// 角色权限控制
//        for (TRole tRole : this.tRoleList) {
//            // 角色的名称必须以ROLE_开头
//            authorities.add(new SimpleGrantedAuthority("ROLE_"+tRole.getRole()));
//        }// 资源权限控制 ---> 权限标识符for (TPermission tPermission : this.tPermissionsList) {authorities.add(new SimpleGrantedAuthority((tPermission.getCode())));}return authorities;}

它会判断这个用户是否具备 ROLE_ADMIN 权限。如果没有,则抛出 AccessDeniedException

🔴 步骤 4:抛出异常,触发回调

情况回调处理
没登录(Authentication 是 null)执行 .authenticationEntryPoint(...),比如返回 401 未登录
登录了但权限不够(权限不匹配)执行 .accessDeniedHandler(...),比如返回 403 权限不足

✅ 步骤 5:权限校验通过,放行执行 Controller 方法

如果你有权限,那 Spring Security 就会放行这次请求,请求最终进入你定义的 Controller 方法中,正常执行逻辑。

🔑 Spring Security 和 MD5 的区别

Spring Security

  • 类型:一个全面的安全框架,用于认证、授权、会话管理、防止攻击(如 CSRF、XSS)等。
  • 功能:Spring Security 主要用于保护应用程序的安全,包括用户认证(身份验证)、用户授权(权限控制)、会话管理、密码加密、加密算法支持、跨站请求伪造(CSRF)防护等。它的功能非常全面,适用于需要复杂权限管理和安全控制的应用。
  • 密码管理:Spring Security 本身不会直接加密用户密码,而是提供了密码加密的支持(如 BCryptPasswordEncoderPbkdf2PasswordEncoder 等)。Spring Security 的目的是确保应用程序的整个安全性,并且支持安全认证的实现。
  • 这样的配置可以正确地使用加密算法来对用户密码进行加密
    ​/*** 密码加密* @return*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}​

MD5

  • 类型:一种哈希算法(单向加密算法),通常用于生成一个固定长度的哈希值(消息摘要)。
  • 功能:MD5 主要用于数据的哈希处理,常见的应用场景是密码的存储(通过哈希值存储),数据完整性校验等。它本身不提供认证或授权功能,只能生成哈希值,并且它是不可逆的,即无法通过哈希值反推原始数据。
  • 问题:虽然 MD5 曾经用于密码加密,但由于其速度非常快,且容易受到暴力破解和碰撞攻击的影响,现如今 MD5 已经不再推荐用于密码存储。更现代的加密算法,如 BCryptPBKDF2Argon2 等被认为更安全。

http://www.ppmy.cn/embedded/176083.html

相关文章

将MySQL数据同步到Elasticsearch作为全文检索数据的实战指南

在现代应用中&#xff0c;全文检索是一个非常重要的功能&#xff0c;尤其是在处理大量数据时。Elasticsearch 是一个强大的分布式搜索引擎&#xff0c;能够快速地进行全文检索、分析和可视化。而 MySQL 作为传统的关系型数据库&#xff0c;虽然能够处理结构化数据&#xff0c;但…

好看的css星星效果边框

给客户做的动效图&#xff0c;结果应证了还是第一版的好&#xff0c;不忍舍弃&#xff0c;放这里&#xff0c;喜欢的人自取&#xff1b; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewp…

LeetCode707设计链表

思路&#xff1a;主要是确定&#xff0c;虚拟头节点不算个数&#xff0c;从第一个正式节点开始计数&#xff0c;下标从0开始&#xff0c;这个确定了就写就完了 typedef struct Node // 定义节点 {int val;struct Node* next; } Node;typedef struct MyLinkedList // 定义链表 …

创建自己的github.io

1、创建GitHub账号 GitHub地址&#xff1a;https://github.com/ 点击Sign up创建账号 如果已创建&#xff0c;点击Sign in登录 2、创建仓库 假设Owner为username&#xff0c;则Repository name为username.github.io说明&#xff1a; 1、Owner为用户名 2、Repository name为仓…

SpringMVC全局异常处理机制

异常处理机制 异常处理的两种方式&#xff1a; 编程式异常处理&#xff1a;是指在代码中显式地编写处理异常的逻辑。它通常涉及到对异常类型的检测及其处理&#xff0c;例如使用 try-catch 块来捕获异常&#xff0c;然后在 catch 块中编写特定的处理代码&#xff0c;或者在 f…

Parsing error: Unexpected token, expected “,“

今天在使用Trae AI 编程工具开发大文件切片上传功能&#xff0c;使用的是VUE3,TS技术栈&#xff0c;开发完成运行时&#xff0c;编译报错&#xff08;Parsing error: Unexpected token, expected ","&#xff09;&#xff0c;让AI自行修复此问题多次后还是没有解决&a…

【leetcode hot 100 131】分割回文串

解法一&#xff1a;回溯法动态规划法 回溯法&#xff1a; 假设我们当前搜索到字符串的第 i 个字符&#xff0c;且 s[0…i−1] 位置的所有字符已经被分割成若干个回文串&#xff0c;并且分割结果被放入了答案数组 ans 中&#xff0c;那么我们就需要枚举下一个回文串的右边界 j…

ubuntu人工智能深度学习环境搭建。cuda和cudnn和anaconda和torch的安装。

几乎和wsl差不多&#xff0c;网不好的先下载好软件包&#xff0c;按顺序执行命令。 sudo mv cuda-ubuntu2404.pin /etc/apt/preferences.d/cuda-repository-pin-600 sudo dpkg -i cuda-repo-ubuntu2404-12-8-local_12.8.1-570.124.06-1_amd64.deb sudo cp /var/cuda-repo-ubun…