init
This commit is contained in:
71
zkh-web/pom.xml
Normal file
71
zkh-web/pom.xml
Normal file
@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-framework</artifactId>
|
||||
<version>1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>zkh-web</artifactId>
|
||||
<name>ZKH Web</name>
|
||||
<description>Web components for ZKH framework</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.2.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@ -0,0 +1,33 @@
|
||||
package vip.jcfd.web.config;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
import vip.jcfd.common.core.BizException;
|
||||
import vip.jcfd.common.core.R;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||
|
||||
@ExceptionHandler(value = Exception.class)
|
||||
public R<String> handleException(Exception e) {
|
||||
log.error("服务异常", e);
|
||||
return R.serverError("服务器繁忙,请稍候重试");
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = BizException.class)
|
||||
public R<String> handleBizException(BizException e) {
|
||||
log.error("业务异常", e);
|
||||
return R.error(e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = NoResourceFoundException.class)
|
||||
public R<String> handleNotFoundException(NoResourceFoundException e) {
|
||||
log.error("404异常", e);
|
||||
return new R<>(404, "您访问的地址不存在", false, null);
|
||||
}
|
||||
}
|
||||
29
zkh-web/src/main/java/vip/jcfd/web/config/RedisConfig.java
Normal file
29
zkh-web/src/main/java/vip/jcfd/web/config/RedisConfig.java
Normal file
@ -0,0 +1,29 @@
|
||||
package vip.jcfd.web.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import vip.jcfd.web.config.props.SecurityProps;
|
||||
import vip.jcfd.web.redis.TokenRedisStorage;
|
||||
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
private final SecurityProps securityProps;
|
||||
|
||||
public RedisConfig(SecurityProps securityProps) {
|
||||
this.securityProps = securityProps;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TokenRedisStorage tokenRedisTemplate(RedisConnectionFactory factory, StringRedisTemplate stringRedisTemplate) {
|
||||
TokenRedisStorage tokenRedisStorage = new TokenRedisStorage(securityProps.getDuration(), stringRedisTemplate);
|
||||
tokenRedisStorage.setConnectionFactory(factory);
|
||||
tokenRedisStorage.setValueSerializer(new JdkSerializationRedisSerializer());
|
||||
tokenRedisStorage.setKeySerializer(new StringRedisSerializer());
|
||||
return tokenRedisStorage;
|
||||
}
|
||||
}
|
||||
202
zkh-web/src/main/java/vip/jcfd/web/config/WebSecurityConfig.java
Normal file
202
zkh-web/src/main/java/vip/jcfd/web/config/WebSecurityConfig.java
Normal file
@ -0,0 +1,202 @@
|
||||
package vip.jcfd.web.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.domain.AuditorAware;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
import vip.jcfd.common.core.R;
|
||||
import vip.jcfd.web.config.props.SecurityProps;
|
||||
import vip.jcfd.web.filter.JsonUsernamePasswordAuthenticationFilter;
|
||||
import vip.jcfd.web.filter.TokenFilter;
|
||||
import vip.jcfd.web.redis.TokenRedisStorage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@ConfigurationPropertiesScan(basePackageClasses = {SecurityProps.class})
|
||||
@EnableJpaAuditing
|
||||
@EnableAsync
|
||||
@EnableScheduling
|
||||
public class WebSecurityConfig {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WebSecurityConfig.class);
|
||||
private final SecurityProps securityProps;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final TokenRedisStorage tokenRedisStorage;
|
||||
|
||||
public WebSecurityConfig(SecurityProps securityProps, ObjectMapper objectMapper, TokenRedisStorage tokenRedisStorage) {
|
||||
this.securityProps = securityProps;
|
||||
this.objectMapper = objectMapper;
|
||||
this.tokenRedisStorage = tokenRedisStorage;
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 */30 * * * *")
|
||||
@Async
|
||||
public void scheduleClearExpiredTokens() {
|
||||
tokenRedisStorage.clearExpiredTokens();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuditorAware<String> auditorAware() {
|
||||
return () -> Optional.ofNullable(SecurityContextHolder.getContext())
|
||||
.map(SecurityContext::getAuthentication)
|
||||
.map(Authentication::getName)
|
||||
.or(() -> Optional.of("system"));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public TokenFilter tokenFilter() {
|
||||
return new TokenFilter(tokenRedisStorage);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
|
||||
return configuration.getAuthenticationManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain security(HttpSecurity http, TokenFilter tokenFilter, AuthenticationManager authenticationManager) throws Exception {
|
||||
http.authorizeHttpRequests(config -> {
|
||||
config.requestMatchers(securityProps.getIgnoreUrls()).permitAll();
|
||||
config.anyRequest().authenticated();
|
||||
});
|
||||
CustomAuthenticationEntryPoint authenticationEntryPoint = new CustomAuthenticationEntryPoint(objectMapper, tokenRedisStorage);
|
||||
http.formLogin(config -> {
|
||||
config.loginProcessingUrl("/login");
|
||||
config.failureHandler(authenticationEntryPoint);
|
||||
config.successHandler(authenticationEntryPoint);
|
||||
});
|
||||
http.csrf(AbstractHttpConfigurer::disable);
|
||||
http.logout(config -> {
|
||||
config.addLogoutHandler(new CustomLogoutSuccessHandler(objectMapper, tokenRedisStorage));
|
||||
});
|
||||
http.rememberMe(AbstractHttpConfigurer::disable);
|
||||
http.sessionManagement(AbstractHttpConfigurer::disable);
|
||||
http.exceptionHandling(config -> {
|
||||
config.authenticationEntryPoint(authenticationEntryPoint);
|
||||
config.accessDeniedHandler(new CustomAccessDeniedHandler(objectMapper));
|
||||
});
|
||||
|
||||
http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
JsonUsernamePasswordAuthenticationFilter filter = new JsonUsernamePasswordAuthenticationFilter(objectMapper, authenticationManager);
|
||||
filter.setAuthenticationSuccessHandler(authenticationEntryPoint);
|
||||
http.addFilterAt(filter, UsernamePasswordAuthenticationFilter.class);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
private record CustomAuthenticationEntryPoint(
|
||||
ObjectMapper objectMapper,
|
||||
TokenRedisStorage tokenRedisStorage) implements AuthenticationEntryPoint, AuthenticationFailureHandler, AuthenticationSuccessHandler {
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
|
||||
log.warn("认证失败", authException);
|
||||
R<Object> data = new R<>(HttpServletResponse.SC_UNAUTHORIZED, "未登录", false, null);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
objectMapper.writeValue(response.getWriter(), data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
|
||||
log.warn("登录失败", exception);
|
||||
R<Object> data = new R<>(HttpServletResponse.SC_UNAUTHORIZED, "用户名或密码错误", false, null);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
objectMapper.writeValue(response.getWriter(), data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
|
||||
log.info("用户「{}」登录成功", authentication.getName());
|
||||
String token = UUID.randomUUID().toString();
|
||||
tokenRedisStorage.put(token, authentication);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
R<Object> data = new R<>(HttpServletResponse.SC_OK, "登录成功", true, token);
|
||||
objectMapper.writeValue(response.getWriter(), data);
|
||||
}
|
||||
}
|
||||
|
||||
private record CustomAccessDeniedHandler(ObjectMapper objectMapper) implements AccessDeniedHandler {
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
log.warn("访问被拒绝", accessDeniedException);
|
||||
if (authentication.isAuthenticated()) {
|
||||
log.warn("用户「{}」访问「{}」被拒绝,因为:{}", authentication.getPrincipal(), request.getRequestURI(), accessDeniedException.getMessage());
|
||||
} else {
|
||||
log.warn("匿名用户访问「{}」被拒绝,因为:{}", request.getRequestURI(), accessDeniedException.getMessage());
|
||||
}
|
||||
R<Object> data = new R<>(HttpServletResponse.SC_FORBIDDEN, "无权限", false, null);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
objectMapper.writeValue(response.getWriter(), data);
|
||||
}
|
||||
}
|
||||
|
||||
private record CustomLogoutSuccessHandler(ObjectMapper objectMapper,
|
||||
TokenRedisStorage tokenRedisStorage) implements LogoutHandler {
|
||||
@Override
|
||||
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
||||
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
|
||||
if (header != null && header.startsWith("Bearer ")) {
|
||||
String token = header.substring(7);
|
||||
authentication = tokenRedisStorage.get(token);
|
||||
tokenRedisStorage.remove(token);
|
||||
}
|
||||
if (authentication != null) {
|
||||
log.info("用户「{}」退出成功", authentication.getName());
|
||||
String all = request.getParameter("all");
|
||||
if ("true".equals(all)) {
|
||||
tokenRedisStorage.removeByUserName(authentication.getName());
|
||||
}
|
||||
}
|
||||
R<Object> data = new R<>(HttpServletResponse.SC_OK, "退出成功", true, null);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
try {
|
||||
objectMapper.writeValue(response.getWriter(), data);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package vip.jcfd.web.config.props;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@ConfigurationProperties(prefix = "zkh.security")
|
||||
public class SecurityProps {
|
||||
|
||||
private String[] ignoreUrls;
|
||||
|
||||
private Duration duration;
|
||||
|
||||
public String[] getIgnoreUrls() {
|
||||
return ignoreUrls;
|
||||
}
|
||||
|
||||
public void setIgnoreUrls(String[] ignoreUrls) {
|
||||
this.ignoreUrls = ignoreUrls;
|
||||
}
|
||||
|
||||
public Duration getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public void setDuration(Duration duration) {
|
||||
this.duration = duration;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package vip.jcfd.web.filter;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.ServletInputStream;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class JsonUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public JsonUsernamePasswordAuthenticationFilter(ObjectMapper objectMapper, AuthenticationManager authenticationManager) {
|
||||
super(authenticationManager);
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
private record LoginDTO(String username, String password) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
|
||||
if (!request.getMethod().equals("POST")) {
|
||||
throw new AuthenticationServiceException("登录请求只支持POST");
|
||||
}
|
||||
try {
|
||||
ServletInputStream inputStream = request.getInputStream();
|
||||
LoginDTO loginDTO = objectMapper.readValue(inputStream, LoginDTO.class);
|
||||
UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(loginDTO.username, loginDTO.password);
|
||||
setDetails(request, token);
|
||||
return getAuthenticationManager().authenticate(token);
|
||||
} catch (IOException e) {
|
||||
logger.error("读取请求体失败", e);
|
||||
throw new AuthenticationServiceException("登录请求异常");
|
||||
}
|
||||
}
|
||||
}
|
||||
53
zkh-web/src/main/java/vip/jcfd/web/filter/TokenFilter.java
Normal file
53
zkh-web/src/main/java/vip/jcfd/web/filter/TokenFilter.java
Normal file
@ -0,0 +1,53 @@
|
||||
package vip.jcfd.web.filter;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import vip.jcfd.web.redis.TokenRedisStorage;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class TokenFilter extends OncePerRequestFilter {
|
||||
|
||||
private final TokenRedisStorage tokenRedisStorage;
|
||||
|
||||
public TokenFilter(TokenRedisStorage tokenRedisStorage) {
|
||||
this.tokenRedisStorage = tokenRedisStorage;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
|
||||
// 获取token
|
||||
String token = getToken(request);
|
||||
if (!token.isBlank() && validateToken(token)) {
|
||||
Authentication authentication = extractAuthentication(token);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
boolean validateToken(String token) {
|
||||
return tokenRedisStorage.exists(token);
|
||||
}
|
||||
|
||||
Authentication extractAuthentication(String token) {
|
||||
return tokenRedisStorage.get(token);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
String getToken(HttpServletRequest request) {
|
||||
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (header != null && header.startsWith("Bearer ")) {
|
||||
return header.substring(7);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
package vip.jcfd.web.redis;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class TokenRedisStorage extends RedisTemplate<String, Authentication> {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(TokenRedisStorage.class);
|
||||
private final Duration expire;
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
private static final String TOKEN_KEY_PREFIX = "TOKEN:";
|
||||
private static final String TOKEN_LIST_KEY_PREFIX = TOKEN_KEY_PREFIX + "LIST:";
|
||||
|
||||
public TokenRedisStorage(Duration expire, StringRedisTemplate stringRedisTemplate) {
|
||||
this.expire = expire;
|
||||
this.stringRedisTemplate = stringRedisTemplate;
|
||||
}
|
||||
|
||||
public Authentication get(String token) {
|
||||
return opsForValue().get(TOKEN_KEY_PREFIX + token);
|
||||
}
|
||||
|
||||
public void put(String token, Authentication authentication) {
|
||||
opsForValue().set(TOKEN_KEY_PREFIX + token, authentication, expire);
|
||||
stringRedisTemplate.opsForList().leftPush(TOKEN_LIST_KEY_PREFIX + authentication.getName(), token);
|
||||
}
|
||||
|
||||
public void remove(String token) {
|
||||
Authentication authentication = get(token);
|
||||
if (authentication != null) {
|
||||
stringRedisTemplate.opsForList().remove(TOKEN_LIST_KEY_PREFIX + authentication.getName(), 0, token);
|
||||
expire(TOKEN_KEY_PREFIX + token, Duration.ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean exists(String token) {
|
||||
return opsForValue().get(TOKEN_KEY_PREFIX + token) != null;
|
||||
}
|
||||
|
||||
public void removeByUserName(String username) {
|
||||
List<String> range = stringRedisTemplate.opsForList().range(TOKEN_LIST_KEY_PREFIX + username, 0, -1);
|
||||
if (CollectionUtils.isEmpty(range)) {
|
||||
return;
|
||||
}
|
||||
for (String s : range) {
|
||||
expire(TOKEN_KEY_PREFIX + s, Duration.ZERO);
|
||||
}
|
||||
expire(TOKEN_LIST_KEY_PREFIX + username, Duration.ZERO);
|
||||
}
|
||||
|
||||
private record TokenStorage(String key, Set<String> tokens) {
|
||||
public void addToken(String token) {
|
||||
tokens.add(token);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearExpiredTokens() {
|
||||
logger.info("开始清理过期token");
|
||||
Set<String> keys = keys(TOKEN_LIST_KEY_PREFIX + "*");
|
||||
if (CollectionUtils.isEmpty(keys)) {
|
||||
logger.info("清理过期token完成");
|
||||
return;
|
||||
}
|
||||
List<TokenStorage> tokenStorages = new ArrayList<>();
|
||||
for (String key : keys) {
|
||||
List<String> range = stringRedisTemplate.opsForList().range(key, 0, -1);
|
||||
if (CollectionUtils.isEmpty(range)) {
|
||||
continue;
|
||||
}
|
||||
TokenStorage tokenStorage = new TokenStorage(key, new HashSet<>());
|
||||
tokenStorages.add(tokenStorage);
|
||||
for (String token : range) {
|
||||
if (!exists(token)) {
|
||||
tokenStorage.addToken(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.info("收集过期token完成,共{}个token", tokenStorages.stream().map(TokenStorage::tokens).mapToInt(Set::size).sum());
|
||||
for (TokenStorage tokenStorage : tokenStorages) {
|
||||
for (String token : tokenStorage.tokens) {
|
||||
stringRedisTemplate.opsForList().remove(tokenStorage.key, 0, token);
|
||||
}
|
||||
}
|
||||
logger.info("清理过期token完成");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user