杰克盒子的派对游戏包11绿色免安装版
2.12G · 2025-11-22
作为一名在 Java 开发圈摸爬滚打八年的 “老鸟”,从当年在 SSH 框架里对着 Session 调试到现在扛着微服务权限模块,对 “认证授权” 这事儿的理解早就跳出了 “加个过滤器判断一下” 的初级阶段。尤其是前后端分离成为行业标配后,传统 Session-Cookie 那套玩法越来越难顶 —— 跨域坑、集群 Session 同步坑、CSRF 防护坑,踩过的坑比写过的 BUG 还多。
而现在,SpringSecurity+JWT 几乎成了企业级项目权限方案的 “最优解”:无状态、跨域友好、能细粒度控权,还能配合注解快速落地。今天就从实战角度出发,带大家把这套方案撸通 —— 不搞虚头巴脑的理论堆砌,所有代码都是企业项目里跑过的 “硬货”,新手跟着敲也能直接跑通,老鸟也能 get 到几个避坑点。
八年开发经验告诉我:“选型比编码重要”,先搞懂 “为什么选”,后面写代码才不会盲目。
早年前后端不分离时,Session 存用户信息、Cookie 带 SessionId 的玩法还行,但到了前端部署在localhost:8080、后端在localhost:8081的场景,直接歇菜:
Authorization: Bearer xxx),配合 CORS 配置,跨域直接通;@PreAuthorize这类注解,接口级权限控制一句话的事儿,不用写一堆 if-else;企业项目不追新,稳定第一!选版本时优先挑 “经过市场验证的稳定版”,避免踩新特性的坑:
直接上pom.xml依赖(复制过去就能用):
<!-- SpringWeb (接口基础) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- SpringSecurity (权限核心) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT依赖 (JJWT三件套,别漏了impl和jackson) -->
<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>
<!-- Lombok (省掉getter/setter/构造器) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
整个方案拆成 5 个核心模块,按顺序来,每一步都有 “为什么这么写” 的经验注解:
JWT 的核心操作就三个:生成令牌、解析令牌、验证令牌。这里要注意几个企业级细节:
先在application.yml配 JWT 参数:
jwt:
secret-key: 8a9b0c1d2e3f4g5h6i7j8k9l0m1n2o3p4q5r6s7t8u9v0w1x2 # 自己换个32位以上的复杂密钥
expiration: 3600000 # 令牌过期时间:1小时(单位:毫秒)
再写 JWT 工具类(JwtTokenProvider.java):
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
/**
* JWT工具类:生成/解析/验证令牌
* 八年经验注解:工具类交给Spring托管(单例),参数可配置,异常统一捕获
*/
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
// 从配置文件读密钥和过期时间
@Value("${jwt.secret-key}")
private String secretKey;
@Value("${jwt.expiration}")
private long expiration;
/**
* 生成JWT令牌(核心:用SpringSecurity的Authentication对象拿用户信息)
*/
public String generateToken(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(userDetails.getUsername()) // 存用户名(也能存用户ID,看业务)
.setIssuedAt(now) // 签发时间
.setExpiration(expiryDate) // 过期时间
.signWith(getSecretKey(), SignatureAlgorithm.HS512) // HS512算法,安全度够高
.compact();
}
/**
* 从令牌里拿用户名
*/
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSecretKey())
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
/**
* 验证令牌有效性(两大校验:签名对不对 + 没过期)
*/
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(getSecretKey())
.build()
.parseClaimsJws(token);
return true;
} catch (Exception e) {
// 捕获所有JWT异常(签名错误、过期、格式错等),返回false表示无效
// 实际项目可细分异常类型,返回具体错误信息(如“令牌已过期”)
return false;
}
}
/**
* 把字符串密钥转成SecretKey(JJWT要求的格式,避免新手踩“密钥格式错”的坑)
*/
private SecretKey getSecretKey() {
// Keys.hmacShaKeyFor()会自动校验密钥长度,不够会抛异常
return Keys.hmacShaKeyFor(secretKey.getBytes());
}
}
SpringSecurity 默认从 “内存读用户”(比如inMemoryAuthentication),这在实际项目里就是个摆设 —— 我们得从数据库查用户,还得校验加密后的密码。
这里用 “模拟数据库查询”(实际项目替换成 MyBatis/MyBatis-Plus 调用即可),核心是实现UserDetailsService接口:
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Collections;
/**
* 自定义用户认证服务:从数据库查用户,给SpringSecurity做校验
* 关键:必须实现UserDetailsService,重写loadUserByUsername
*/
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
// SpringSecurity的密码编码器,后面会配置(BCrypt加密)
private final PasswordEncoder passwordEncoder;
/**
* 根据用户名查用户(核心方法,SpringSecurity会自动调这个方法做认证)
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 模拟数据库查询(实际项目替换成:userMapper.selectByUsername(username))
// 这里造两个测试用户:admin(ROLE_ADMIN)、user(ROLE_USER)
if (!"admin".equals(username) && !"user".equals(username)) {
throw new UsernameNotFoundException("用户名不存在:" + username);
}
// 2. 构造用户信息(注意:密码是加密后的!角色必须带ROLE_前缀)
String password;
String role;
if ("admin".equals(username)) {
// 密码原文:123456,用passwordEncoder.encode("123456")生成加密后的字符串
password = "$2a$10$Z8H4k1y7G6F3d2S1a0D9f8A7b6C5e4B3c2D1e0F9g8H7h6G5";
role = "ROLE_ADMIN"; // 管理员角色,必须带ROLE_
} else {
password = "$2a$10$A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2";
role = "ROLE_USER"; // 普通用户角色
}
// 3. 返回SpringSecurity的User对象(封装用户名、加密密码、权限)
return new User(
username,
password, // 数据库存加密后的密码,别存明文!
Collections.singletonList(new SimpleGrantedAuthority(role)) // 权限列表
);
}
}
这一步是整个方案的 “灵魂”,要配置:
直接上配置类(SecurityConfig.java),每一行都有经验注解:
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
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;
/**
* SpringSecurity核心配置
* 注解说明:
* - @EnableWebSecurity:启用Security
* - @EnableGlobalMethodSecurity(prePostEnabled = true):启用方法级权限注解(@PreAuthorize)
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomUserDetailsService customUserDetailsService;
private final JwtTokenProvider jwtTokenProvider;
/**
* 密码编码器:用BCrypt加密(不可逆,企业级首选)
* 八年经验:千万别自己写加密逻辑,用SpringSecurity现成的!
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 认证管理器:处理登录时的用户名密码校验(必须注入,后面登录接口要用)
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
/**
* JWT过滤器:拦截请求,解析令牌,设置认证信息
*/
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter(jwtTokenProvider, customUserDetailsService);
}
/**
* 跨域配置:前后端分离必配,不然前端调接口会被浏览器拦截
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList("*")); // 实际项目替换成前端域名(如http://localhost:8080)
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 允许的请求方法
config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type")); // 允许的请求头(带令牌)
config.setAllowCredentials(true); // 允许前端带Cookie(可选)
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config); // 所有接口都允许跨域
return source;
}
/**
* 核心规则配置:拦截策略、认证流程、Session策略
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 1. 关闭CSRF:前后端分离用JWT,CSRF没用还添乱
.csrf().disable()
// 2. 配置跨域
.cors().configurationSource(corsConfigurationSource()).and()
// 3. Session策略:无状态(JWT不需要Session)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 4. 配置请求拦截规则
.authorizeRequests()
.antMatchers("/api/auth/login").permitAll() // 登录接口放行(不用认证)
.antMatchers("/static/**", "/swagger-ui/**").permitAll() // 静态资源、Swagger放行
.anyRequest().authenticated(); // 其他所有接口必须认证
// 5. 加JWT过滤器:在UsernamePasswordAuthenticationFilter之前执行(先验令牌)
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
这个过滤器的作用是:拦截所有需要认证的请求,从请求头拿 JWT 令牌,验证通过后把用户信息塞进 SpringSecurity 上下文(SecurityContext) —— 这样后面的权限注解才能 “认得到” 用户。
代码实现(JwtAuthenticationFilter.java):
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* JWT认证过滤器:每次请求都拦截,验令牌、设认证信息
* 继承OncePerRequestFilter:确保每次请求只过滤一次
*/
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final UserDetailsService userDetailsService;
/**
* 核心过滤逻辑
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
// 1. 从请求头获取JWT令牌(格式:Authorization: Bearer xxx)
String jwt = getJwtFromRequest(request);
// 2. 验证令牌有效性
if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
// 3. 从令牌里拿用户名,再查用户详情
String username = jwtTokenProvider.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 4. 构造认证令牌,塞进SecurityContext
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 5. 设置认证信息(后面的权限判断会用到)
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
// 令牌验证失败,不设认证信息,后续会返回401
logger.error("无法设置用户认证信息:", e);
}
// 6. 继续走过滤链(不管有没有认证,都要继续往下走)
filterChain.doFilter(request, response);
}
/**
* 从请求头提取JWT令牌(处理Bearer前缀)
*/
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
// 去掉“Bearer ”前缀,拿到纯令牌
return bearerToken.substring(7);
}
return null;
}
}
到这里,基础框架搭好了,接下来写两个核心:登录接口(返回 JWT) 和 权限注解(控制接口访问) 。
AuthController.java)登录接口是唯一 “不用认证” 的接口,逻辑是:接收用户名密码 → 用 AuthenticationManager 认证 → 认证成功生成 JWT 返回。
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
/**
* 认证控制器:登录接口
*/
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
/**
* 登录接口:接收用户名密码,返回JWT令牌
*/
@PostMapping("/login")
public ResponseEntity<?> login(@Valid @RequestBody LoginRequest loginRequest) {
// 1. 构造认证请求(用户名+密码)
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
// 2. 把认证信息塞进SecurityContext(虽然登录后用JWT,但这里是标准流程)
SecurityContextHolder.getContext().setAuthentication(authentication);
// 3. 生成JWT令牌
String jwt = jwtTokenProvider.generateToken(authentication);
// 4. 返回令牌(前后端分离常用JSON格式)
return new ResponseEntity<>(new JwtResponse(jwt), HttpStatus.OK);
}
// 登录请求DTO(接收前端参数)
public static class LoginRequest {
private String username;
private String password;
// getter/setter(Lombok的@Data也能省,这里为了清晰写出来)
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
// 登录响应DTO(返回JWT)
public static class JwtResponse {
private String token;
public JwtResponse(String token) { this.token = token; }
public String getToken() { return token; }
public void setToken(String token) { this.token = token; }
}
}
TestController.java)用@PreAuthorize注解实现细粒度权限控制,比如 “只有 ADMIN 能访问”“只要有某权限就能访问”。
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("/api/test")
public class TestController {
/**
* 所有认证用户都能访问(只要登录了就行)
*/
@GetMapping("/all")
public String allUsers() {
return "所有登录用户都能看到这条信息";
}
/**
* 只有ROLE_USER角色能访问(普通用户)
*/
@PreAuthorize("hasRole('USER')")
@GetMapping("/user")
public String userOnly() {
return "只有普通用户能看到这条信息";
}
/**
* 只有ROLE_ADMIN角色能访问(管理员)
*/
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin")
public String adminOnly() {
return "只有管理员能看到这条信息";
}
/**
* 复杂权限:ADMIN角色 或 有VIEW_INFO权限的用户(实际项目可扩展)
*/
@PreAuthorize("hasRole('ADMIN') or hasAuthority('VIEW_INFO')")
@GetMapping("/complex")
public String complexAuth() {
return "管理员或有VIEW_INFO权限的用户能看到这条信息";
}
}
用 Postman 或 Apifox 测试,步骤超简单:
登录获取 JWT:
请求地址:POST http://localhost:8080/api/auth/login
请求体(JSON):
{
"username": "admin",
"password": "123456"
}
响应会返回token:eyJhbGciOiJIUzUxMiJ9...(很长的字符串)
访问需要认证的接口:
GET http://localhost:8080/api/test/adminAuthorization: Bearer eyJhbGciOiJIUzUxMiJ9...(把上面的 token 填进去)@EnableGlobalMethodSecurity(prePostEnabled = true),不然@PreAuthorize注解没用;AllowedOrigins在生产环境别用*,要指定具体的前端域名(如http://www.xxx.com),避免安全风险;这套方案我在至少 5 个企业项目里落地过,从单体应用到微服务都能用,稳定性没问题。其实 SpringSecurity+JWT 不难,重点是理解 “认证流程” 和 “权限传递” 的逻辑,而不是死记代码。
2.12G · 2025-11-22
236.48MB · 2025-11-22
5.76G · 2025-11-22