百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

全栈安全实战:Spring Boot前后端分离JWT跨域鉴权方案

haoteby 2025-09-18 19:13 3 浏览

全栈安全实战:Spring Boot前后端分离JWT跨域鉴权方案


一、后端实现(Spring Boot)

1. 添加依赖 (pom.xml)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <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>
</dependencies>

2. JWT工具类(支持Access Token & Refresh Token)

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Component
public class JwtTokenUtil {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.access.expiration}")
    private Long accessExpiration;

    @Value("${jwt.refresh.expiration}")
    private Long refreshExpiration;

    private SecretKey getSigningKey() {
        return Keys.hmacShaKeyFor(secret.getBytes());
    }

    // 获取用户名
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    // 获取过期时间
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    // 通用获取Claims
    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    // 生成Access Token
    public String generateAccessToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, userDetails.getUsername(), accessExpiration);
    }

    // 生成Refresh Token
    public String generateRefreshToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return doGenerateToken(claims, userDetails.getUsername(), refreshExpiration);
    }

    private String doGenerateToken(Map<String, Object> claims, String subject, Long expiration) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    // 验证Token
    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

3. JWT认证过滤器

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 javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        final String requestTokenHeader = request.getHeader("Authorization");

        String username = null;
        String jwtToken = null;

        if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
            jwtToken = requestTokenHeader.substring(7);
            try {
                username = jwtTokenUtil.getUsernameFromToken(jwtToken);
            } catch (Exception e) {
                logger.warn("JWT Token已过期或无效");
            }
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
                UsernamePasswordAuthenticationToken authenticationToken = 
                    new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }
        chain.doFilter(request, response);
    }
}

4. Spring Security配置(Spring Boot 2.x / 3.x)

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.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
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 {

    private final UserDetailsService userDetailsService;
    private final JwtRequestFilter jwtRequestFilter;

    public SecurityConfig(UserDetailsService userDetailsService, JwtRequestFilter jwtRequestFilter) {
        this.userDetailsService = userDetailsService;
        this.jwtRequestFilter = jwtRequestFilter;
    }

    @Bean
    public AuthenticationManager authManager(HttpSecurity http) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder())
                .and()
                .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

5. CORS配置(支持前后端分离)

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:3000") // 可改为配置或环境变量
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .exposedHeaders("Authorization")
                .allowCredentials(false)
                .maxAge(3600);
    }
}

6. 认证控制器(支持Access & Refresh Token)

import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final UserDetailsService userDetailsService;
    private final JwtTokenUtil jwtTokenUtil;

    public AuthController(AuthenticationManager authenticationManager, UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil) {
        this.authenticationManager = authenticationManager;
        this.userDetailsService = userDetailsService;
        this.jwtTokenUtil = jwtTokenUtil;
    }

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody AuthRequest request) throws Exception {
        try {
            authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
            );
        } catch (BadCredentialsException e) {
            throw new Exception("用户名或密码错误", e);
        }

        final UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
        final String accessToken = jwtTokenUtil.generateAccessToken(userDetails);
        final String refreshToken = jwtTokenUtil.generateRefreshToken(userDetails);

        return ResponseEntity.ok(new AuthResponse(accessToken, refreshToken));
    }

    @PostMapping("/refresh")
    public ResponseEntity<?> refresh(@RequestBody RefreshRequest request) {
        // 这里可加入Refresh Token验证逻辑
        final String refreshToken = request.getRefreshToken();
        String username = jwtTokenUtil.getUsernameFromToken(refreshToken);
        final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if (jwtTokenUtil.validateToken(refreshToken, userDetails)) {
            final String newAccessToken = jwtTokenUtil.generateAccessToken(userDetails);
            return ResponseEntity.ok(new AuthResponse(newAccessToken, refreshToken));
        }
        return ResponseEntity.status(401).body("Refresh Token无效或过期");
    }

    public static class AuthRequest {
        public String username;
        public String password;
    }

    public static class RefreshRequest {
        public String refreshToken;
    }

    public static class AuthResponse {
        public String accessToken;
        public String refreshToken;

        public AuthResponse(String accessToken, String refreshToken) {
            this.accessToken = accessToken;
            this.refreshToken = refreshToken;
        }
    }
}

7. 受保护API示例

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping("/profile")
    public String getProfile() {
        return "这是用户个人信息(受保护资源)";
    }
}

8. 配置文件 (application.properties)

# JWT配置
jwt.secret=企业级超长随机密钥,256位以上
jwt.access.expiration=900       # 15分钟
jwt.refresh.expiration=604800   # 7天

server.port=8080

二、前端实现(React示例)

1. axios封装 + 拦截器

import axios from 'axios';

const api = axios.create({ baseURL: 'http://localhost:8080/api' });

api.interceptors.request.use(config => {
    const token = localStorage.getItem('accessToken');
    if (token) config.headers.Authorization = `Bearer ${token}`;
    return config;
});

api.interceptors.response.use(
    response => response,
    async error => {
        if (error.response?.status === 401) {
            const refreshToken = localStorage.getItem('refreshToken');
            if (refreshToken) {
                try {
                    const resp = await axios.post('http://localhost:8080/api/auth/refresh', { refreshToken });
                    localStorage.setItem('accessToken', resp.data.accessToken);
                    error.config.headers.Authorization = `Bearer ${resp.data.accessToken}`;
                    return axios(error.config); // 重试请求
                } catch {
                    localStorage.removeItem('accessToken');
                    localStorage.removeItem('refreshToken');
                    window.location.href = '/login';
                }
            } else {
                window.location.href = '/login';
            }
        }
        return Promise.reject(error);
    }
);

export default api;

2. 登录组件示例

import React, { useState } from 'react';
import api from './api';

const Login = () => {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const resp = await api.post('/auth/login', { username, password });
      localStorage.setItem('accessToken', resp.data.accessToken);
      localStorage.setItem('refreshToken', resp.data.refreshToken);
      window.location.href = '/profile';
    } catch {
      setError('登录失败,请检查用户名和密码');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={username} onChange={e => setUsername(e.target.value)} placeholder="用户名" />
      <input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="密码" />
      <button type="submit">登录</button>
      {error && <div>{error}</div>}
    </form>
  );
};

export default Login;

3. 受保护页面示例

import React, { useEffect, useState } from 'react';
import api from './api';

const Profile = () => {
  const [data, setData] = useState('');
  useEffect(() => {
    api.get('/users/profile')
       .then(res => setData(res.data))
       .catch(() => window.location.href = '/login');
  }, []);

  return (
    <div>
      <h2>用户信息</h2>
      <p>{data}</p>
      <button onClick={() => {
        localStorage.clear();
        window.location.href = '/login';
      }}>退出</button>
    </div>
  );
};

export default Profile;

4. React路由示例

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Login from './Login';
import Profile from './Profile';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/login" element={<Login />} />
        <Route path="/profile" element={<Profile />} />
        <Route path="/" element={<Login />} />
      </Routes>
    </Router>
  );
}

export default App;

关键企业级注意事项

  1. Token存储安全:Access Token可存localStorage,Refresh Token建议HttpOnly Cookie。
  2. Token刷新机制:Access Token短时效,Refresh Token长时效。
  3. CORS配置:根据前端部署环境动态配置,生产环境避免 *。
  4. HTTPS:生产环境必须使用HTTPS,确保Token传输安全。
  5. 安全Secret:JWT secret应长随机、从环境变量或配置中心获取。

相关推荐

如何随时清理浏览器缓存_清理浏览器缓存怎么弄

想随时清理浏览器缓存吗?Cookieformac版是Macos上一款浏览器缓存清理工具,所有的浏览器Cookie,本地存储数据,HTML5数据库,FlashCookie,Silverlight,...

Luminati代理动态IP教程指南配置代理VMLogin中文版反指纹浏览器

介绍如何使用在VMLogin中文版设置Luminati代理。首先下载VMLogin中文版反指纹浏览器(https://cn.vmlogin.com)对于刚接触Luminati动态ip的朋友,是不是不懂...

mac清除工具分享,解除您在安全方面的后顾之忧

想要永久的安全的处理掉重要数据,删除是之一,使用今天小编分享的mac清除工具,为您的操作再增一层“保护”,小伙伴慎用哟,一旦使用就不可以恢复咯,来吧一起看看吧~mac清除工具分享,解除您在安全方面的后...

取代cookie的网站追踪技术:”帆布指纹识别”

【前言】一般情况下,网站或者广告联盟都会非常想要一种技术方式可以在网络上精确定位到每一个个体,这样可以通过收集这些个体的数据,通过分析后更加精准的去推送广告(精准化营销)或其他有针对性的一些活动。Co...

辅助上网为啥会被抛弃 曲奇(Cookie)虽甜但有毒

近期有个小新闻,大概很多小伙伴都没有注意到,那就是谷歌Chrome浏览器要弃用Cookie了!说到Cookie功能,很多小伙伴大概觉得不怎么熟悉,有可能还不如前一段时间被弃用的Flash“出名”,但它...

浏览器指纹是什么?浏览器指纹包括哪些信息

本文关键词:浏览器指纹、指纹浏览器、浏览器指纹信息、指纹浏览器原理什么是浏览器指纹?浏览器指纹是指浏览器的各种信息,当我们访问其他网站时,即使是在匿名的模式下,这些信息也可以帮助网站识别我们的身份。...

那些通用清除软件不曾注意的秘密_清理不常用的应用软件

系统清理就像卫生检查前的大扫除,即使你使出吃奶的劲儿把一切可能的地方都打扫过,还会留下边边角角的遗漏。随着大家电脑安全意识的提高,越来越多的朋友开始关注自己的电脑安全,也知道安装360系列软件来"武装...

「网络安全宣传周」这些安全上网小知识你要知道!

小布说:互联网改变了人们的衣食住行,但与之伴生的网络安全威胁也不容忽视。近些年来,风靡全球的勒索病毒、时有发生的电信诈骗、防不胜防的个人信息泄露时时刻刻都威胁着我们的生活。9月18日-24日是第四届...

TypeScript 终极初学者指南_typescript 进阶

在过去的几年里TypeScript变得越来越流行,现在许多工作都要求开发人员了解TypeScript...

jQuery知识一览_jquery的认识和使用

一、概览jQuery官网:https://jquery.com/jQuery是一个高效、轻量并且功能丰富的js库。核心在于查询query。...

我的第一个Electron应用_electronmy

hello,好久不见,最近笔者花了几天时间入门Electron,然后做了一个非常简单的应用,本文就来给各位分享一下过程,Electron大佬请随意~笔者开源了一个Web思维导图,虽然借助showSav...

HTML5 之拖放(Drag 和 Drop)_html拖放api

简介拖放是一种常见的特性,即抓取对象以后拖到另一个位置。在HTML5中,拖放是标准的一部分,任何元素都能够拖放。先点击一个小例子:在用户开始拖动<p>元素时执行JavaScrip...

如何用JavaScript判断输入值是数字还是字母?

在日常开发中,我们有时候需要判断用户输入的是数字还是字母。本文将介绍如何用JavaScript实现这一功能。检查输入值是否是数字或字母...

图形编辑器开发:快捷键的管理_图形编辑工具

大家好,我是前端西瓜哥。...

浏览器原生剪贴板:原来它能这样读取用户截图!

当我们使用GitHub时,会发现Ctrl+V就能直接读取用户剪贴板图片进行粘贴,那么它是如何工作的?安全性如何?...