实现后端校验登录

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) { public Result<Object> exceptionHandler(RuntimeException exception) {
String message = exception.getMessage(); String message = exception.getMessage();
message = StringUtils.hasText(message) ? message : "服务器异常"; 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 (.*)"; 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 class AuthorizationLogic {
public boolean decide(String name) { public boolean decide(String name) {
System.out.println(name);
// 直接使用name的实现 // 直接使用name的实现
// System.out.println(name);
return name.equalsIgnoreCase("user"); 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.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.util.List;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@EnableMethodSecurity(jsr250Enabled = true) @EnableMethodSecurity(jsr250Enabled = true)
@RequiredArgsConstructor @RequiredArgsConstructor
public class SecurityWebConfiguration { public class SecurityWebConfiguration {
public static List<String> securedPaths = List.of("/api/**");
public static List<String> noAuthPaths = List.of("/*/login");
private final JwtAuthenticationFilter jwtAuthenticationFilter; private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean @Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception { SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http http
// 前端段分离不需要---禁用明文验证 // 前端段分离不需要---禁用明文验证
.httpBasic(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable)
@ -44,9 +49,14 @@ public class SecurityWebConfiguration {
// 访问路径为 /api 时需要进行认证 // 访问路径为 /api 时需要进行认证
authorizeRequests authorizeRequests
// 不认证登录接口 // 不认证登录接口
.requestMatchers("/*/login", "/api/security/**").permitAll() .requestMatchers(noAuthPaths.toArray(String[]::new)).permitAll()
// 只认证 /api/** 下的所有接口 // 只认证 securedPaths 下的所有接口
.requestMatchers("/api/**").authenticated() // =======================================================================
// 也可以在这里写多参数传入"/api/**","/admin/**"
// 但是在 Spring过滤器中如果要放行不需要认证请求但是需要认证的接口必需要携带token
// 做法是在这里定义要认证的接口如果要做成动态可以放到数据库
// =======================================================================
.requestMatchers(securedPaths.toArray(String[]::new)).authenticated()
// 其余请求都放行 // 其余请求都放行
.anyRequest().permitAll() .anyRequest().permitAll()
) )
@ -56,7 +66,7 @@ public class SecurityWebConfiguration {
// 没有权限访问 // 没有权限访问
exception.accessDeniedHandler(new SecurityAccessDeniedHandler()); exception.accessDeniedHandler(new SecurityAccessDeniedHandler());
}) })
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .addFilterAfter(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
; ;
return http.build(); 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.config.context.BaseContext;
import com.spring.step3.domain.vo.result.ResultCodeEnum; import com.spring.step3.domain.vo.result.ResultCodeEnum;
import com.spring.step3.exception.AuthenticSecurityException; 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.DbUserDetailService;
import com.spring.step3.security.service.JwtBearTokenService; import com.spring.step3.security.service.JwtTokenService;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; 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.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; 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.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
@ -26,52 +32,91 @@ import java.io.IOException;
@RequiredArgsConstructor @RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter { public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtBearTokenService jwtBearTokenService; private final JwtTokenService jwtTokenService;
private final DbUserDetailService userDetailsService; private final DbUserDetailService userDetailsService;
private final SecurityAuthenticationEntryPoint securityAuthenticationEntryPoint;
@Override @Override
protected void doFilterInternal(@NotNull HttpServletRequest request, protected void doFilterInternal(@NotNull HttpServletRequest request,
@NotNull HttpServletResponse response, @NotNull HttpServletResponse response,
@NotNull FilterChain filterChain) throws ServletException, IOException, AuthenticSecurityException { @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直接返回 // 如果当前请求不包含验证Token直接返回
if (authHeader == null || !authHeader.startsWith("Bearer ")) { if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
// throw new SecurityException(ResultCodeEnum.LOGIN_AUTH); throw new AuthenticSecurityException(ResultCodeEnum.LOGIN_AUTH);
return;
} }
// 当前请求的Token // 当前请求的Token
final String jwtToken = authHeader.substring(7); final String jwtToken = authHeader.substring(7);
// 检查当前Token是否过期 try {
if (jwtBearTokenService.isTokenValid(jwtToken)) { // 检查当前Token是否过期
// TODO 抛出异常 Security 未处理 if (jwtTokenService.isExpired(jwtToken)) {
throw new AuthenticSecurityException(ResultCodeEnum.AUTHENTICATION_EXPIRED); // 💡如果过期不需要进行判断和验证需要直接放行可以像下面这样写
// ===================================================
// 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);
} }
// IMPORTANT:
// 解析当前Token中的用户名 // ==========================================================================
final String username = jwtBearTokenService.getUsernameFromToken(jwtToken); // catch 块中securityAuthenticationEntryPoint.commence() 已经处理了错误响应
final Long userId = jwtBearTokenService.getUserIdFromToken(jwtToken); // 所以应该 直接返回避免继续执行后续逻辑
// ==========================================================================
// 当前用户名存在并且 Security上下文为空设置认证相关信息 catch (RuntimeException exception) {
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { securityAuthenticationEntryPoint.commence(
// 调用用户信息进行登录 request,
UserDetails userDetails = userDetailsService.loadUserByUsername(username); response,
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( new MyAuthenticationException(exception.getMessage(), exception)
userDetails,
null,
userDetails.getAuthorities()
); );
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.Result;
import com.spring.step3.domain.vo.result.ResultCodeEnum; 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 com.spring.step3.utils.ResponseUtil;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@ -21,14 +21,16 @@ import org.springframework.util.StringUtils;
@RequiredArgsConstructor @RequiredArgsConstructor
public class JwtTokenLogoutHandler implements LogoutHandler { public class JwtTokenLogoutHandler implements LogoutHandler {
private final JwtBearTokenService jwtBearTokenService; private final JwtTokenService jwtTokenService;
@Override @Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
// 在这里可以设置不同的请求头标识符常见的AuthorizationToken等
String authorizationToken = request.getHeader("Authorization"); String authorizationToken = request.getHeader("Authorization");
if (StringUtils.hasText(authorizationToken)) { if (StringUtils.hasText(authorizationToken)) {
// 如果当前用户信息存在redis中可以通过这个进行退出 // 如果当前用户信息存在redis中可以通过这个进行退出
String username = jwtBearTokenService.getUsernameFromToken(authorizationToken); String username = jwtTokenService.getUsernameFromToken(authorizationToken);
log.info("username : {}", username); 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.Result;
import com.spring.step3.domain.vo.result.ResultCodeEnum; import com.spring.step3.domain.vo.result.ResultCodeEnum;
import com.spring.step3.exception.MyAuthenticationException;
import com.spring.step3.utils.ResponseUtil; import com.spring.step3.utils.ResponseUtil;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j @Slf4j
@Component
public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint { public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override @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()); 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); ResponseUtil.out(response, result);
} }
} }

View File

@ -13,7 +13,7 @@ import java.util.Map;
@Configuration @Configuration
@ConfigurationProperties(prefix = "jwt-token") @ConfigurationProperties(prefix = "jwt-token")
public class JwtBearTokenService { public class JwtTokenService {
@Value("${jwtToken.secret}") @Value("${jwtToken.secret}")
public String secret; public String secret;
@ -89,7 +89,7 @@ public class JwtBearTokenService {
* @param token 令牌 * @param token 令牌
* @return 是否国企 * @return 是否国企
*/ */
public boolean isTokenValid(String token) { public boolean isExpired(String token) {
SecretKey secretKey = getSecretKey(); SecretKey secretKey = getSecretKey();
return JwtTokenUtil.isExpired(token, secretKey); 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.domain.vo.result.ResultCodeEnum;
import com.spring.step3.mapper.UserMapper; import com.spring.step3.mapper.UserMapper;
import com.spring.step3.security.service.DbUserDetailService; 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.LoginService;
import com.spring.step3.service.user.strategy.DefaultLoginStrategy; import com.spring.step3.service.user.strategy.DefaultLoginStrategy;
import com.spring.step3.service.user.strategy.LoginContext; import com.spring.step3.service.user.strategy.LoginContext;
@ -30,7 +30,7 @@ import java.util.List;
@RequiredArgsConstructor @RequiredArgsConstructor
public class LoginServiceImpl extends ServiceImpl<UserMapper, UserEntity> implements LoginService { public class LoginServiceImpl extends ServiceImpl<UserMapper, UserEntity> implements LoginService {
private final JwtBearTokenService jwtBearTokenService; private final JwtTokenService jwtTokenService;
private final DbUserDetailService dbUserDetailService; private final DbUserDetailService dbUserDetailService;
private final UserMapper userMapper; private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
@ -73,10 +73,10 @@ public class LoginServiceImpl extends ServiceImpl<UserMapper, UserEntity> implem
List<String> roles = dbUserDetailService.findUserRolesByUserId(userId); List<String> roles = dbUserDetailService.findUserRolesByUserId(userId);
List<String> permission = dbUserDetailService.findPermissionByUserId(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); long expirationMillis = System.currentTimeMillis() + (expiresInSeconds * 1000);
Date date = new Date(expirationMillis); Date date = new Date(expirationMillis);

View File

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

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"> <!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"> <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);
}
}