登录controller
java
package com.example.demo.controller;
import com.example.demo.Vo.LoginResult;
import com.example.demo.Vo.R;
import com.example.demo.entity.LoginBody;
import com.example.demo.service.AuthService;
import com.example.demo.service.CaptchaService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 用户认证 Controller
*/
@RestController
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
private final CaptchaService captchaService;
/**
* 登录接口
*/
@PostMapping("/login")
public R login(@RequestBody LoginBody loginBody) {
// 1. 验证码校验 (可选)
if (loginBody.getCaptchaCode() != null) {
captchaService.verifyCaptcha(loginBody.getUserName(),
loginBody.getCaptchaCode());
}
// 2. 执行登录认证
LoginResult result = authService.login(
loginBody.getUserName(),
loginBody.getPassword(),
loginBody.getCode(),
loginBody.getUuid()
);
// 3. 返回 Token 和用户信息
Map<String, Object> response = new HashMap<>();
response.put("accessToken", result.getAccessToken());
response.put("refreshToken", result.getRefreshToken());
response.put("userInfo", result.getUserInfo());
return R.ok(response);
}
/**
* 退出登录
*/
// @PostMapping("/logout")
// public R<Void> logout(HttpServletRequest request) {
// String token = getTokenFromRequest(request);
// authService.logout(token);
// return R.ok();
// }
/**
* 刷新 Token
*/
@PostMapping("/refresh")
public R refreshToken(
@RequestParam String refreshToken) {
Map<String, String> tokens = authService.refreshToken(refreshToken);
return R.ok(tokens);
}
}
查询用户信息服务
java
package com.example.demo.service;
import com.example.demo.entity.SysUser;
public interface UserService {
SysUser selectUserByUserName(String username);
}
java
package com.example.demo.service.impl;
import com.example.demo.entity.SysUser;
import com.example.demo.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Override
public SysUser selectUserByUserName(String username) {
return null;
}
}
权限认证服务
java
package com.example.demo.service;
import com.example.demo.Vo.LoginResult;
import com.example.demo.entity.LoginUser;
import com.example.demo.entity.SysUser;
import com.example.demo.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.service.spi.ServiceException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 认证服务
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class AuthService {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;
private final TokenService tokenService;
private final UserService userService;
private final UserDetailsService userDetailsService;
/**
* 登录认证
*/
@Transactional
public LoginResult login(String username, String password,
String code, String uuid) {
// 1. 创建认证 Token
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(username, password);
// 2. 执行认证 (会调用 UserDetailsService)
Authentication authenticate;
try {
authenticate = authenticationManager.authenticate(authToken);
} catch (BadCredentialsException e) {
throw new ServiceException("用户名或密码错误");
} catch (DisabledException e) {
throw new ServiceException("账号已被停用");
} catch (Exception e) {
log.error("登录认证失败", e);
throw new ServiceException("登录失败:" + e.getMessage());
}
// 3. 获取登录用户
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
// 4. 记录登录日志
// recordLogininfor(username, Constants.LOGIN_SUCCESS, "登录成功");
// 5. 生成 Token
String deviceId = UUID.randomUUID().toString();
String accessToken = jwtUtil.generateToken(loginUser, deviceId);
String refreshToken = jwtUtil.generateRefreshToken(username);
// 6. 将用户信息缓存到 Redis
tokenService.saveLoginUser(loginUser, accessToken, deviceId);
// 7. 构建返回结果
LoginResult result = new LoginResult();
result.setAccessToken(accessToken);
result.setRefreshToken(refreshToken);
// result.setUserInfo(buildUserInfo(loginUser));
return result;
}
/**
* 退出登录
*/
public void logout(String token) {
// 从 Redis 中删除登录信息
// tokenService.deleteLoginUser(token);
// 记录登出日志
Claims claims = jwtUtil.parseToken(token);
if (claims != null) {
String username = claims.getSubject();
// recordLogininfor(username, Constants.LOGOUT, "退出成功");
}
}
/**
* 刷新 Token
*/
@Transactional
public Map<String, String> refreshToken(String refreshToken) {
// 1. 验证 RefreshToken
if (!jwtUtil.validateToken(refreshToken)) {
throw new ServiceException("RefreshToken 已过期");
}
// 2. 获取用户名
Claims claims = jwtUtil.parseToken(refreshToken);
String username = claims.getSubject();
// 3. 重新生成 Token
LoginUser loginUser = loadUserByUsername(username);
String deviceId = UUID.randomUUID().toString();
String newAccessToken = jwtUtil.generateToken(loginUser, deviceId);
String newRefreshToken = jwtUtil.generateRefreshToken(username);
// 4. 更新 Redis缓存
tokenService.saveLoginUser(loginUser, newAccessToken, deviceId);
// 5. 返回新 Token
Map<String, String> tokens = new HashMap<>();
tokens.put("accessToken", newAccessToken);
tokens.put("refreshToken", newRefreshToken);
return tokens;
}
/**
* 加载用户信息
*/
private LoginUser loadUserByUsername(String username) {
SysUser user = userService.selectUserByUserName(username);
if (user == null) {
throw new ServiceException("用户不存在");
}
return (LoginUser)userDetailsService.loadUserByUsername(username);
}
}
验证码服务接口设计
java
package com.example.demo.service;
public interface CaptchaService {
void verifyCaptcha(String userName,String captchaCode);
}
java
package com.example.demo.service.impl;
import com.example.demo.service.CaptchaService;
import org.springframework.stereotype.Service;
@Service
public class CaptchaServiceImpl implements CaptchaService {
@Override
public void verifyCaptcha(String userName,String captchaCode) {
}
}
springSecurity配置类
java
package com.example.demo.config;
import com.example.demo.entity.SysUser;
import com.example.demo.filter.JwtAuthenticationFilter;
import com.example.demo.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
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;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Spring Security 6 配置
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
// private final AuthenticationProvider authenticationProvider;
private final UserService userService;
/**
* 关键步骤:定义 AuthenticationManager Bean
*/
// @Bean
// public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
// // 构建并返回 AuthenticationManager
// return http.getSharedObject(AuthenticationManager.class);
// // 或者更常见的做法是直接使用 build() 后的对象,但在 Spring Security 6 中推荐以下方式:
// }
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
/**
* 配置安全过滤器链
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用 CSRF
.csrf(csrf -> csrf.disable())
// 禁用 Session
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 配置授权规则
.authorizeHttpRequests(auth -> auth
// 放行静态资源和公开接口
.requestMatchers("/login", "/register", "/captchaImage")
.permitAll()
.requestMatchers("/common/download/**", "/common/download/resource")
.permitAll()
.requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**")
.permitAll()
// 其他请求需要认证
.anyRequest().authenticated()
)
// 添加 JWT 过滤器
.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class)
// 配置异常处理
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint((request, response, authException) -> {
// 未认证处理
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"未认证\"}");
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
// 无权限处理
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":403,\"msg\":\"无权限\"}");
})
);
return http.build();
}
/**
* 配置认证提供者
*/
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
/**
* 密码编码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 用户详情服务
*/
@Bean
public UserDetailsService userDetailsService() {
return username -> {
// 从数据库加载用户信息
SysUser user = userService.selectUserByUserName(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
return new org.springframework.security.core.userdetails.User(
username,
user.getPassword(),
getAuthorities(user)
);
};
}
/**
* 获取用户权限列表
*/
private Collection<? extends GrantedAuthority> getAuthorities(SysUser user) {
List<GrantedAuthority> authorities = new ArrayList<>();
// // 添加角色权限
// for (SysRole role : user.getRoles()) {
// authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleKey()));
// }
//
// // 添加操作权限
// for (String permission : user.getPermissions()) {
// authorities.add(new SimpleGrantedAuthority(permission));
// }
return authorities;
}
}
jwt配置
java
package com.example.demo.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
/**
* JWT配置类,读取Application.yml中的配置
*
* @author AdminMall
*
*/
@Setter
@Getter
@Configuration
public class JwtConfig {
@Value("${jwt.tokenHeader}")
private String tokenHeader; // JWT存储的请求头
@Value("${jwt.secret}")
private String secret; // jwt加解密使用的密钥
@Value("${jwt.expiration}")
private long expiration; // JWT的超时时间
@Value("${jwt.tokenHead}")
private String tokenHead; // JWT负载中拿到的开头
@Value("${jwt.refreshExpiration}")
private Long refreshExpiration; // RefreshToken 过期时间 (7 天)
}
jwt生成工具类
java
package com.example.demo.utils;
import com.example.demo.config.JwtConfig;
import com.example.demo.entity.LoginUser;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* JWT Token 工具类
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtUtil {
@Autowired
private JwtConfig jwtConfig;
/**
* 生成 Token
*/
public String generateToken(LoginUser loginUser, String deviceId) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", loginUser.getUserId());
claims.put("userName", loginUser.getUsername());
claims.put("deviceId", deviceId);
Date now = new Date();
Date expireDate = new Date(now.getTime() + jwtConfig.getExpiration());
return Jwts.builder()
.setClaims(claims)
.setId(UUID.randomUUID().toString())
.setIssuedAt(now)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS256, jwtConfig.getSecret())
.compact();
}
/**
* 生成 RefreshToken
*/
public String generateRefreshToken(String username) {
Date now = new Date();
Date expireDate = new Date(now.getTime() + jwtConfig.getRefreshExpiration());
return Jwts.builder()
.setSubject(username)
.setId(UUID.randomUUID().toString())
.setIssuedAt(now)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS256, jwtConfig.getSecret())
.compact();
}
/**
* 解析 Token
*/
public Claims parseToken(String token) {
try {
return Jwts.parser()
.setSigningKey(jwtConfig.getSecret())
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
log.error("解析 Token 失败:{}", e.getMessage());
return null;
}
}
/**
* 从 Token 中获取用户 ID
*/
public Long getUserId(String token) {
Claims claims = parseToken(token);
if (claims == null) {
return null;
}
return Long.valueOf(claims.get("userId").toString());
}
/**
* 验证 Token 是否过期
*/
public boolean isTokenExpired(String token) {
Claims claims = parseToken(token);
if (claims == null) {
return true;
}
return claims.getExpiration().before(new Date());
}
/**
* 验证 Token 是否有效
*/
public boolean validateToken(String token) {
try {
Claims claims = parseToken(token);
return claims != null && !isTokenExpired(token);
} catch (Exception e) {
log.error("验证 Token 失败", e);
return false;
}
}
}
分布式token存redis共享服务
java
package com.example.demo.service;
import com.example.demo.entity.LoginUser;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* Token 服务优化版
*/
@Service
@RequiredArgsConstructor
public class TokenService {
// private final RedisUtil redisUtil;
/**
* 保存登录用户 (带缓存预热)
*/
public void saveLoginUser(LoginUser user, String token, String deviceId) {
String userKey = "login:token:" + token;
String deviceKey = "login:device:" + deviceId;
// 1. 缓存用户信息
// redisUtil.set(userKey, user, 2, TimeUnit.HOURS);
// 2. 建立 Token 和设备映射
// redisUtil.set(deviceKey, token, 2, TimeUnit.HOURS);
// 3. 预热用户权限数据
warmUpUserPermissions(user.getUserId());
}
/**
* 获取登录用户 (多级缓存)
*/
public LoginUser getLoginUser(String token) {
// 1. 检查 Token 黑名单
// if (isTokenBlacklisted(token)) {
// return null;
// }
// 2. 从 Redis 获取
String userKey = "login:token:" + token;
// LoginUser user = redisUtil.get(userKey, LoginUser.class);
// if (user != null) {
// // 3. 续期
// redisUtil.expire(userKey, 2, TimeUnit.HOURS);
// }
return null;
}
/**
* 预热用户权限数据
*/
private void warmUpUserPermissions(Long userId) {
// 提前加载用户权限到缓存
String permKey = "user:perms:" + userId;
// ... 加载权限数据
}
/**
* 检查 Token 是否在黑名单中
// */
// private boolean isTokenBlacklisted(String token) {
// return Boolean.TRUE.equals(redisUtil.hasKey("token:blacklist:" + token));
// }
}
userDetailsService服务,springSecurity框架提供
java
/*
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.core.userdetails;
/**
* Core interface which loads user-specific data.
* <p>
* It is used throughout the framework as a user DAO and is the strategy used by the
* {@link org.springframework.security.authentication.dao.DaoAuthenticationProvider
* DaoAuthenticationProvider}.
*
* <p>
* The interface requires only one read-only method, which simplifies support for new
* data-access strategies.
*
* @author Ben Alex
* @see org.springframework.security.authentication.dao.DaoAuthenticationProvider
* @see UserDetails
*/
public interface UserDetailsService {
/**
* Locates the user based on the username. In the actual implementation, the search
* may possibly be case sensitive, or case insensitive depending on how the
* implementation instance is configured. In this case, the <code>UserDetails</code>
* object that comes back may have a username that is of a different case than what
* was actually requested..
* @param username the username identifying the user whose data is required.
* @return a fully populated user record (never <code>null</code>)
* @throws UsernameNotFoundException if the user could not be found or the user has no
* GrantedAuthority
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
jwt过滤器
java
package com.example.demo.filter;
import com.example.demo.entity.LoginUser;
import com.example.demo.service.TokenService;
import com.example.demo.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import io.micrometer.common.util.StringUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* JWT 认证过滤器
*/
@Slf4j
@Component
@RequiredArgsConstructor
@Lazy
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private final JwtUtil jwtUtil;
@Autowired
private final TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 1. 获取 Token
String token = getTokenFromRequest(request);
// 2. 没有 Token,直接放行
if (StringUtils.isEmpty(token)) {
filterChain.doFilter(request, response);
return;
}
// 3. 验证 Token
if (!jwtUtil.validateToken(token)) {
log.warn("Token 无效或已过期");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"Token 无效或已过期\"}");
return;
}
// 4. 解析 Token,获取用户信息
Claims claims = jwtUtil.parseToken(token);
Long userId = Long.valueOf(claims.get("userId").toString());
String username = claims.getSubject();
// 5. 从 Redis 中获取登录用户信息
LoginUser loginUser = tokenService.getLoginUser(token);
if (loginUser == null) {
log.warn("登录用户不存在");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("{\"code\":401,\"msg\":\"登录用户不存在\"}");
return;
}
// 6. 设置 Spring Security 上下文
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null,
loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource()
.buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
log.debug("用户 [{}] 认证成功", username);
// 7. 继续过滤链
filterChain.doFilter(request, response);
}
/**
* 从请求中获取 Token
*/
private String getTokenFromRequest(HttpServletRequest request) {
// 从 Header 中获取
String token = request.getHeader("Authorization");
// 支持 Bearer Token 格式
if (StringUtils.isNotEmpty(token) && token.startsWith("Bearer ")) {
token = token.substring(7);
}
// 如果 Header 中没有,尝试从参数中获取
if (StringUtils.isEmpty(token)) {
token = request.getParameter("token");
}
return token;
}
}
token防止篡改
java
package com.example.demo.utils;
import com.example.demo.entity.LoginUser;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import java.util.Map;
/**
* Token 签名验证增强
*/
@Slf4j
public class SecureJwtUtil extends JwtUtil {
@Value("${jwt.secret}")
private String secret;
/**
* 双重签名验证
*/
public boolean validateTokenSecure(String token) {
// 1. 基础验证
if (!validateToken(token)) {
return false;
}
// 2. 检查签名算法
Claims claims = parseToken(token);
String alg = (String) claims.get("alg");
if (!"HS256".equals(alg)) {
log.warn("不支持的签名算法:{}", alg);
return false;
}
// 3. 检查必要字段
if (claims.get("userId") == null || claims.getSubject() == null) {
log.warn("Token 缺少必要字段");
return false;
}
return true;
}
/**
* 添加指纹验证
*/
public String generateTokenWithFingerprint(LoginUser user, String fingerprint) {
Map<String, Object> claims = buildClaims(user);
claims.put("fingerprint", fingerprint); // 设备指纹
return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
}
yaml配置
java
jwt:
tokenHeader: Authorization #JWT存储的请求头
secret: mall-jwt-test #jwt加解密使用的密钥
expiration: 604800 #JWT的超时时间
tokenHead: Bearer #JWT负载中拿到的开头
refreshExpiration: 604800
代码写道这里基本上大体上就完成了一个简单的demo搭建,大家可以照着试一试,学习永不止步