✨ 实现后端校验登录
This commit is contained in:
parent
02901efa33
commit
20e7f71936
File diff suppressed because it is too large
Load Diff
|
@ -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 (.*)";
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
// 在这里可以设置不同的请求头标识符,常见的:Authorization、Token等
|
||||
String authorizationToken = request.getHeader("Authorization");
|
||||
|
||||
if (StringUtils.hasText(authorizationToken)) {
|
||||
// 如果当前用户信息存在redis中可以通过这个进行退出
|
||||
String username = jwtBearTokenService.getUsernameFromToken(authorizationToken);
|
||||
String username = jwtTokenService.getUsernameFromToken(authorizationToken);
|
||||
log.info("username : {}", username);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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">
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue