Appearance
Spring Boot 4
Spring Security 7
Authentication
方案一:JWT + 自定义过滤器
1. 添加依赖
在pom.xml(Maven)中添加jjwt等JWT库,用于生成和解析Token。
xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.13.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>- JWT 工具类
java
package study.helloworld.springsecuritydemo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import java.time.Instant;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.crypto.SecretKey;
/** JJWT 工具类 使用 HS256 算法,支持自定义 claims,自动处理过期验证。 */
public class JwtUtils {
private static final SecretKey SECRET_KEY = Jwts.SIG.HS256.key().build();
private static final long DEFAULT_EXPIRE_SECONDS = 3600L; // 默认1小时
/**
* 生成 token(默认过期时间)
*
* @param subject 用户标识(如 userId)
* @param claims 自定义 claims(可为 null)
* @return JWT 字符串
*/
public static String generateToken(String subject, Map<String, Object> claims) {
return generateToken(subject, claims, DEFAULT_EXPIRE_SECONDS);
}
/**
* 生成 token(自定义过期时间)
*
* @param subject 用户标识
* @param claims 自定义 claims(可为 null)
* @param expireSeconds 过期秒数
* @return JWT 字符串
*/
public static String generateToken(
String subject, Map<String, Object> claims, long expireSeconds) {
Instant now = Instant.now();
Instant expiration = now.plusSeconds(expireSeconds);
var builder =
Jwts.builder()
.subject(subject)
.issuedAt(Date.from(now))
.expiration(Date.from(expiration))
.signWith(SECRET_KEY, Jwts.SIG.HS256);
if (claims != null && !claims.isEmpty()) {
builder.claims(claims);
}
return builder.compact();
}
/**
* 解析 token 并获取 Claims
*
* @param token JWT 字符串
* @return Claims
*/
private static Claims parseToken(String token) {
Claims claims = null;
try {
Jws<Claims> jws = Jwts.parser().verifyWith(SECRET_KEY).build().parseSignedClaims(token);
claims = jws.getPayload();
} catch (JwtException | IllegalArgumentException e) {
e.printStackTrace();
}
return claims;
}
/**
* 验证 token 是否有效(签名正确且未过期)
*
* @param token JWT 字符串
* @return true 有效,false 无效
*/
public static boolean validateToken(String token) {
return parseToken(token) != null;
}
/**
* 从 token 中提取 subject(会验证签名和过期时间)
*
* @param token JWT 字符串
* @return subject
*/
public static String getSubject(String token) {
Claims claims = parseToken(token);
return claims != null ? claims.getSubject() : null;
}
// =================== 示例用法 ===================
public static void main(String[] args) {
// 1. 生成 token
Map<String, Object> customClaims = new ConcurrentHashMap<>();
customClaims.put("role", "admin");
customClaims.put("email", "[email protected]");
String token = generateToken("user123", customClaims, 7200L); // 2小时过期
System.out.println("生成的 Token: " + token);
// 2. 验证 token
boolean valid = validateToken(token);
System.out.println("验证结果: " + valid);
// 3. 解析 token
System.out.println("Subject: " + getSubject(token));
}
}2. 编写JWT认证过滤器
创建JwtAuthenticationFilter(继承OncePerRequestFilter),它会在每个请求到达Controller前被调用,负责:
- 从HTTP请求的
Authorization: Bearer <token>头中提取Token。 - 验证Token的签名和有效期。
- 如果Token有效,解析出用户信息(如用户名、权限),创建一个
UsernamePasswordAuthenticationToken并设置到SecurityContextHolder中,代表当前用户已通过认证。
java
package study.helloworld.springsecuritydemo;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 从请求头中获取令牌
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
String token = authorizationHeader.substring(7);
// 校验并解析令牌
if (JwtUtils.validateToken(token)) {
String subject = JwtUtils.getSubject(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(subject);
// 若有效,构建 Authentication 并存入 SecurityContext
Authentication authentication =
new UsernamePasswordAuthenticationToken(subject, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}3. 配置示例(SecurityConfig)
java
package study.helloworld.springsecuritydemo;
import jakarta.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
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.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import tools.jackson.databind.ObjectMapper;
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(
HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception {
http.csrf(AbstractHttpConfigurer::disable);
http.formLogin(AbstractHttpConfigurer::disable);
http.httpBasic(AbstractHttpConfigurer::disable);
http.logout(AbstractHttpConfigurer::disable);
http.sessionManagement(
sessionManagement ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.authorizeHttpRequests(
authorize ->
authorize.requestMatchers("/auth/**").permitAll().anyRequest().authenticated());
http.exceptionHandling(
exception ->
exception
.authenticationEntryPoint(
(request, response, authException) -> {
// 设置响应状态码为 401 Unauthorized
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
// 设置响应内容类型为 JSON
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
// 构建 JSON 响应体(可以使用任意 JSON 库,这里以手动拼接为例)
Map<String, Object> body = new HashMap<>();
body.put("code", HttpServletResponse.SC_UNAUTHORIZED);
body.put("message", "未认证,请先登录");
body.put("timestamp", System.currentTimeMillis());
ObjectMapper mapper = new ObjectMapper(); // 也可以用注入的 ObjectMapper
response.getWriter().write(mapper.writeValueAsString(body));
})
.accessDeniedHandler(
(request, response, accessDeniedException) -> {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
// 设置响应内容类型为 JSON
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
// 构建 JSON 响应体(可以使用任意 JSON 库,这里以手动拼接为例)
Map<String, Object> body = new HashMap<>();
body.put("code", HttpServletResponse.SC_FORBIDDEN);
body.put("message", "未授权,请先授权");
body.put("timestamp", System.currentTimeMillis());
ObjectMapper mapper = new ObjectMapper(); // 也可以用注入的 ObjectMapper
response.getWriter().write(mapper.writeValueAsString(body));
}));
http.addFilterAt(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder,
AuthenticationEventPublisher authenticationEventPublisher) {
DaoAuthenticationProvider authenticationProvider =
new DaoAuthenticationProvider(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder);
ProviderManager providerManager = new ProviderManager(authenticationProvider);
providerManager.setAuthenticationEventPublisher(authenticationEventPublisher);
return providerManager;
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails =
User.withUsername("user").password("{noop}password").roles("USER").build();
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
public AuthenticationEventPublisher authenticationEventPublisher(
ApplicationEventPublisher applicationEventPublisher) {
return new DefaultAuthenticationEventPublisher(applicationEventPublisher);
}
}4. 登录接口的实现
创建一个AuthController,提供一个/auth/jwt/login接口。该接口接收用户名/密码,验证成功后生成JWT并返回给客户端。
java
package study.helloworld.springsecuritydemo.controller;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
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.AuthenticationException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import study.helloworld.springsecuritydemo.JwtUtils;
@RestController()
@RequestMapping("/auth")
public class AuthController {
@Autowired private AuthenticationManager authenticationManager;
@RequestMapping("/jwt/login")
public ResponseEntity<Map<String, Object>> jwtLogin(LoginRequest loginRequest)
throws IOException {
Map<String, Object> map = new HashMap<>();
try {
Authentication authenticationRequest =
UsernamePasswordAuthenticationToken.unauthenticated(
loginRequest.username(), loginRequest.password());
Authentication authenticationResponse =
this.authenticationManager.authenticate(authenticationRequest);
String token = JwtUtils.generateToken(authenticationResponse.getName(), null);
map.put("token", token);
} catch (AuthenticationException e) {
e.printStackTrace();
}
return ResponseEntity.ok(map);
}
public record LoginRequest(String username, String password) {}
}方案二:OAuth2资源服务器
1. 添加依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security-oauth2-resource-server</artifactId>
</dependency>2. 配置示例(JwtConfig)
java
package com.howxuexi.springsecurityoauth2resourceserverdemo.config;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.stereotype.Component;
/** JWT 工具类 使用 HS256 算法,支持自定义 claims,自动处理过期验证。 */
@Component
public class JwtConfig {
public static final SecretKey SECRET_KEY;
private static final long DEFAULT_EXPIRE_SECONDS = 3600L; // 默认1小时
static {
byte[] secret =
"UibraTiIZ7OgBJGUkv6KfVoKNOoOElHLfHuFcjZiA64MFh47MzpTVfLGBqSqd5if5mXkUVhFvkFyva8VSOapudtRC5FkmGF8aCmxF5C6uiYUYxGRGTHBgkqRsDVUwWxQ4Nn5IcWUFNva2hp2yxJ8GJnzt3nHJYxvjPfumnxM42EIyvEq13T7QFYmr5VXBSIgxA64uuKWLWAAYB2iz05pDG9L0dZeoeTFvnfRiPHyd5IIER4vrzLbfTf5HhWbCmKV"
.getBytes();
SECRET_KEY = new SecretKeySpec(secret, "HmacSHA256");
}
@Autowired private JwtEncoder jwtEncoder;
/**
* 生成 token(默认过期时间)
*
* @param subject 用户标识(如 userId)
* @param claims 自定义 claims(可为 null)
* @return JWT 字符串
*/
public String generateToken(String subject, Map<String, Object> claims) {
return generateToken(subject, claims, DEFAULT_EXPIRE_SECONDS);
}
/**
* 生成 token(自定义过期时间)
*
* @param subject 用户标识
* @param claims 自定义 claims(可为 null)
* @param expireSeconds 过期秒数
* @return JWT 字符串
*/
public String generateToken(String subject, Map<String, Object> claims, long expireSeconds) {
Instant now = Instant.now();
Instant expiration = now.plusSeconds(expireSeconds);
var builder =
JwtClaimsSet.builder()
.issuer("http://localhost:9000") // 签发者(iss)
.issuedAt(now) // 签发时间(iat)
.expiresAt(expiration) // 过期时间(exp)
.subject(subject); // 主题 = 用户名(sub)
if (claims != null && !claims.isEmpty()) {
builder.claims(claims1 -> Optional.ofNullable(claims).ifPresent(_ -> claims1.putAll(claims)));
}
return jwtEncoder
.encode(
JwtEncoderParameters.from(JwsHeader.with(MacAlgorithm.HS256).build(), builder.build()))
.getTokenValue();
}
}3. 配置示例(SecurityConfig)
java
package com.howxuexi.springsecurityoauth2resourceserverdemo.config;
import com.nimbusds.jose.jwk.source.ImmutableSecret;
import jakarta.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
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.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import tools.jackson.databind.ObjectMapper;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable);
http.formLogin(AbstractHttpConfigurer::disable);
http.httpBasic(AbstractHttpConfigurer::disable);
http.logout(AbstractHttpConfigurer::disable);
http.sessionManagement(
sessionManagement ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// 仅对 /api/** 路径生效,不影响其他路径(如静态资源、actuator 等)
http.securityMatcher("/api/**");
http.authorizeHttpRequests(
auth ->
auth
// 登录接口不需要认证
.requestMatchers("/api/auth/login")
.permitAll()
// 其他所有 /api/** 请求必须携带有效 JWT
.anyRequest()
.authenticated())
// 启用 OAuth2 Resource Server,使用 JWT 方式校验 token
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
http.exceptionHandling(
exception ->
exception
.authenticationEntryPoint(
(request, response, authException) -> {
// 设置响应状态码为 401 Unauthorized
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
// 设置响应内容类型为 JSON
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
// 构建 JSON 响应体(可以使用任意 JSON 库,这里以手动拼接为例)
Map<String, Object> body = new HashMap<>();
body.put("code", HttpServletResponse.SC_UNAUTHORIZED);
body.put("message", "未认证,请先登录");
body.put("timestamp", System.currentTimeMillis());
ObjectMapper mapper = new ObjectMapper(); // 也可以用注入的 ObjectMapper
response.getWriter().write(mapper.writeValueAsString(body));
})
.accessDeniedHandler(
(request, response, accessDeniedException) -> {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
// 设置响应内容类型为 JSON
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
// 构建 JSON 响应体(可以使用任意 JSON 库,这里以手动拼接为例)
Map<String, Object> body = new HashMap<>();
body.put("code", HttpServletResponse.SC_FORBIDDEN);
body.put("message", "未授权,请先授权");
body.put("timestamp", System.currentTimeMillis());
ObjectMapper mapper = new ObjectMapper(); // 也可以用注入的 ObjectMapper
response.getWriter().write(mapper.writeValueAsString(body));
}));
return http.build();
}
@Bean
public JwtEncoder jwtEncoder() {
return new NimbusJwtEncoder(new ImmutableSecret<>(JwtConfig.SECRET_KEY));
}
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withSecretKey(JwtConfig.SECRET_KEY).build();
}
@Bean
public AuthenticationManager authenticationManager(
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder,
AuthenticationEventPublisher authenticationEventPublisher) {
DaoAuthenticationProvider authenticationProvider =
new DaoAuthenticationProvider(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder);
ProviderManager providerManager = new ProviderManager(authenticationProvider);
providerManager.setAuthenticationEventPublisher(authenticationEventPublisher);
return providerManager;
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails =
User.withUsername("user").password("{noop}password").roles("USER").build();
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}4. 登录接口的实现
创建一个AuthController,提供一个/api/auth/login接口。该接口接收用户名/密码,验证成功后生成JWT并返回给客户端。
java
package com.howxuexi.springsecurityoauth2resourceserverdemo.controller;
import com.howxuexi.springsecurityoauth2resourceserverdemo.config.JwtConfig;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
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.AuthenticationException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController()
@RequestMapping("/api/auth")
public class AuthController {
@Autowired private AuthenticationManager authenticationManager;
@Autowired
private JwtConfig jwtConfig;
@RequestMapping("/login")
public ResponseEntity<Map<String, Object>> login(LoginRequest loginRequest) {
Map<String, Object> map = new HashMap<>();
try {
Authentication authenticationRequest =
UsernamePasswordAuthenticationToken.unauthenticated(
loginRequest.username(), loginRequest.password());
Authentication authenticationResponse =
this.authenticationManager.authenticate(authenticationRequest);
String token = jwtConfig.generateToken(authenticationResponse.getName(), null);
map.put("token", token);
} catch (AuthenticationException e) {
e.printStackTrace();
}
return ResponseEntity.ok(map);
}
public record LoginRequest(String username, String password) {}
}