前后端 Token 处理与登录机制详解

前后端 Token 处理与登录机制详解

目录

  • 一、登录机制概述
  • 二、Token 基础知识
  • 三、前端 Token 处理
  • 四、后端 Token 处理
  • 五、常见登录流程实现
  • 六、Token 安全与最佳实践
  • 七、常见问题与解决方案
  • 八、总结

一、登录机制概述

1.1 登录机制的重要性

在现代 Web 应用中,登录机制是用户身份验证和授权的基础,它确保了只有经过验证的用户才能访问受保护的资源。一个安全、高效的登录机制对于保护用户数据和系统安全至关重要。

1.2 常见的登录方式

  1. 基于 Session 的登录:服务器创建会话并存储用户信息,客户端通过 Cookie 存储会话 ID。
  2. 基于 Token 的登录:服务器生成 Token 并返回给客户端,客户端在后续请求中携带 Token。
  3. OAuth 2.0 授权:允许第三方应用代表用户访问资源。
  4. 单点登录 (SSO):用户只需登录一次,即可访问多个相关但独立的系统。

1.3 为什么选择基于 Token 的登录

  • 无状态:服务器不需要存储会话信息,减轻服务器负担。
  • 可扩展性:适合分布式系统和微服务架构。
  • 跨域支持:可以在不同域名下使用。
  • 移动应用友好:适合移动应用和单页应用 (SPA)。

二、Token 基础知识

2.1 Token 的类型

  1. JWT (JSON Web Token):一种基于 JSON 的开放标准,用于创建访问令牌。
  2. Opaque Token:不透明的令牌,服务器需要查询数据库验证。
  3. Access Token:用于访问资源的短期令牌。
  4. Refresh Token:用于获取新的 Access Token 的长期令牌。

2.2 JWT 结构

JWT 由三部分组成,用点 (.) 分隔:

  1. Header (头部):包含令牌类型和使用的算法。

    {
      "alg": "HS256",
      "typ": "JWT"
    }
    
  2. Payload (负载):包含声明(claims),如用户 ID、角色、过期时间等。

    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022,
      "exp": 1516242622
    }
    
  3. Signature (签名):用于验证令牌的真实性。

    HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      secret
    )
    

2.3 Token 的生命周期

  1. 创建:用户登录成功后,服务器生成 Token。
  2. 传输:Token 从服务器传输到客户端。
  3. 存储:客户端存储 Token。
  4. 使用:客户端在请求中携带 Token。
  5. 验证:服务器验证 Token 的有效性。
  6. 过期:Token 达到过期时间后失效。
  7. 刷新:使用 Refresh Token 获取新的 Access Token。

三、前端 Token 处理

3.1 Token 的获取

前端通常在用户登录成功后从服务器响应中获取 Token:

// 使用 fetch API 登录
async function login(username, password) {
  try {
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ username, password })
    });
    
    if (!response.ok) {
      throw new Error('登录失败');
    }
    
    const data = await response.json();
    // 保存 token
    localStorage.setItem('token', data.token);
    // 如果有 refresh token,也保存
    if (data.refreshToken) {
      localStorage.setItem('refreshToken', data.refreshToken);
    }
    
    return data;
  } catch (error) {
    console.error('登录错误:', error);
    throw error;
  }
}

3.2 Token 的存储

前端有多种存储 Token 的方式,各有优缺点:

3.2.1 localStorage
// 存储
localStorage.setItem('token', token);

// 获取
const token = localStorage.getItem('token');

// 删除
localStorage.removeItem('token');

优点

  • 简单易用
  • 持久化存储,页面刷新后仍然存在
  • 可以存储较大数据

缺点

  • 容易受到 XSS 攻击
  • 无法设置过期时间
  • 同源策略限制
3.2.2 sessionStorage
// 存储
sessionStorage.setItem('token', token);

// 获取
const token = sessionStorage.getItem('token');

// 删除
sessionStorage.removeItem('token');

优点

  • 页面关闭后自动清除
  • 简单易用

缺点

  • 容易受到 XSS 攻击
  • 同源策略限制
3.2.3 Cookie
// 设置 cookie
document.cookie = `token=${token}; path=/; secure; samesite=strict`;

// 获取 cookie
function getCookie(name) {
  const value = `; ${document.cookie}`;
  const parts = value.split(`; ${name}=`);
  if (parts.length === 2) return parts.pop().split(';').shift();
  return null;
}

const token = getCookie('token');

// 删除 cookie
document.cookie = 'token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';

优点

  • 可以设置 HttpOnly 标志防止 XSS 攻击
  • 可以设置 Secure 标志确保只通过 HTTPS 传输
  • 可以设置 SameSite 属性防止 CSRF 攻击
  • 可以设置过期时间

缺点

  • 每次请求都会发送,增加请求大小
  • 存储空间有限
3.2.4 内存变量(React/Vue 状态管理)
// React 示例
const [token, setToken] = useState(null);

// 存储
setToken(tokenValue);

// 使用
if (token) {
  // 使用 token
}

// Vue 示例 (Vuex)
// 存储
this.$store.commit('setToken', tokenValue);

// 获取
const token = this.$store.state.token;

优点

  • 页面刷新后丢失,安全性较高
  • 不依赖浏览器存储机制

缺点

  • 页面刷新后需要重新登录
  • 不适合需要持久化的场景

3.3 Token 的使用

前端在发送请求时需要在请求头中携带 Token:

// 使用 fetch API 发送请求
async function fetchData() {
  const token = localStorage.getItem('token');
  
  try {
    const response = await fetch('/api/data', {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });
    
    if (response.status === 401) {
      // Token 过期或无效,尝试刷新
      const newToken = await refreshToken();
      if (newToken) {
        // 使用新 token 重试请求
        return fetchData();
      } else {
        // 刷新失败,重定向到登录页
        window.location.href = '/login';
      }
    }
    
    return await response.json();
  } catch (error) {
    console.error('请求错误:', error);
    throw error;
  }
}

3.4 Token 的刷新

当 Access Token 过期时,前端可以使用 Refresh Token 获取新的 Access Token:

async function refreshToken() {
  const refreshToken = localStorage.getItem('refreshToken');
  
  if (!refreshToken) {
    return null;
  }
  
  try {
    const response = await fetch('/api/refresh', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ refreshToken })
    });
    
    if (!response.ok) {
      // 刷新失败,清除所有 token
      localStorage.removeItem('token');
      localStorage.removeItem('refreshToken');
      return null;
    }
    
    const data = await response.json();
    // 保存新的 token
    localStorage.setItem('token', data.token);
    if (data.refreshToken) {
      localStorage.setItem('refreshToken', data.refreshToken);
    }
    
    return data.token;
  } catch (error) {
    console.error('刷新 token 错误:', error);
    return null;
  }
}

3.5 拦截器实现

使用 Axios 等 HTTP 客户端库可以实现请求拦截器,自动添加 Token 和处理 Token 过期:

// Axios 拦截器示例
import axios from 'axios';

// 创建 axios 实例
const api = axios.create({
  baseURL: '/api'
});

// 请求拦截器
api.interceptors.request.use(
  config => {
    const token = localStorage.getItem('token');
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
    return config;
  },
  error => {
    return Promise.reject(error);
  }
);

// 响应拦截器
api.interceptors.response.use(
  response => {
    return response;
  },
  async error => {
    const originalRequest = error.config;
    
    // 如果是 401 错误且不是刷新 token 的请求
    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      
      try {
        // 尝试刷新 token
        const newToken = await refreshToken();
        if (newToken) {
          // 更新请求头
          originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
          // 重试请求
          return api(originalRequest);
        }
      } catch (refreshError) {
        // 刷新失败,重定向到登录页
        window.location.href = '/login';
        return Promise.reject(refreshError);
      }
    }
    
    return Promise.reject(error);
  }
);

四、后端 Token 处理

4.1 Token 的生成

后端在用户登录成功后生成 Token:

// Java 示例 (使用 JJWT 库)
public String generateToken(UserDetails userDetails) {
    Map<String, Object> claims = new HashMap<>();
    claims.put("sub", userDetails.getUsername());
    claims.put("roles", userDetails.getAuthorities());
    claims.put("created", new Date());
    
    return Jwts.builder()
            .setClaims(claims)
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1小时过期
            .signWith(SignatureAlgorithm.HS256, secretKey)
            .compact();
}

// 生成 Refresh Token
public String generateRefreshToken(UserDetails userDetails) {
    return Jwts.builder()
            .setSubject(userDetails.getUsername())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + 86400000 * 7)) // 7天过期
            .signWith(SignatureAlgorithm.HS256, refreshSecretKey)
            .compact();
}

4.2 Token 的验证

后端需要验证客户端请求中携带的 Token:

// Java 示例 (使用 JJWT 库)
public boolean validateToken(String token) {
    try {
        Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
        return true;
    } catch (SignatureException e) {
        // 签名不匹配
        return false;
    } catch (MalformedJwtException e) {
        // JWT 格式错误
        return false;
    } catch (ExpiredJwtException e) {
        // Token 已过期
        return false;
    } catch (UnsupportedJwtException e) {
        // 不支持的 JWT
        return false;
    } catch (IllegalArgumentException e) {
        // 参数错误
        return false;
    }
}

// 从 Token 中获取用户名
public String getUsernameFromToken(String token) {
    Claims claims = Jwts.parser()
            .setSigningKey(secretKey)
            .parseClaimsJws(token)
            .getBody();
    
    return claims.getSubject();
}

4.3 Token 的刷新

后端处理 Token 刷新请求:

// Java 示例
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest request) {
    String refreshToken = request.getRefreshToken();
    
    try {
        // 验证 refresh token
        if (!validateRefreshToken(refreshToken)) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid refresh token");
        }
        
        // 从 refresh token 中获取用户名
        String username = getUsernameFromRefreshToken(refreshToken);
        
        // 获取用户详情
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        
        // 生成新的 access token
        String newToken = generateToken(userDetails);
        
        // 可选:生成新的 refresh token
        String newRefreshToken = generateRefreshToken(userDetails);
        
        // 返回新的 token
        Map<String, String> response = new HashMap<>();
        response.put("token", newToken);
        response.put("refreshToken", newRefreshToken);
        
        return ResponseEntity.ok(response);
    } catch (Exception e) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Error refreshing token");
    }
}

4.4 权限控制

后端使用 Token 中的信息进行权限控制:

// Spring Security 配置示例
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests()
                .antMatchers("/api/auth/**").permitAll()
                .antMatchers("/api/public/**").permitAll()
                .antMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

// JWT 认证过滤器
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtTokenProvider tokenProvider;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            String jwt = getJwtFromRequest(request);
            
            if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
                String username = tokenProvider.getUsernameFromToken(jwt);
                
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception ex) {
            logger.error("Could not set user authentication in security context", ex);
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

五、常见登录流程实现

5.1 用户名密码登录

5.1.1 前端实现
// 登录表单提交
async function handleLogin(event) {
  event.preventDefault();
  
  const username = document.getElementById('username').value;
  const password = document.getElementById('password').value;
  
  try {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ username, password })
    });
    
    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(errorData.message || '登录失败');
    }
    
    const data = await response.json();
    
    // 保存 token
    localStorage.setItem('token', data.token);
    if (data.refreshToken) {
      localStorage.setItem('refreshToken', data.refreshToken);
    }
    
    // 重定向到首页或之前的页面
    window.location.href = '/dashboard';
  } catch (error) {
    // 显示错误信息
    document.getElementById('error-message').textContent = error.message;
  }
}
5.1.2 后端实现
// 登录控制器
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private JwtTokenProvider tokenProvider;
    
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
        try {
            // 验证用户名和密码
            Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    loginRequest.getUsername(),
                    loginRequest.getPassword()
                )
            );
            
            // 设置安全上下文
            SecurityContextHolder.getContext().setAuthentication(authentication);
            
            // 生成 token
            String token = tokenProvider.generateToken(authentication);
            String refreshToken = tokenProvider.generateRefreshToken(authentication);
            
            // 返回 token
            Map<String, String> response = new HashMap<>();
            response.put("token", token);
            response.put("refreshToken", refreshToken);
            
            return ResponseEntity.ok(response);
        } catch (AuthenticationException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body("Invalid username or password");
        }
    }
}

5.2 手机验证码登录

5.2.1 前端实现
// 发送验证码
async function sendVerificationCode() {
  const phone = document.getElementById('phone').value;
  
  try {
    const response = await fetch('/api/auth/send-code', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ phone })
    });
    
    if (!response.ok) {
      throw new Error('发送验证码失败');
    }
    
    // 开始倒计时
    startCountdown();
    
    // 显示成功消息
    document.getElementById('code-sent-message').textContent = '验证码已发送';
  } catch (error) {
    document.getElementById('error-message').textContent = error.message;
  }
}

// 验证码登录
async function verifyCode(event) {
  event.preventDefault();
  
  const phone = document.getElementById('phone').value;
  const code = document.getElementById('code').value;
  
  try {
    const response = await fetch('/api/auth/verify-code', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ phone, code })
    });
    
    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(errorData.message || '验证失败');
    }
    
    const data = await response.json();
    
    // 保存 token
    localStorage.setItem('token', data.token);
    if (data.refreshToken) {
      localStorage.setItem('refreshToken', data.refreshToken);
    }
    
    // 重定向到首页
    window.location.href = '/dashboard';
  } catch (error) {
    document.getElementById('error-message').textContent = error.message;
  }
}
5.2.2 后端实现
// 发送验证码
@PostMapping("/send-code")
public ResponseEntity<?> sendVerificationCode(@RequestBody PhoneRequest request) {
    String phone = request.getPhone();
    
    // 生成验证码
    String code = generateVerificationCode();
    
    // 保存验证码到缓存或数据库,设置过期时间
    saveVerificationCode(phone, code);
    
    // 发送验证码短信
    sendSms(phone, "您的验证码是: " + code);
    
    return ResponseEntity.ok().body("Verification code sent");
}

// 验证码登录
@PostMapping("/verify-code")
public ResponseEntity<?> verifyCode(@RequestBody VerifyCodeRequest request) {
    String phone = request.getPhone();
    String code = request.getCode();
    
    // 验证验证码
    if (!validateVerificationCode(phone, code)) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body("Invalid verification code");
    }
    
    // 查找或创建用户
    User user = userService.findByPhone(phone);
    if (user == null) {
        user = userService.createUserFromPhone(phone);
    }
    
    // 生成 token
    String token = tokenProvider.generateToken(user);
    String refreshToken = tokenProvider.generateRefreshToken(user);
    
    // 返回 token
    Map<String, String> response = new HashMap<>();
    response.put("token", token);
    response.put("refreshToken", refreshToken);
    
    return ResponseEntity.ok(response);
}

5.3 第三方登录 (OAuth 2.0)

5.3.1 前端实现
// 第三方登录按钮点击
function handleThirdPartyLogin(provider) {
  // 重定向到第三方授权页面
  window.location.href = `/api/auth/oauth2/authorization/${provider}`;
}

// 处理 OAuth 回调
// 这个页面由后端提供,处理 OAuth 回调并重定向到前端应用
// 前端应用从 URL 参数中获取 token
window.onload = function() {
  const urlParams = new URLSearchParams(window.location.search);
  const token = urlParams.get('token');
  const refreshToken = urlParams.get('refreshToken');
  
  if (token) {
    // 保存 token
    localStorage.setItem('token', token);
    if (refreshToken) {
      localStorage.setItem('refreshToken', refreshToken);
    }
    
    // 重定向到首页
    window.location.href = '/dashboard';
  }
};
5.3.2 后端实现
// OAuth 2.0 配置
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            .and()
            .oauth2Login()
                .authorizationEndpoint()
                    .baseUri("/api/auth/oauth2/authorize")
                .and()
                .redirectionEndpoint()
                    .baseUri("/api/auth/oauth2/callback/*")
                .and()
                .successHandler((request, response, authentication) -> {
                    // 生成 token
                    String token = tokenProvider.generateToken(authentication);
                    String refreshToken = tokenProvider.generateRefreshToken(authentication);
                    
                    // 重定向到前端应用,并传递 token
                    response.sendRedirect("/oauth2-redirect.html?token=" + token + "&refreshToken=" + refreshToken);
                });
    }
    
    @Bean
    public OAuth2UserService oauth2UserService() {
        return new CustomOAuth2UserService();
    }
}

// 自定义 OAuth2UserService
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
    
    @Autowired
    private UserService userService;
    
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oauth2User = super.loadUser(userRequest);
        
        // 从 OAuth2 用户信息中获取邮箱
        String email = oauth2User.getAttribute("email");
        
        // 查找或创建用户
        User user = userService.findByEmail(email);
        if (user == null) {
            user = userService.createUserFromOAuth2(oauth2User);
        }
        
        // 返回自定义的 OAuth2User
        return new CustomOAuth2User(oauth2User, user);
    }
}

六、Token 安全与最佳实践

6.1 Token 安全风险

  1. XSS 攻击:攻击者通过注入恶意脚本获取用户的 Token。
  2. CSRF 攻击:攻击者诱导用户执行恶意请求,利用用户的 Token。
  3. Token 泄露:Token 被窃取或泄露。
  4. 重放攻击:攻击者截获请求并重复发送。
  5. 中间人攻击:攻击者截获通信并获取 Token。

6.2 Token 安全最佳实践

  1. 使用 HTTPS:确保所有通信都通过 HTTPS 进行。
  2. 设置合适的过期时间:Access Token 短期有效,Refresh Token 长期有效。
  3. 使用 HttpOnly Cookie:防止 JavaScript 访问 Cookie 中的 Token。
  4. 使用 Secure 标志:确保 Cookie 只通过 HTTPS 传输。
  5. 使用 SameSite 属性:防止 CSRF 攻击。
  6. 实现 Token 轮换:定期更换 Token。
  7. 实现 Token 撤销机制:允许用户主动撤销 Token。
  8. 限制 Token 使用范围:为不同用途创建不同的 Token。
  9. 实现 IP 绑定:将 Token 与用户 IP 绑定。
  10. 实现设备指纹:将 Token 与设备特征绑定。

6.3 实现 Token 撤销

// 存储已撤销的 Token
@Autowired
private RedisTemplate<String, String> redisTemplate;

// 撤销 Token
public void revokeToken(String token) {
    String tokenId = getTokenId(token);
    // 将 Token ID 加入黑名单,设置过期时间与 Token 相同
    redisTemplate.opsForValue().set("revoked:" + tokenId, "", getExpirationFromToken(token), TimeUnit.MILLISECONDS);
}

// 检查 Token 是否被撤销
public boolean isTokenRevoked(String token) {
    String tokenId = getTokenId(token);
    return Boolean.TRUE.equals(redisTemplate.hasKey("revoked:" + tokenId));
}

// 从 Token 中获取 ID
private String getTokenId(String token) {
    Claims claims = Jwts.parser()
            .setSigningKey(secretKey)
            .parseClaimsJws(token)
            .getBody();
    
    return claims.getId();
}

// 从 Token 中获取过期时间
private long getExpirationFromToken(String token) {
    Claims claims = Jwts.parser()
            .setSigningKey(secretKey)
            .parseClaimsJws(token)
            .getBody();
    
    return claims.getExpiration().getTime() - System.currentTimeMillis();
}

6.4 实现 Token 轮换

// 每次请求都生成新的 Token
@PostMapping("/rotate-token")
public ResponseEntity<?> rotateToken(HttpServletRequest request) {
    String oldToken = getJwtFromRequest(request);
    
    if (oldToken != null && tokenProvider.validateToken(oldToken)) {
        String username = tokenProvider.getUsernameFromToken(oldToken);
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        
        // 生成新 Token
        String newToken = tokenProvider.generateToken(userDetails);
        
        // 可选:撤销旧 Token
        tokenProvider.revokeToken(oldToken);
        
        // 返回新 Token
        Map<String, String> response = new HashMap<>();
        response.put("token", newToken);
        
        return ResponseEntity.ok(response);
    }
    
    return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid token");
}

七、常见问题与解决方案

7.1 Token 过期处理

问题:Token 过期后,用户需要重新登录,影响用户体验。

解决方案

  1. 使用 Refresh Token 自动刷新 Access Token。
  2. 实现 Token 自动续期机制。
  3. 在 Token 即将过期时提前刷新。
// 前端实现 Token 自动续期
function setupTokenRefresh() {
  const token = localStorage.getItem('token');
  if (token) {
    const payload = JSON.parse(atob(token.split('.')[1]));
    const expirationTime = payload.exp * 1000; // 转换为毫秒
    const currentTime = Date.now();
    const timeUntilExpiration = expirationTime - currentTime;
    
    // 如果 Token 将在 5 分钟内过期,提前刷新
    if (timeUntilExpiration > 0 && timeUntilExpiration < 300000) {
      refreshToken();
    }
    
    // 设置定时器,在 Token 过期前刷新
    setTimeout(() => {
      refreshToken();
      // 重新设置定时器
      setupTokenRefresh();
    }, Math.max(0, timeUntilExpiration - 300000));
  }
}

// 页面加载时设置 Token 刷新
window.addEventListener('load', setupTokenRefresh);

7.2 多设备登录处理

问题:用户在多设备登录时,Token 管理复杂。

解决方案

  1. 为每个设备生成独立的 Token。
  2. 实现设备管理功能,允许用户查看和撤销设备登录。
  3. 使用设备 ID 或指纹区分不同设备。
// 生成带有设备 ID 的 Token
public String generateToken(UserDetails userDetails, String deviceId) {
    Map<String, Object> claims = new HashMap<>();
    claims.put("sub", userDetails.getUsername());
    claims.put("deviceId", deviceId);
    claims.put("created", new Date());
    
    return Jwts.builder()
            .setClaims(claims)
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + 3600000))
            .signWith(SignatureAlgorithm.HS256, secretKey)
            .compact();
}

// 获取用户的所有活跃设备
public List<DeviceInfo> getUserDevices(String username) {
    // 从数据库或缓存中获取用户的设备信息
    return deviceRepository.findByUsername(username);
}

// 撤销特定设备的 Token
public void revokeDeviceToken(String username, String deviceId) {
    // 将设备 ID 加入黑名单
    redisTemplate.opsForValue().set("revoked:device:" + username + ":" + deviceId, "", 24, TimeUnit.HOURS);
}

7.3 单点登录 (SSO) 实现

问题:用户需要在多个系统中重复登录。

解决方案

  1. 实现 SSO 服务器,集中管理用户认证。
  2. 使用 OAuth 2.0 或 OpenID Connect 协议。
  3. 实现 Token 共享机制。
// SSO 服务器配置
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("client1")
                .secret(passwordEncoder.encode("secret1"))
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .scopes("openid", "profile", "email")
                .redirectUris("http://client1.example.com/callback")
                .accessTokenValiditySeconds(3600)
                .refreshTokenValiditySeconds(86400);
    }
    
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
                .tokenStore(tokenStore());
    }
    
    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource);
    }
}

// 客户端配置
@Configuration
@EnableOAuth2Sso
public class OAuth2ClientConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/", "/login**").permitAll()
                .anyRequest().authenticated()
                .and()
                .oauth2Login();
    }
}

7.4 移动应用 Token 处理

问题:移动应用需要安全存储 Token。

解决方案

  1. 使用安全存储机制(如 Keychain、Keystore)。
  2. 实现生物认证(如指纹、面部识别)。
  3. 使用 App 沙盒隔离存储。
// Android 示例 (使用 EncryptedSharedPreferences)
public class SecureTokenManager {
    private static final String PREF_NAME = "secure_prefs";
    private static final String KEY_TOKEN = "token";
    private static final String KEY_REFRESH_TOKEN = "refresh_token";
    
    private final EncryptedSharedPreferences prefs;
    
    public SecureTokenManager(Context context) throws Exception {
        MasterKey masterKey = new MasterKey.Builder(context)
                .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
                .build();
        
        prefs = EncryptedSharedPreferences.create(
                context,
                PREF_NAME,
                masterKey,
                EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
                EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
        );
    }
    
    public void saveToken(String token) {
        prefs.edit().putString(KEY_TOKEN, token).apply();
    }
    
    public String getToken() {
        return prefs.getString(KEY_TOKEN, null);
    }
    
    public void saveRefreshToken(String refreshToken) {
        prefs.edit().putString(KEY_REFRESH_TOKEN, refreshToken).apply();
    }
    
    public String getRefreshToken() {
        return prefs.getString(KEY_REFRESH_TOKEN, null);
    }
    
    public void clearTokens() {
        prefs.edit().clear().apply();
    }
}

八、总结

前后端 Token 处理与登录机制是现代 Web 应用的重要组成部分,它确保了用户身份验证和授权的安全性和可靠性。通过合理设计和实现 Token 处理机制,可以提高系统的安全性和用户体验。

关键要点

  1. 选择合适的 Token 类型:JWT 适合无状态应用,Opaque Token 适合需要撤销的场景。
  2. 安全存储 Token:前端使用 HttpOnly Cookie 或安全存储机制,后端使用安全的密钥管理。
  3. 实现 Token 刷新机制:使用 Refresh Token 延长用户会话,提高用户体验。
  4. 处理多设备登录:为每个设备生成独立的 Token,实现设备管理功能。
  5. 实现 SSO:集中管理用户认证,减少用户重复登录。
  6. 遵循安全最佳实践:使用 HTTPS,设置合适的过期时间,实现 Token 撤销机制。

你可能感兴趣的:(安全,Java,前端,后端,token)