一、先搞懂:Spring Security 到底能解决什么问题?
很多刚接触的同学会问:“我自己写个 Token 验证不行吗?为啥要用 Spring Security?”其实 Spring Security 不是 “重复造轮子”,而是帮我们封装了 认证(“你是谁”) 和 授权(“你能做什么”) 的全流程,比如:
- 用户名密码校验、第三方登录(OAuth2.0);
- 接口权限控制(谁能访问
/admin/*,谁只能看/user/*); - 密码加密(BCrypt 不可逆加密,避免明文存储);
- 安全防护(防 CSRF、XSS,限制登录失败次数)。
但 Spring Security 默认依赖 Session 存储状态,在 前后端分离 / 微服务 场景下会有跨域、Session 共享的问题 —— 这时候就需要结合 JWT 实现 “无状态认证”,两者搭配堪称 “黄金组合”!
二、核心概念:3 分钟搞懂关键组件
不用死记硬背,用 “机场安检” 类比一下:
| 组件 | 作用(类比机场安检) | 核心职责 |
|---|---|---|
| Authentication | 旅客的 “身份证 + 机票” | 存储用户身份信息(用户名、角色、权限) |
| AuthenticationManager | 安检负责人 | 主导认证流程,调用 UserDetailsService 查用户 |
| UserDetailsService | 航空公司的 “旅客信息库” | 自定义用户查询逻辑(从数据库 / Redis 查用户) |
| SecurityContext | 安检后的 “临时通行证” 存储处 | 保存当前登录用户的 Authentication 对象 |
| JWT | 可随身携带的 “电子通行证” | 无状态存储用户信息,避免服务端存 Session |
三、实战:Spring Security + JWT 落地步骤(附完整代码)
1. 环境准备:引入依赖
首先创建 Spring Boot 项目,在 pom.xml 中加入核心依赖(JWT 用 JJWT 工具包):
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT 工具包(JJWT) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- 数据库相关(MyBatis + MySQL) -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
2. 第一步:自定义用户认证(UserDetailsService)
Spring Security 默认用 “user / 随机密码” 登录,我们需要改成从 数据库查用户,所以实现 UserDetailsService 接口:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper; // 自己写的Mapper,查用户和角色
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 从数据库查用户(这里根据自己的表结构调整)
User user = userMapper.selectByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在!");
}
// 2. 查用户的角色/权限(比如 ADMIN、USER)
List<String> roles = userMapper.selectRolesByUserId(user.getId());
// 转换为 Spring Security 能识别的权限对象
Collection<? extends GrantedAuthority> authorities =
roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// 3. 返回 UserDetails 对象(包含用户名、加密后的密码、权限)
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(), // 数据库存的是 BCrypt 加密后的密码
authorities
);
}
}
3. 第二步:JWT 工具类(生成 / 验证 Token)
写一个工具类封装 JWT 的生成、解析、验证逻辑,注意 密钥要保密(建议放配置文件):
@Component
public class JwtUtils {
// 从配置文件读取密钥和过期时间(这里简化写死,实际用 @Value 注入)
private static final String SECRET_KEY = "your-secret-key-123456"; // 至少32位
private static final long EXPIRATION_TIME = 24 * 60 * 60 * 1000; // 1天过期
// 1. 生成 JWT Token
public String generateToken(Authentication authentication) {
// 获取当前登录用户信息
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
Date now = new Date();
Date expirationDate = new Date(now.getTime() + EXPIRATION_TIME);
// 构建 Token(包含用户名、过期时间、签名)
return Jwts.builder()
.setSubject(userDetails.getUsername()) // 主题:用户名
.setIssuedAt(now) // 签发时间
.setExpiration(expirationDate) // 过期时间
.signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 签名算法
.compact();
}
// 2. 从 Token 中获取用户名
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
// 3. 验证 Token 是否有效(未过期、签名正确)
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
return true;
} catch (Exception e) {
// Token 过期、签名错误等都会抛异常
return false;
}
}
}
4. 第三步:JWT 过滤器(拦截请求并认证)
前后端分离项目中,前端每次请求会在 Authorization 头带 Token,我们需要写一个过滤器 拦截请求→解析 Token→完成认证:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 1. 从请求头获取 Token(格式:Bearer xxxxxx)
String authorizationHeader = request.getHeader("Authorization");
String token = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
token = authorizationHeader.substring(7); // 去掉 "Bearer " 前缀
}
// 2. 验证 Token 并完成认证
if (token != null && jwtUtils.validateToken(token)) {
// 从 Token 中获取用户名
String username = jwtUtils.getUsernameFromToken(token);
// 查用户信息(含权限)
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 构造 Authentication 对象,存入 SecurityContext
Authentication authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
// 3. 继续执行后续过滤器
filterChain.doFilter(request, response);
}
}
5. 第四步:配置 Spring Security(SecurityConfig)
这是核心配置类,控制 “哪些接口需要认证”“用什么方式认证”:
@Configuration
@EnableWebSecurity // 启用 Spring Security
@EnableMethodSecurity // 启用方法级别的权限控制(@PreAuthorize)
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
private UserDetailsService userDetailsService;
// 密码编码器(BCrypt 加密)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 配置 AuthenticationManager(认证管理器)
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
// 核心配置:接口权限、过滤器顺序
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 1. 关闭 CSRF(前后端分离项目不需要,否则会拦截 POST 请求)
.csrf(csrf -> csrf.disable())
// 2. 配置跨域(解决前端跨域请求问题)
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// 3. 配置接口权限
.authorizeHttpRequests(auth -> auth
// 放行登录接口(不需要认证就能访问)
.requestMatchers("/api/login").permitAll()
// 放行静态资源(如 swagger、前端页面)
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
// 管理员接口:只有 ADMIN 角色能访问
.requestMatchers("/api/admin/**").hasRole("ADMIN")
// 其他接口:需要认证(登录后才能访问)
.anyRequest().authenticated()
)
// 4. 配置登录逻辑(自定义登录接口返回 Token)
.formLogin(form -> form
.loginProcessingUrl("/api/login") // 登录请求路径
.successHandler((request, response, authentication) -> {
// 登录成功:生成 Token 并返回给前端
String token = jwtUtils.generateToken(authentication);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(
Result.success("登录成功", token) // 自定义返回格式
));
})
.failureHandler((request, response, exception) -> {
// 登录失败:返回错误信息
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(
Result.error("登录失败:" + exception.getMessage())
));
})
)
// 5. 添加 JWT 过滤器(在 UsernamePasswordAuthenticationFilter 之前执行)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
// 跨域配置(允许前端域名访问)
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("http://localhost:8080"); // 前端域名
config.addAllowedMethod("*"); // 允许所有请求方法
config.addAllowedHeader("*"); // 允许所有请求头
config.setAllowCredentials(true); // 允许携带 Cookie
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
6. 第五步:方法级权限控制(@PreAuthorize)
除了路径匹配授权,还能在 方法上直接加注解 控制权限,更灵活:
@RestController
@RequestMapping("/api/activity")
public class ActivityController {
// 只有 ADMIN 角色能创建活动
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/create")
public Result createActivity(@RequestBody ActivityDTO activityDTO) {
// 业务逻辑...
return Result.success("活动创建成功");
}
// 登录用户就能查看活动(USER/ADMIN 都可以)
@PreAuthorize("isAuthenticated()")
@GetMapping("/{id}")
public Result getActivityDetail(@PathVariable Long id) {
// 业务逻辑...
return Result.success(activityDetail);
}
}
四、避坑指南:这些问题我踩过!
- 跨域问题:一定要配置
corsConfigurationSource,否则前端请求会被拦截; - Token 过期:可以加 “刷新 Token” 机制(生成一个过期时间更长的 Refresh Token);
- 密码加密:数据库存的必须是 BCrypt 加密后的密码,不能存明文(用
passwordEncoder.encode("123456")加密); - CSRF 关闭:前后端分离项目必须关 CSRF,否则 POST/PUT 请求会被拒绝;
- Token 黑名单:如果需要 “用户登出后立即失效 Token”,可以用 Redis 存黑名单(Token 未过期但已登出)。
五、总结
Spring Security + JWT 的组合,完美解决了前后端分离 / 微服务的认证授权问题:
- Spring Security 负责 “身份校验” 和 “权限控制”,不用自己写重复逻辑;
- JWT 负责 “无状态传递”,不用存 Session,服务端更轻量,分布式部署更简单。
如果你正在做 Java 项目,这套方案可以直接落地!如果有疑问,欢迎在评论区交流~ 后续还会分享 Spring Security 结合 OAuth2.0 实现第三方登录(微信、GitHub),关注我不迷路~
27万+

被折叠的 条评论
为什么被折叠?



