在现代 Web 应用中,登录机制是用户身份验证和授权的基础,它确保了只有经过验证的用户才能访问受保护的资源。一个安全、高效的登录机制对于保护用户数据和系统安全至关重要。
JWT 由三部分组成,用点 (.) 分隔:
Header (头部):包含令牌类型和使用的算法。
{
"alg": "HS256",
"typ": "JWT"
}
Payload (负载):包含声明(claims),如用户 ID、角色、过期时间等。
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622
}
Signature (签名):用于验证令牌的真实性。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
前端通常在用户登录成功后从服务器响应中获取 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;
}
}
前端有多种存储 Token 的方式,各有优缺点:
// 存储
localStorage.setItem('token', token);
// 获取
const token = localStorage.getItem('token');
// 删除
localStorage.removeItem('token');
优点:
缺点:
// 存储
sessionStorage.setItem('token', token);
// 获取
const token = sessionStorage.getItem('token');
// 删除
sessionStorage.removeItem('token');
优点:
缺点:
// 设置 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';
优点:
缺点:
// React 示例
const [token, setToken] = useState(null);
// 存储
setToken(tokenValue);
// 使用
if (token) {
// 使用 token
}
// Vue 示例 (Vuex)
// 存储
this.$store.commit('setToken', tokenValue);
// 获取
const token = this.$store.state.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;
}
}
当 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;
}
}
使用 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:
// 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();
}
后端需要验证客户端请求中携带的 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();
}
后端处理 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");
}
}
后端使用 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;
}
}
// 登录表单提交
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;
}
}
// 登录控制器
@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");
}
}
}
// 发送验证码
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;
}
}
// 发送验证码
@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);
}
// 第三方登录按钮点击
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';
}
};
// 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
@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();
}
// 每次请求都生成新的 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");
}
问题: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);
问题:用户在多设备登录时,Token 管理复杂。
解决方案:
// 生成带有设备 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);
}
问题:用户需要在多个系统中重复登录。
解决方案:
// 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();
}
}
问题:移动应用需要安全存储 Token。
解决方案:
// 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 处理机制,可以提高系统的安全性和用户体验。