不依赖OAuth使用JWT是否安全?Spring公开Web应用无OAuth的JWT实现方案咨询
Absolutely, your approach is not only valid but also a very common practice for many REST API scenarios—especially when you want to keep your architecture lightweight and avoid the overhead of a full OAuth2 authorization server.
Here’s why it makes perfect sense:
- HTTPS already encrypts all traffic, so your JWT tokens are safe from eavesdropping during transit.
- JWT’s stateless nature aligns perfectly with REST principles, eliminating the need to store session data on the server.
- Ditching OAuth2 removes unnecessary complexity when you don’t need features like third-party login, complex refresh token flows, or centralized authorization.
This setup is ideal for internal tools, small-to-medium consumer apps, or any scenario where you control the entire authentication flow end-to-end.
Let’s walk through a minimal, clean implementation using Spring Security and JJWT.
1. Add Dependencies
First, include the required dependencies in your pom.xml (if using Maven):
<!-- Spring Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- JJWT (Java JWT) --> <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>
2. JWT Utility Class
Create a helper class to handle token generation and validation:
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.function.Function; @Component public class JwtUtil { @Value("${jwt.secret}") private String secretKey; @Value("${jwt.expiration.ms}") private long jwtExpirationMs; // Extract username from token public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } // Extract expiration date from token public Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { final Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); } private Claims extractAllClaims(String token) { return Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(token) .getBody(); } // Check if token is expired private Boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } // Generate token for user public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); return createToken(claims, userDetails.getUsername()); } private String createToken(Map<String, Object> claims, String subject) { return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); } // Validate token against user details public Boolean validateToken(String token, UserDetails userDetails) { final String username = extractUsername(token); return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } }
Add these properties to application.properties:
jwt.secret=your-strong-secret-key-hide-this-in-production jwt.expiration.ms=86400000 # 1 day (adjust as needed)
3. Custom JWT Authentication Filter
Create a filter that intercepts requests, validates the JWT token, and sets the authentication context:
import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; 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.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Autowired private UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String authorizationHeader = request.getHeader("Authorization"); String username = null; String jwt = null; if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { jwt = authorizationHeader.substring(7); username = jwtUtil.extractUsername(jwt); } // If username is found and no authentication is set in context if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtUtil.validateToken(jwt, userDetails)) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } } filterChain.doFilter(request, response); } }
4. Spring Security Configuration
Configure Spring Security to use your JWT filter instead of HTTP Basic auth, and allow access to the login endpoint:
import org.springframework.beans.factory.annotation.Autowired; 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.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; @Configuration @EnableWebSecurity public class SecurityConfig { @Autowired private JwtAuthenticationFilter jwtAuthenticationFilter; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) // Disable CSRF for REST APIs .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/login").permitAll() // Allow login without auth .anyRequest().authenticated() ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // No sessions, use JWT ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } }
5. Login Controller
Create a simple login endpoint to generate JWT tokens:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; 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; @RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Autowired private JwtUtil jwtUtil; @PostMapping("/login") public String createAuthenticationToken(@RequestBody AuthRequest authRequest) throws Exception { // Authenticate user authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword()) ); // Load user details and generate token final UserDetails userDetails = userDetailsService.loadUserByUsername(authRequest.getUsername()); return jwtUtil.generateToken(userDetails); } // DTO for login request public static class AuthRequest { private String username; private String password; // Getters and setters 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; } } }
6. UserDetailsService Implementation
You’ll need to implement UserDetailsService to load user data from your database (or other source):
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.stereotype.Service; import java.util.ArrayList; @Service public class CustomUserDetailsService implements UserDetailsService { // Replace with your user repository logic @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // Example: Fetch user from DB if ("testuser".equals(username)) { // Password is "password" hashed with BCrypt return new User("testuser", "$2a$10$EixZaY3s7vjR0d6VnXyM/.tKf5gK1L5yK5yK5yK5yK5yK5yK5yK5", new ArrayList<>()); } else { throw new UsernameNotFoundException("User not found with username: " + username); } } }
- Secret Key Management: Never hardcode your secret key. Use environment variables or a secrets manager (like Spring Cloud Config, HashiCorp Vault) in production.
- Token Expiry: Set a shorter expiry time (e.g., 15-60 minutes) and implement a refresh token flow if needed (you can add this without OAuth2 too).
- Token Validation: Always validate the token’s signature and expiry before using it.
- HTTPS Enforcement: Ensure your app is only accessible via HTTPS—this is non-negotiable for JWT security.
内容的提问来源于stack exchange,提问作者Mamut

