目录
设计思路
登录成功后生成 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?
前端在登录成功后:
-
将 Token 存储在
localStorage
或sessionStorage
:localStorage.setItem("token", token);
-
每次请求后端接口时,将 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 本身不会直接加密用户密码,而是提供了密码加密的支持(如
BCryptPasswordEncoder
、Pbkdf2PasswordEncoder
等)。Spring Security 的目的是确保应用程序的整个安全性,并且支持安全认证的实现。 - 这样的配置可以正确地使用加密算法来对用户密码进行加密
/*** 密码加密* @return*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
MD5:
- 类型:一种哈希算法(单向加密算法),通常用于生成一个固定长度的哈希值(消息摘要)。
- 功能:MD5 主要用于数据的哈希处理,常见的应用场景是密码的存储(通过哈希值存储),数据完整性校验等。它本身不提供认证或授权功能,只能生成哈希值,并且它是不可逆的,即无法通过哈希值反推原始数据。
- 问题:虽然 MD5 曾经用于密码加密,但由于其速度非常快,且容易受到暴力破解和碰撞攻击的影响,现如今 MD5 已经不再推荐用于密码存储。更现代的加密算法,如 BCrypt、PBKDF2、Argon2 等被认为更安全。