实现后端校验登录

This commit is contained in:
Bunny 2025-07-17 17:08:04 +08:00
parent 02901efa33
commit 20e7f71936
14 changed files with 37504 additions and 36007 deletions

View File

@ -36,7 +36,14 @@ public class GlobalExceptionHandler {
public Result<Object> exceptionHandler(RuntimeException exception) {
String message = exception.getMessage();
message = StringUtils.hasText(message) ? message : "服务器异常";
exception.printStackTrace();
log.error("发生业务异常: {}", exception.getMessage(), exception);
// 💡IDEA如果需要特殊情况的日志可以参考下面的代码
// =========================================
// StringWriter sw = new StringWriter();
// e.printStackTrace(new PrintWriter(sw));
// logger.error(sw.toString());
// =========================================
// 解析异常
String jsonParseError = "JSON parse error (.*)";

View File

@ -0,0 +1,19 @@
package com.spring.step3.exception;
import org.springframework.security.core.AuthenticationException;
/**
* 自定义未认证异常
*/
public class MyAuthenticationException extends AuthenticationException {
/**
* Constructs an {@code AuthenticationException} with the specified message and root
* cause.
*
* @param msg the detail message
* @param cause the root cause
*/
public MyAuthenticationException(String msg, Throwable cause) {
super(msg, cause);
}
}

View File

@ -6,8 +6,8 @@ import org.springframework.stereotype.Component;
public class AuthorizationLogic {
public boolean decide(String name) {
System.out.println(name);
// 直接使用name的实现
// System.out.println(name);
return name.equalsIgnoreCase("user");
}

View File

@ -14,16 +14,21 @@ import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.util.List;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(jsr250Enabled = true)
@RequiredArgsConstructor
public class SecurityWebConfiguration {
public static List<String> securedPaths = List.of("/api/**");
public static List<String> noAuthPaths = List.of("/*/login");
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 前端段分离不需要---禁用明文验证
.httpBasic(AbstractHttpConfigurer::disable)
@ -44,9 +49,14 @@ public class SecurityWebConfiguration {
// 访问路径为 /api 时需要进行认证
authorizeRequests
// 不认证登录接口
.requestMatchers("/*/login", "/api/security/**").permitAll()
// 只认证 /api/** 下的所有接口
.requestMatchers("/api/**").authenticated()
.requestMatchers(noAuthPaths.toArray(String[]::new)).permitAll()
// 只认证 securedPaths 下的所有接口
// =======================================================================
// 也可以在这里写多参数传入"/api/**","/admin/**"
// 但是在 Spring过滤器中如果要放行不需要认证请求但是需要认证的接口必需要携带token
// 做法是在这里定义要认证的接口如果要做成动态可以放到数据库
// =======================================================================
.requestMatchers(securedPaths.toArray(String[]::new)).authenticated()
// 其余请求都放行
.anyRequest().permitAll()
)
@ -56,7 +66,7 @@ public class SecurityWebConfiguration {
// 没有权限访问
exception.accessDeniedHandler(new SecurityAccessDeniedHandler());
})
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
;
return http.build();

View File

@ -3,8 +3,11 @@ package com.spring.step3.security.filter;
import com.spring.step3.config.context.BaseContext;
import com.spring.step3.domain.vo.result.ResultCodeEnum;
import com.spring.step3.exception.AuthenticSecurityException;
import com.spring.step3.exception.MyAuthenticationException;
import com.spring.step3.security.config.SecurityWebConfiguration;
import com.spring.step3.security.handler.SecurityAuthenticationEntryPoint;
import com.spring.step3.security.service.DbUserDetailService;
import com.spring.step3.security.service.JwtBearTokenService;
import com.spring.step3.security.service.JwtTokenService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
@ -16,6 +19,9 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@ -26,52 +32,91 @@ import java.io.IOException;
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtBearTokenService jwtBearTokenService;
private final JwtTokenService jwtTokenService;
private final DbUserDetailService userDetailsService;
private final SecurityAuthenticationEntryPoint securityAuthenticationEntryPoint;
@Override
protected void doFilterInternal(@NotNull HttpServletRequest request,
@NotNull HttpServletResponse response,
@NotNull FilterChain filterChain) throws ServletException, IOException, AuthenticSecurityException {
final String authHeader = request.getHeader("Authorization");
// 先校验不需要认证的接口
RequestMatcher[] requestNoAuthMatchers = SecurityWebConfiguration.noAuthPaths.stream()
.map(AntPathRequestMatcher::new)
.toArray(RequestMatcher[]::new);
OrRequestMatcher noAuthRequestMatcher = new OrRequestMatcher(requestNoAuthMatchers);
if (noAuthRequestMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
// 获取需要认证的接口
RequestMatcher[] requestSecureMatchers = SecurityWebConfiguration.securedPaths.stream()
.map(AntPathRequestMatcher::new)
.toArray(RequestMatcher[]::new);
OrRequestMatcher secureRequestMatcher = new OrRequestMatcher(requestSecureMatchers);
// 公开接口直接放行
if (!secureRequestMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
final String authHeader = request.getHeader("Authorization");
// 如果当前请求不包含验证Token直接返回
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
// throw new SecurityException(ResultCodeEnum.LOGIN_AUTH);
return;
throw new AuthenticSecurityException(ResultCodeEnum.LOGIN_AUTH);
}
// 当前请求的Token
final String jwtToken = authHeader.substring(7);
// 检查当前Token是否过期
if (jwtBearTokenService.isTokenValid(jwtToken)) {
// TODO 抛出异常 Security 未处理
throw new AuthenticSecurityException(ResultCodeEnum.AUTHENTICATION_EXPIRED);
try {
// 检查当前Token是否过期
if (jwtTokenService.isExpired(jwtToken)) {
// 💡如果过期不需要进行判断和验证需要直接放行可以像下面这样写
// ===================================================
// filterChain.doFilter(request, response);
// return;
// ===================================================
throw new AuthenticSecurityException(ResultCodeEnum.AUTHENTICATION_EXPIRED);
}
// 解析当前Token中的用户名
String username = jwtTokenService.getUsernameFromToken(jwtToken);
Long userId = jwtTokenService.getUserIdFromToken(jwtToken);
// 当前用户名存在并且 Security上下文为空设置认证相关信息
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 调用用户信息进行登录
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 设置认证用户信息
SecurityContextHolder.getContext().setAuthentication(authToken);
BaseContext.setUsername(username);
BaseContext.setUserId(userId);
}
filterChain.doFilter(request, response);
}
// 解析当前Token中的用户名
final String username = jwtBearTokenService.getUsernameFromToken(jwtToken);
final Long userId = jwtBearTokenService.getUserIdFromToken(jwtToken);
// 当前用户名存在并且 Security上下文为空设置认证相关信息
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 调用用户信息进行登录
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
// IMPORTANT:
// ==========================================================================
// catch 块中securityAuthenticationEntryPoint.commence() 已经处理了错误响应
// 所以应该 直接返回避免继续执行后续逻辑
// ==========================================================================
catch (RuntimeException exception) {
securityAuthenticationEntryPoint.commence(
request,
response,
new MyAuthenticationException(exception.getMessage(), exception)
);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 设置认证用户信息
SecurityContextHolder.getContext().setAuthentication(authToken);
BaseContext.setUsername(username);
BaseContext.setUserId(userId);
}
filterChain.doFilter(request, response);
}
}

View File

@ -2,7 +2,7 @@ package com.spring.step3.security.handler;
import com.spring.step3.domain.vo.result.Result;
import com.spring.step3.domain.vo.result.ResultCodeEnum;
import com.spring.step3.security.service.JwtBearTokenService;
import com.spring.step3.security.service.JwtTokenService;
import com.spring.step3.utils.ResponseUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@ -21,14 +21,16 @@ import org.springframework.util.StringUtils;
@RequiredArgsConstructor
public class JwtTokenLogoutHandler implements LogoutHandler {
private final JwtBearTokenService jwtBearTokenService;
private final JwtTokenService jwtTokenService;
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
// 在这里可以设置不同的请求头标识符常见的AuthorizationToken等
String authorizationToken = request.getHeader("Authorization");
if (StringUtils.hasText(authorizationToken)) {
// 如果当前用户信息存在redis中可以通过这个进行退出
String username = jwtBearTokenService.getUsernameFromToken(authorizationToken);
String username = jwtTokenService.getUsernameFromToken(authorizationToken);
log.info("username : {}", username);
}

View File

@ -2,24 +2,32 @@ package com.spring.step3.security.handler;
import com.spring.step3.domain.vo.result.Result;
import com.spring.step3.domain.vo.result.ResultCodeEnum;
import com.spring.step3.exception.MyAuthenticationException;
import com.spring.step3.utils.ResponseUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import java.io.IOException;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
log.error("SecurityAuthenticationEntryPoint:{}", authException.getLocalizedMessage());
Result<Object> result;
// 自定义认证异常
if (authException instanceof MyAuthenticationException) {
result = Result.error(null, authException.getMessage());
ResponseUtil.out(response, result);
}
// 未认证---未登录
Result<Object> result = Result.error(authException.getMessage(), ResultCodeEnum.LOGIN_AUTH);
result = Result.error(authException.getMessage(), ResultCodeEnum.LOGIN_AUTH);
ResponseUtil.out(response, result);
}
}

View File

@ -13,7 +13,7 @@ import java.util.Map;
@Configuration
@ConfigurationProperties(prefix = "jwt-token")
public class JwtBearTokenService {
public class JwtTokenService {
@Value("${jwtToken.secret}")
public String secret;
@ -89,7 +89,7 @@ public class JwtBearTokenService {
* @param token 令牌
* @return 是否国企
*/
public boolean isTokenValid(String token) {
public boolean isExpired(String token) {
SecretKey secretKey = getSecretKey();
return JwtTokenUtil.isExpired(token, secretKey);
}

View File

@ -9,7 +9,7 @@ import com.spring.step3.domain.vo.LoginVo;
import com.spring.step3.domain.vo.result.ResultCodeEnum;
import com.spring.step3.mapper.UserMapper;
import com.spring.step3.security.service.DbUserDetailService;
import com.spring.step3.security.service.JwtBearTokenService;
import com.spring.step3.security.service.JwtTokenService;
import com.spring.step3.service.user.LoginService;
import com.spring.step3.service.user.strategy.DefaultLoginStrategy;
import com.spring.step3.service.user.strategy.LoginContext;
@ -30,7 +30,7 @@ import java.util.List;
@RequiredArgsConstructor
public class LoginServiceImpl extends ServiceImpl<UserMapper, UserEntity> implements LoginService {
private final JwtBearTokenService jwtBearTokenService;
private final JwtTokenService jwtTokenService;
private final DbUserDetailService dbUserDetailService;
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
@ -73,10 +73,10 @@ public class LoginServiceImpl extends ServiceImpl<UserMapper, UserEntity> implem
List<String> roles = dbUserDetailService.findUserRolesByUserId(userId);
List<String> permission = dbUserDetailService.findPermissionByUserId(userId);
String token = jwtBearTokenService.createToken(userId, user.getUsername(), roles, permission);
String token = jwtTokenService.createToken(userId, user.getUsername(), roles, permission);
// 过期时间
Long expiresInSeconds = jwtBearTokenService.expired;
Long expiresInSeconds = jwtTokenService.expired;
long expirationMillis = System.currentTimeMillis() + (expiresInSeconds * 1000);
Date date = new Date(expirationMillis);

View File

@ -9,6 +9,9 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.Map;
@ -95,15 +98,23 @@ public class JwtTokenUtil {
*
* @param userId 用户ID
* @param username 用户名
* @param day 过期时间
* @param second 过期时间
* @return token值
*/
public static String createToken(Long userId, String username,
List<String> roles, List<String> permissions,
String subject, SecretKey key, Long day) {
String subject, SecretKey key, Long second) {
// 传进来的是秒转成未来过期时间
LocalDateTime localDateTime = LocalDateTime.now().plusSeconds(second);
Date date = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
// 转成过期时间
String format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
return Jwts.builder()
.subject(subject)
.expiration(new Date(System.currentTimeMillis() + day))
.expiration(date)
.claim("expiredTime", format)
.claim("userId", userId)
.claim("username", username)
.claim("roles", roles)
@ -197,6 +208,7 @@ public class JwtTokenUtil {
*
* @param token token
* @return 是否过期
* @throws RuntimeException ️解析失败和过期都属于异常类型
*/
public static boolean isExpired(String token, SecretKey key) {
try {
@ -204,9 +216,10 @@ public class JwtTokenUtil {
Date expiration = claimsJws.getPayload().getExpiration();
return expiration != null && expiration.before(new Date());
} catch (Exception exception) {
// TODO 抛出异常 Security 未处理
throw new AuthenticSecurityException(ResultCodeEnum.TOKEN_PARSING_FAILED);
} catch (RuntimeException exception) {
// ResultCodeEnum codeEnum = ResultCodeEnum.AUTHENTICATION_EXPIRED;
// throw new IllegalArgumentException(codeEnum.getMessage(), exception);
return true;
}
}
}

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.spring.step2.mapper.UserRoleMapper">
<mapper namespace="com.spring.step3.mapper.UserRoleMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.spring.step3.domain.entity.UserRoleEntity">

View File

@ -0,0 +1,29 @@
package com.spring.step3.security.service;
import io.jsonwebtoken.Jwts;
import org.junit.jupiter.api.Test;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.UUID;
class JwtTokenServiceTest {
@Test
void createToken() {
byte[] secretBytes = "aVeryLongAndSecureRandomStringWithAtLeast32Characters".getBytes(StandardCharsets.UTF_8);
SecretKeySpec hmacSHA256 = new SecretKeySpec(secretBytes, "HmacSHA256");
String token = Jwts.builder()
.subject("")
.expiration(new Date(System.currentTimeMillis() - 60 * 60 * 60 * 24 * 7))
.id(UUID.randomUUID().toString())
.signWith(hmacSHA256)
.compressWith(Jwts.ZIP.GZIP).compact();
System.out.println(token);
}
}