简介 官网
Spring Security是一个Java框架,用于保护应用程序的安全性。它提供了一套全面的安全解决方案,包括身份验证、授权、防止攻击等功能。Spring Security基于过滤器链的概念,可以轻松地集成到任何基于Spring的应用程序中。它支持多种身份验证选项和授权策略,开发人员可以根据需要选择适合的方式。此外,Spring Security还提供了一些附加功能,如集成第三方身份验证提供商和单点登录,以及会话管理和密码编码等。总之,Spring Security是一个强大且易于使用的框架,可以帮助开发人员提高应用程序的安全性和可靠性。
👉🏻示例代码地址
主要功能: 1. 认证(Authentication)
用户身份验证 :Spring Security 通过多种方式(如表单登录、HTTP Basic、OAuth2 等)验证用户身份,确保只有合法用户能够访问系统。
灵活的认证机制 :支持自定义认证流程和提供者,便于与各种身份验证方案集成。
2. 授权(Authorization)
访问控制 :在用户通过认证后,Spring Security 根据配置的权限规则(如角色、权限等)控制用户对资源的访问。
方法级安全 :通过注解(例如 @PreAuthorize
、@Secured
等)可以在业务逻辑层面直接控制访问权限,实现细粒度的权限管理。
3. 防御常见攻击
CSRF 攻击防护 :内置跨站请求伪造(CSRF)防护机制,有效降低攻击风险。
会话管理 :提供会话固定攻击防护、并发登录控制等功能,保障会话安全。
安全头设置 :自动配置 HTTP 安全头(如 X-Frame-Options、X-XSS-Protection 等)来增强安全性。
4. 可扩展性与定制化
高度定制化 :可以根据项目需求定制安全策略,从认证流程到授权规则均可自定义。
与 Spring 生态系统无缝集成 :与 Spring Boot、Spring MVC 等其他 Spring 模块结合紧密,简化了安全配置和管理。
5. 支持多种安全协议
OAuth2 与 OpenID Connect :内置对 OAuth2 和 OpenID Connect 的支持,便于构建基于第三方身份验证的应用程序。
LDAP 集成 :支持 LDAP 作为用户信息和权限的存储方案,方便与企业级认证系统集成。
快速入门 在原有SpringBoot项目中引入依赖即可
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency >
再次访问时,会出现登录窗口
用户名为user,登录密码会打印在控制台
登录后就可以访问原来接口的数据。
认证 登录校验流程
原理 Spring Security 主要通过 一系列的过滤器(Filter) 进行请求的拦截、认证和授权。
常见的过滤器包括:
过滤器名称
作用
SecurityContextPersistenceFilter
读取、存储 SecurityContext
认证信息
UsernamePasswordAuthenticationFilter
处理表单登录请求(用户名密码认证)
BasicAuthenticationFilter
处理 Basic 认证
BearerTokenAuthenticationFilter
解析 JWT Token 认证
ExceptionTranslationFilter
处理认证或授权失败的异常
FilterSecurityInterceptor
进行授权(权限判断)
⚠ 过滤器链的执行顺序决定了 Spring Security 处理请求的方式。
认证流程
名称
类型
作用
UsernamePasswordAuthenticationFilter
过滤器
拦截登录请求(通常是 /login
),从 HTTP 请求中提取用户名和密码,封装成一个待认证的 Authentication
对象,并触发后续认证流程。
ProviderManager
认证管理器
实现了 AuthenticationManager
,内部维护一组 AuthenticationProvider
,负责将认证请求委派给能处理该类型令牌的 Provider。
DaoAuthenticationProvider
认证提供者
实现了 AuthenticationProvider
,调用 UserDetailsService
加载用户、校验密码,并在认证成功后构造包含用户权限的 Authentication
。
InMemoryUserDetailsManager
用户服务
实现了 UserDetailsService
,负责根据用户名从内存(或数据库)中查询用户信息,并返回一个包含用户名、密码、权限等的 UserDetails
时序步骤详解
提交用户名和密码
Client → UsernamePasswordAuthenticationFilter 用户在登录页面输入用户名/密码,浏览器发起 POST /login
请求,UsernamePasswordAuthenticationFilter
拦截该请求。
封装 Authentication 对象
UsernamePasswordAuthenticationFilter
从请求中解析出用户名和密码
封装成一个 UsernamePasswordAuthenticationToken
(此时 isAuthenticated()==false
,且内部只有用户名和密码,没有权限信息)
调用认证管理器
UsernamePasswordAuthenticationFilter → ProviderManager.authenticate(…) 将上一步的 UsernamePasswordAuthenticationToken
传给 ProviderManager
(即 AuthenticationManager
)进行认证。
委派给 DaoAuthenticationProvider
ProviderManager → DaoAuthenticationProvider.authenticate(…) ProviderManager
遍历其持有的所有 AuthenticationProvider
,找到支持 UsernamePasswordAuthenticationToken
的 DaoAuthenticationProvider
,并调用它的 authenticate()
方法。
加载用户信息
返回 UserDetails 对象
InMemoryUserDetailsManager → DaoAuthenticationProvider 返回包含用户名、加密密码、权限列表等的 UserDetails
。
密码校验
DaoAuthenticationProvider
使用注入的 PasswordEncoder
(如 BCryptPasswordEncoder
)将用户提交的密码加密后,与 UserDetails.getPassword()
(数据库中已加密的密码)进行比对。
如果不匹配,则抛出 BadCredentialsException
,认证失败。
构造已认证的 Authentication
DaoAuthenticationProvider
如果密码校验通过,就基于原来的 UsernamePasswordAuthenticationToken
,将其 principal
(UserDetails
)和 authorities
(权限列表)填充进去,并将 authenticated
标志设为 true
,形成一个完整的已认证令牌。
返回已认证的 Authentication
DaoAuthenticationProvider → ProviderManager → UsernamePasswordAuthenticationFilter 最终将这个已认证的 Authentication
对象一路返回给最初的过滤器。
保存到 SecurityContextHolder
UsernamePasswordAuthenticationFilter
调用 SecurityContextHolder.getContext().setAuthentication(authentication)
,将认证结果存入当前线程的安全上下文中,后续同一请求的其他过滤器或业务代码都能通过 SecurityContextHolder
获取到当前用户信息。
随后通常会跳转到登录成功页面或返回 JWT Token(前后端分离时)
形象解释 想象你要进入一家高级俱乐部,整个 Spring Security 认证流程就像你从门外走到俱乐部大堂,再到最终拿到 VIP 通行证的过程:
走到门口——UsernamePasswordAuthenticationFilter(门卫)
你来到俱乐部门口,门卫(过滤器)会先问:“请出示你的邀请函(用户名/密码)。”
如果你连邀请函都没带,他会拦下你,直接说“请先登录”。
如果你递上了邀请函,他就把它交给保安队长去进一步核实。
找保安队长——ProviderManager(保安队长)
门卫把邀请函交给保安队长,队长说:“好,我这里有好几位专门负责不同类型邀请函的保安(AuthenticationProvider),我来决定把你交给谁检查。”
队长看了下这是常规的“用户名+密码”邀请函,就交给负责这类的保安(DaoAuthenticationProvider
)。
验证身份——DaoAuthenticationProvider(专职核查保安)
这位保安会带你去后台档案室(调用 UserDetailsService
)找你的“会员档案”:
去档案室查询:这就像他打开了会员数据库,找到了你的档案(UserDetails
)。
核对签名:保安拿着你手上的签名(密码),用他们的密钥(PasswordEncoder
)进行比对。
比对不符 :保安会立刻说“这签名不对,你不是会员”,认证失败。
比对通过 :保安给你盖章,给你的邀请函加上“已认证”标记,告诉队长“他是真会员”。
盖上“已认证”印章——返回 Authentication
保安把“已盖章的邀请函”(已认证的 Authentication
对象,里面写着你的会员等级、特权列表)交还给保安队长,队长再转交给门卫。
登记通行证——SecurityContextHolder(签到簿)
门卫把你的“已认证邀请函”放进签到簿(SecurityContext
),这样整个俱乐部其他区域的工作人员(后续的过滤器或业务逻辑)都能查到你的身份和权限。
发放 VIP 通行证——生成 JWT & 缓存
同时,俱乐部后台给你制作了一张带有你身份信息和到期时间的 VIP 通行证(JWT),并在门卫办公室(Redis)存了一份你的档案副本:
JWT :就像一张加密的电子通行证,你离开后还可以凭它再次进入。
Redis 缓存 :就像门卫办公室里存了一份你的会员资料复印件,加快下次验证速度。
你拿着通行证入内
拿到通行证后,你就可以自由进出俱乐部的各个受保护区域(受保护的 API)。每次进门,门卫只要扫描你的通行证(解析 JWT),确认签名没问题,再从办公室(Redis)快速取出你的会员档案,就知道你有哪几种特权。
注销离场——logout(销毁缓存)
当你要离开时,门卫把你在办公室的会员档案复印件(Redis 缓存)销毁。这样即使有人捡到你的旧通行证,门卫扫描后也查不到你的档案,就会被拒绝入内。
具体实现 流程 :前端→封装令牌→AuthenticationManager
→UserDetailsService
+PasswordEncoder
→认证通过→生成 JWT→Redis 缓存→返回 Token
创建一个类实现UserDetailsService接口,自定义加载逻辑
默认情况下,Spring Security 并不知道你的用户数据存在哪里,也不知道你用的是哪张表、哪种 ORM。
通过自己实现 loadUserByUsername
,你可以:
从数据库(MyBatis、JPA、JDBC)、缓存(Redis)、外部系统(LDAP、微服务)中查询用户;
查询用户的角色和权限,把它们封装到返回的 UserDetails
(这里是 LoginUser
)里;
对不存在的用户抛出异常,让 Spring Security 知道该如何反馈“用户名不存在”或“密码错误”的信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper <>(); wrapper.eq(User::getUserName,username); User user = userMapper.selectOne(wrapper); if (Objects.isNull(user)){ throw new RuntimeException ("用户名或密码错误" ); } return new LoginUser (user); } }
因为UserDetailsService方法的返回值是UserDetails类型,所以需要定义一个类,实现该接口,把用户信息封装在其中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Data @NoArgsConstructor @AllArgsConstructor public class LoginUser implements UserDetails { private User user; @Override public Collection<? extends GrantedAuthority > getAuthorities() { return null ; } @Override public String getPassword () { return user.getPassword(); } @Override public String getUsername () { return user.getUserName(); } @Override public boolean isAccountNonExpired () { return true ; } @Override public boolean isAccountNonLocked () { return true ; } @Override public boolean isCredentialsNonExpired () { return true ; } @Override public boolean isEnabled () { return true ; } }
密码加密存储 我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。
我们可以定义一个SpringSecurity的配置类
1.基于 WebSecurityConfigurerAdapter
(5.6 及更早版本)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder () { return new BCryptPasswordEncoder (); } @Autowired JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Override protected void configure (HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement().disable() .authorizeRequests() .antMatchers("/user/login" ).anonymous() .anyRequest().authenticated(); http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } @Bean @Override public AuthenticationManager authenticationManagerBean () throws Exception { return super .authenticationManagerBean(); } }
2.Spring Security 5.7+ 推荐的 无侵入式 Bean 配置 (不再继承 WebSecurityConfigurerAdapter
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder () { return new BCryptPasswordEncoder (); } @Bean public SecurityFilterChain securityFilterChain (HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception { http .csrf().disable() .sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS) .and() .authorizeHttpRequests(authz -> authz .antMatchers("/user/login" ).anonymous() .anyRequest().authenticated() ) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public AuthenticationManager authenticationManager (AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } }
定义登录接口 1 2 3 4 5 6 7 8 9 10 11 12 @RestController public class LoginController { @Autowired private LoginServcie loginServcie; @PostMapping("/user/login") public ResponseResult login (@RequestBody User user) { return loginServcie.login(user); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class LoginServiceImpl implements LoginService { @Autowired AuthenticationManager authenticationManager; @Autowired RedisCache redisCache; @Override public ResponseResult login (User user) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken (user.getUserName(), user.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); if (Objects.isNull(authenticate)){ throw new RuntimeException ("用户名或密码错误" ); } LoginUser loginUser = (LoginUser)authenticate.getPrincipal(); String userId = loginUser.getUser().getId().toString(); String jwt = JwtUtil.createJWT(userId); redisCache.setCacheObject("login+" +userId,loginUser); HashMap<String,String> map = new HashMap <>(); map.put("token" ,jwt); return new ResponseResult (200 ,"登录成功" ,map); } @Override public ResponseResult logout () { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser)authentication.getPrincipal(); Long userId = loginUser.getUser().getId(); redisCache.deleteObject("login:" +userId); return new ResponseResult (200 ,"注销成功" ); } }
这段代码是 Spring Security + JWT 进行登录认证 的 LoginServiceImpl
实现。它的主要作用是:
验证用户名和密码 ,通过 AuthenticationManager
进行认证。
生成 JWT Token ,用于后续请求的身份认证。
将用户信息存入 Redis ,方便后续的 Token 解析。
返回 JWT Token 给前端 ,前端在后续请求时携带 Token 进行身份认证。
🔍 详细解析代码
1 public class LoginServiceImpl implements LoginService {
这里 LoginServiceImpl
实现 了 LoginService
接口,表示它是一个用户登录的业务逻辑类 。
1️⃣ 注入依赖
1 2 3 4 @Autowired AuthenticationManager authenticationManager; @Autowired RedisCache redisCache;
authenticationManager
:Spring Security 提供的认证管理器,用来校验用户名和密码是否正确 。
redisCache
:用于将用户信息存入 Redis ,以便后续使用。
2️⃣ 处理用户登录
1 2 @Override public ResponseResult login (User user) {
这个方法的作用是: 用户登录 -> 验证身份 -> 生成 Token -> 存入 Redis -> 返回 Token
📌 2.1 构造 AuthenticationToken
进行认证
1 2 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken (user.getUserName(), user.getPassword());
UsernamePasswordAuthenticationToken
是 Spring Security 内置的 用户名+密码认证 令牌。
这个对象封装了用户提交的用户名和密码 ,后续交给 AuthenticationManager
进行认证。
📌 2.2 进行身份认证
1 Authentication authenticate = authenticationManager.authenticate(authenticationToken);
authenticationManager.authenticate(authenticationToken)
负责调用 Spring Security 的认证流程 :
通过 UserDetailsService
查询用户信息 (通常是数据库查询)。
使用 PasswordEncoder
校验密码是否正确 。
如果认证成功,返回一个 Authentication
对象,其中包含了用户的权限信息。
如果 authenticate == null
,说明用户名或密码错误 ,抛出异常:
1 2 3 if (Objects.isNull(authenticate)){ throw new RuntimeException ("用户名或密码错误" ); }
📌 2.3 生成 JWT Token
1 2 3 LoginUser loginUser = (LoginUser) authenticate.getPrincipal();String userId = loginUser.getUser().getId().toString();String jwt = JwtUtil.createJWT(userId);
authenticate.getPrincipal()
返回的是 UserDetails
(我们这里的 LoginUser
)。
userId
取出用户 ID(因为 JWT 需要一个唯一标识)。
JwtUtil.createJWT(userId)
生成 JWT Token,后续请求都会携带这个 Token 进行身份认证。
📌 2.4 存入 Redis
1 redisCache.setCacheObject("login+" +userId, loginUser);
把 LoginUser
(包括用户信息和权限)存入 Redis,键名是 "login+userId"
。
后续用户请求时,系统会用 JWT 中的 userId
去 Redis 查找用户信息,避免每次都查询数据库 。
📌 2.5 返回 Token
1 2 3 HashMap<String,String> map = new HashMap <>(); map.put("token" , jwt); return new ResponseResult (200 , "登录成功" , map);
创建 HashMap<String, String>
存放 token
,返回给前端。
前端拿到 Token 后,后续请求会在 Authorization
头部携带这个 Token 进行身份认证 。
🔎 代码执行流程
前端 发送 POST
请求到 /user/login
,携带用户名和密码。
Spring Security 认证
authenticationManager.authenticate()
调用 UserDetailsService
查询用户信息。
PasswordEncoder
进行密码校验。
认证成功,返回 Authentication
对象。
生成 JWT Token ,用于后续的请求身份认证。
用户信息存入 Redis ,避免每次都查询数据库。
返回 Token 给前端 ,前端后续请求带上 Token。
📝 总结
步骤
说明
1. 认证
authenticationManager.authenticate(authenticationToken)
进行用户认证
2. 生成 JWT
JwtUtil.createJWT(userId)
生成 Token
3. 存入 Redis
redisCache.setCacheObject("login+"+userId, loginUser)
缓存用户信息
4. 返回 Token
返回 JWT 给前端,前端存储并在请求中携带
这段代码的目的是 用 JWT 替代 Session 进行身份认证 ,并结合 Redis 进行缓存 ,提升系统性能。🚀
认证过滤器 我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。
使用userid去redis中获取对应的LoginUser对象。
然后封装Authentication对象存入SecurityContextHolder
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisCache redisCache; @Override protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = request.getHeader("token" ); if (!StringUtils.hasText(token)) { filterChain.doFilter(request, response); return ; } String userId; try { Claims claims = JwtUtil.parseJWT(token); userId = claims.getSubject(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException ("token 非法或已过期" ); } String redisKey = "login:" + userId; LoginUser loginUser = redisCache.getCacheObject(redisKey); if (Objects.isNull(loginUser)) { throw new RuntimeException ("用户未登录" ); } UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken ( loginUser, null , null ); SecurityContextHolder.getContext().setAuthentication(authenticationToken); filterChain.doFilter(request, response); } }
注销 我们只需要定义一个注销接口,然后获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可
1 2 3 4 5 6 7 8 @Override public ResponseResult logout () { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); Long userid = loginUser.getUser().getId(); redisCache.deleteObject("login:" +userid); return new ResponseResult (200 ,"退出成功" ); }
授权 对已经认证(登录)成功的用户,判断他是否有权限访问某些资源或执行某些操作,授权发生在认证之后。Spring Security 会根据用户的权限(Authorities
)和访问的资源,决定是否放行。
🧠 一句话理解授权流程:
用户携带 Token 请求接口 → JWTFilter 提取用户权限 → Security 判断用户是否有权访问 URL(或方法)
🔐 Spring Security 授权的三种常见方式: ✅ 1. 基于 URL 的授权 (控制接口访问权限) 配置在 SecurityFilterChain
里的:
1 2 3 4 http.authorizeHttpRequests() .antMatchers("/admin/**" ).hasRole("ADMIN" ) .antMatchers("/user/**" ).hasAnyAuthority("user:add" , "user:update" ) .anyRequest().authenticated();
常用方法:
方法
说明
.hasAuthority("权限名")
拥有某个具体权限
.hasAnyAuthority(...)
拥有任意一个权限即可
.hasRole("角色名")
拥有某个角色(底层其实是加前缀:ROLE_
)
.hasAnyRole(...)
拥有任意一个角色
.authenticated()
已登录用户可访问
.permitAll()
所有人都可以访问
.anonymous()
匿名用户才能访问(未登录)
✅ 2. 基于方法的授权 (精细控制某个接口或业务方法) 使用注解,在 Controller 或 Service 方法上使用:
启用方法级安全:
然后用注解控制权限:
1 2 3 4 5 @PreAuthorize("hasAuthority('user:add')") @GetMapping("/user/add") public String addUser () { return "添加用户" ; }
常用表达式:
表达式
说明
hasAuthority('xxx')
拥有指定权限
hasRole('ADMIN')
拥有指定角色(会自动加 ROLE_
前缀)
hasAnyAuthority('a','b')
拥有任一权限
#id == authentication.principal.id
当前用户只能操作自己的数据(比如只改自己的资料)
✅ 3. 自定义权限校验逻辑 如果你想写一个更加灵活的权限判断逻辑,可以自定义权限校验组件:
1 2 3 4 5 6 7 8 @Component("myAuth") public class MyAuthorizationService { public boolean checkPermission (Authentication auth, String permission) { return auth.getAuthorities().stream() .anyMatch(a -> a.getAuthority().equals(permission)); } }
然后这样用:
1 @PreAuthorize("@myAuth.checkPermission(authentication, 'user:delete')")
具体实现 限制访问资源所需权限 1 2 3 4 5 6 7 8 9 10 @RestController public class HelloController { @RequestMapping("/hello") @PreAuthorize("hasAuthority('test')") public String hello () { return "hello" ; } }
封装权限信息 封装权限信息到LoginUser
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 @Data @NoArgsConstructor public class LoginUser implements UserDetails { private User user; private List<String> permissions; public LoginUser (User user,List<String> permissions) { this .user = user; this .permissions = permissions; } @JSONField(serialize = false) private List<GrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority > getAuthorities() { if (authorities!=null ){ return authorities; } authorities = permissions.stream(). map(SimpleGrantedAuthority::new ) .collect(Collectors.toList()); return authorities; } @Override public String getPassword () { return user.getPassword(); } @Override public String getUsername () { return user.getUserName(); } @Override public boolean isAccountNonExpired () { return true ; } @Override public boolean isAccountNonLocked () { return true ; } @Override public boolean isCredentialsNonExpired () { return true ; } @Override public boolean isEnabled () { return true ; } }
在UserDetailsServiceImpl中去把权限信息封装到LoginUser中了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper <>(); wrapper.eq(User::getUserName,username); User user = userMapper.selectOne(wrapper); if (Objects.isNull(user)){ throw new RuntimeException ("用户名或密码错误" ); } List<String> list = new ArrayList <>(Arrays.asList("test" )); return new LoginUser (user,list); } }
去JwtAuthenticationTokenFilter类中把用户中的authorities权限封装到UsernamePasswordAuthenticationToken中,然后交给SecurityContextHolder。
1 2 3 4 5 6 UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken (loginUser, null , loginUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
从数据库查询权限信息 RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 @TableName(value="sys_menu") @Data @AllArgsConstructor @NoArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public class Menu implements Serializable { private static final long serialVersionUID = -54979041104113736L ; @TableId private Long id; private String menuName; private String path; private String component; private String visible; private String status; private String perms; private String icon; private Long createBy; private Date createTime; private Long updateBy; private Date updateTime; private Integer delFlag; private String remark; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 create table sys_menu( id bigint auto_increment primary key, menu_name varchar (64 ) default 'NULL' not null comment '菜单名' , path varchar (200 ) null comment '路由地址' , component varchar (255 ) null comment '组件路径' , visible char default '0' null comment '菜单状态(0显示 1隐藏)' , status char default '0' null comment '菜单状态(0正常 1停用)' , perms varchar (100 ) null comment '权限标识' , icon varchar (100 ) default '#' null comment '菜单图标' , create_by bigint null , create_time datetime null , update_by bigint null , update_time datetime null , del_flag int default 0 null comment '是否删除(0未删除 1已删除)' , remark varchar (500 ) null comment '备注' ) comment '菜单表' ;
接口具体实现 mapper层实现
1 2 3 4 public interface MenuMapper extends BaseMapper <Menu> { List<String> selectPermsByUserId (Long id) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.example.mapper.MenuMapper" > <select id ="selectPermsByUserId" resultType ="java.lang.String" > SELECT DISTINCT m.`perms` FROM sys_user_role ur LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id` LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id` LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id` WHERE user_id = #{userid} AND r.`status` = 0 AND m.`status` = 0 </select > </mapper >
完善UserServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Autowired private MenuMapper menuMapper; @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper <>(); wrapper.eq(User::getUserName,username); User user = userMapper.selectOne(wrapper); if (Objects.isNull(user)){ throw new RuntimeException ("用户名或密码错误" ); } List<String> permissionKeyList = menuMapper.selectPermsByUserId(user.getId()); return new LoginUser (user,permissionKeyList); } }
自定义失败处理 异常拦截点:ExceptionTranslationFilter
职责 :在过滤器链中专门捕获认证或授权过程中抛出的异常。
工作流程 :
当认证(Authentication)或授权(Authorization)失败时,相关的过滤器(如 UsernamePasswordAuthenticationFilter
、FilterSecurityInterceptor
)会抛出异常。
ExceptionTranslationFilter
捕获这些异常,判断是认证失败还是授权失败。
根据异常类型分别交给 AuthenticationEntryPoint
或 AccessDeniedHandler
去处理。
认证失败 vs 授权失败
异常类型
场景
默认处理
AuthenticationException
用户未登录、登录凭证(用户名/密码、Token)不合法
调用 AuthenticationEntryPoint.commence()
,默认重定向到登录页面或返回 401
AccessDeniedException
用户已登录,但没有访问某个资源的权限(角色/权限不足)
调用 AccessDeniedHandler.handle()
,默认返回 403 页面或错误响应
自定义处理器 自定义 AuthenticationEntryPoint
当捕获到 AuthenticationException
时会走这里,通常用于“未登录”或“Token 无效”场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence (HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ResponseResult result = new ResponseResult ( HttpStatus.UNAUTHORIZED.value(), "认证失败,请重新登录" ); String json = JSON.toJSONString(result); WebUtils.renderString(response, json); } }
commence()
方法 :
response
:直接往 HTTP 响应里写状态码和 JSON,前端可以根据结构化数据统一处理。
ResponseResult
:你的统一响应对象,包含 code
、msg
、data
等字段。
WebUtils.renderString()
:工具方法,设置响应类型为 application/json
并写入字符串。
自定义 AccessDeniedHandler
当捕获到 AccessDeniedException
时会走这里,通常用于“权限不足”场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle (HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { ResponseResult result = new ResponseResult ( HttpStatus.FORBIDDEN.value(), "权限不足" ); String json = JSON.toJSONString(result); WebUtils.renderString(response, json); } }
handle()
方法 :
同样构造统一的 JSON 响应并写回,状态码使用 403(Forbidden)。
将自定义处理器注入到 Spring Security 在你的 HttpSecurity
配置中,调用 exceptionHandling()
方法将它们注册进去:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Configuration @EnableWebSecurity public class SecurityConfig { @Autowired private AuthenticationEntryPointImpl authenticationEntryPoint; @Autowired private AccessDeniedHandlerImpl accessDeniedHandler; @Bean public SecurityFilterChain securityFilterChain (HttpSecurity http) throws Exception { http .exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler) ; return http.build(); } }
**authenticationEntryPoint(...)
**:指定当出现 AuthenticationException
时调用哪个 Bean。
**accessDeniedHandler(...)
**:指定当出现 AccessDeniedException
时调用哪个 Bean。
跨域 什么是跨域(CORS)?
同源策略 :浏览器出于安全考虑,只有当 协议、域名、端口 三者都相同时,才能正常发起 AJAX 请求。
跨域场景 :前后端分离时,前端通常跑在 http://localhost:3000
,后端在 http://localhost:8080
,端口不同,就属于跨域。
CORS (Cross‑Origin Resource Sharing)机制允许服务器声明:哪些域名、哪些方法、哪些请求头可以访问它的资源。
Spring MVC 层面允许跨域 1 2 3 4 5 6 7 8 9 10 11 12 @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings (CorsRegistry registry) { registry.addMapping("/**" ) .allowedOriginPatterns("*" ) .allowCredentials(true ) .allowedMethods("GET" ,"POST" ,"DELETE" ,"PUT" ) .allowedHeaders("*" ) .maxAge(3600 ); } }
为什么要 MVC 配置? Spring MVC 默认会注册一个 CorsFilter
,它根据上面的规则响应浏览器的 预检请求 (OPTIONS
),并在实际请求中加上相应的 CORS 响应头。
Spring Security 层面允许跨域 即使你在 MVC 层面配置了 CORS,Spring Security 默认也会拦截所有请求(包括 OPTIONS
预检),导致预检被拒绝。 所以在你的安全配置里,还要显式开启 CORS 支持:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Override protected void configure (HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/user/login" ).anonymous() .anyRequest().authenticated(); http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); http.exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); http.cors(); }
http.cors()
它会让 Spring Security 去查找 Spring MVC 中的 CorsConfiguration
(即上面 CorsConfig
定义的规则),并在 SecurityFilterChain 的最前面插入一个 CorsFilter
。
效果 :
预检请求(OPTIONS) 先经过 CORS 过滤器,返回允许的跨域响应头,浏览器才会继续发真正的请求。
实际请求 也会带上 Access-Control-Allow-Origin
、Access-Control-Allow-Credentials
等头,浏览器才允许前端 JS 访问响应内容。
为什么两处都要配置?
MVC 层 :负责 定义 哪些路径、哪些域名、哪些方法可以跨域。
Security 层 :负责 允许 Spring Security 过滤链里也执行这套跨域规则,否则所有跨域请求(包括预检)都会被 Security 拦截成 401/403。
自定义处理器 认证成功处理器 实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候,如果登录成功了是会调用AuthenticationSuccessHandler的方法进行认证成功后的处理的。AuthenticationSuccessHandler就是登录成功处理器。
我们也可以自己去自定义成功处理器进行成功后的相应处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Component public class SGSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess (HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { System.out.println("认证成功了" ); } } @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationSuccessHandler successHandler; @Override protected void configure (HttpSecurity http) throws Exception { http.formLogin().successHandler(successHandler); http.authorizeRequests().anyRequest().authenticated(); } }
认证失败处理器 实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候,如果认证失败了是会调用AuthenticationFailureHandler的方法进行认证失败后的处理的。AuthenticationFailureHandler就是登录失败处理器。
我们也可以自己去自定义失败处理器进行失败后的相应处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Component public class SGFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure (HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { System.out.println("认证失败了" ); } } @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationSuccessHandler successHandler; @Autowired private AuthenticationFailureHandler failureHandler; @Override protected void configure (HttpSecurity http) throws Exception { http.formLogin() .successHandler(successHandler) .failureHandler(failureHandler); http.authorizeRequests().anyRequest().authenticated(); } }
⚠️注意: 这和之前的AuthenticationEntryPoint
两个类虽然都是处理 认证失败 的情况,但它们的作用场景是完全不同的
总结区别:
比较项
AuthenticationEntryPoint
AuthenticationFailureHandler
所在阶段
用户访问受保护资源但未登录
用户登录时输入错误
出发原因
请求接口没带 token、token 无效
登录接口用户名/密码错误
响应方式
通常返回 401(未认证)
通常返回 401(登录失败)或业务自定义状态码
注册位置
http.exceptionHandling().authenticationEntryPoint(...)
登录过滤器 UsernamePasswordAuthenticationFilter
的 setAuthenticationFailureHandler(...)
方法
登出成功处理器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Component public class SGLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess (HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { System.out.println("注销成功" ); } } @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationSuccessHandler successHandler; @Autowired private AuthenticationFailureHandler failureHandler; @Autowired private LogoutSuccessHandler logoutSuccessHandler; @Override protected void configure (HttpSecurity http) throws Exception { http.formLogin() .successHandler(successHandler) .failureHandler(failureHandler); http.logout() .logoutSuccessHandler(logoutSuccessHandler); http.authorizeRequests().anyRequest().authenticated(); } }