Bootstrap

【SpringSecurity】基本开发流程

概要

security-spring-boot-starter

整体架构流程

模块代码截图
在这里插入图片描述
表结构

实现流程

1、编写各种Handler

1.1 AccessDeniedHandler 处理当访问被拒绝时的逻辑

import com.core.web.Web;
import com.core.web.result.Result;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class myAccessDeniedHandler implements AccessDeniedHandler {

	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
		Result<Object> result = Result.fail();
		if (accessDeniedException != null) {
			result.setMessage(accessDeniedException.getLocalizedMessage());
		}
		Web.Response.json(response, HttpStatus.FORBIDDEN, result);
	}
}

1.2 AuthenticationEntryPoint 处理未认证逻辑

import com.core.web.Web;
import com.core.web.result.Result;
import com.core.web.result.ResultCode;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class myAuthenticationEntryPoint implements AuthenticationEntryPoint {

	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
		Result<Object> result = Result.build(ResultCode.UNAUTHORIZED);

		if (authException != null && !(authException instanceof InsufficientAuthenticationException)) {
			result.setMessage(authException.getLocalizedMessage());
		}

		Web.Response.json(response, HttpStatus.UNAUTHORIZED, result);
	}
}

1.3 AuthenticationFailureHandler 处理认证失败逻辑

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class myAuthenticationFailureHandler implements AuthenticationFailureHandler {
	@Override
	public void onAuthenticationFailure(
		HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
		throws IOException, ServletException {

		Web.Response.json(response, Result.fail(exception.getLocalizedMessage()));
	}
}

1.4 AuthenticationSuccessHandler 处理认证成功逻辑

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class myAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

	@Override
	public void onAuthenticationSuccess(
		HttpServletRequest request, HttpServletResponse response, Authentication authentication)
		throws IOException, ServletException {

		LoginUser user = (LoginUser) authentication.getPrincipal();
		AccessTokenManager.create(user);

		Web.Response.json(response, Result.ok(user));
	}
}

1.5 LogoutSuccessHandler 处理退出登录逻辑

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
@EnableConfigurationProperties(TokenProperties.class) // 如何不写这个注解可以采用写在第五步的 CommonConfig
public class JsonLogoutSuccessHandler implements LogoutSuccessHandler {

	@Resource
	private TokenProperties tokenProperties ;


	@Override
	public void onLogoutSuccess(
		HttpServletRequest request, HttpServletResponse response, Authentication authentication)
		throws IOException, ServletException {

		String token = request.getHeader("Authorization");
		LoginUser user = AccessTokenManager.getLoginUser(token);

		if($.isNotNull(user) && !StringUtils.equals(tokenProperties.getScreen(), token)){
			AccessTokenManager.remove(token);
		}
		Web.Response.json(response, Result.ok());
	}
}

2 、AccessToken处理器

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.InitializingBean;

import static com.zyc.core.Constants.Separator.COLON;


@RequiredArgsConstructor
public class AccessTokenManager implements InitializingBean {

	private static AccessTokenManager MANAGER;

	public static AccessTokenManager getInstance() {
		return MANAGER;
	}

	/**
	 * 过期时间(单位:秒)
	 */
	private static final long EXPIRE_IN_SECONDS = 24 * 60 * 60;
	/**
	 * 访问凭证缓存键
	 */
	public static final String TOKEN_CACHE_KEY = "token_to_user" + COLON;

	/**
	 * 根据访问凭证获取登录用户信息
	 *
	 * @param accessToken 访问凭证
	 * @param <T>         用户信息类型
	 * @return 登录用户信息
	 */
	public static <T extends LoginUser> T getLoginUser(String accessToken) {
		T user = RedisCache.get(getInstance().getCacheKey(accessToken));

		if (user == null) {
			return null;
		}

		refresh(accessToken, user);
		return user;
	}


	public static void remove(String accessToken) {
		RedisCache.del(getInstance().getCacheKey(accessToken));
	}


	public static String create(LoginUser user) {
		String token = $.id();
		user.setToken(token);
		refresh(token, user);
		return token;
	}

	public static void refresh(String accessToken, LoginUser user) {
		if ($.isBlank(accessToken)) {
			return;
		}

		boolean isSuperAdmin = SecureContextHolder.hasSuperAdmin(user.getPermissions());
		if (isSuperAdmin) {
			RedisCache.set(getInstance().getCacheKey(accessToken), user);
		} else {
			RedisCache.set(getInstance().getCacheKey(accessToken), user, EXPIRE_IN_SECONDS);
		}

	}

	String getCacheKey(String accessToken) {
		return TOKEN_CACHE_KEY.concat(accessToken);
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		MANAGER = this;
	}
}

3、定义AuthenticationFilter 继承 OncePerRequestFilter (OncePerRequestFilter是Spring提供的一个过滤器基类,它确保了在一次完整的HTTP请求中,无论请求经过多少次内部转发,过滤器的逻辑都只会被执行一次。这对于需要在请求处理之前或之后进行一次性设置或清理资源的场景特别有用。)

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
 * 认证过滤
 * <p>
 * 用于从请求信息 {@link HttpServletRequest} 获取访问凭证,获取登录信息,设置到安全认证上下文中;
 * 以实现登录
 * </p>
 * <p>
 * set before at {@link org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter}
 * </p>
 *
 */
@Slf4j
public class AuthenticationFilter extends OncePerRequestFilter {

	@Override
	protected void doFilterInternal(
		HttpServletRequest request, HttpServletResponse response, FilterChain chain)
		throws ServletException, IOException {

		log.debug("[auth.filter] server_name: {} uri: {}, query: {}",
			request.getServerName(), request.getRequestURI(), request.getQueryString());

		Authentication authentication = getAuthentication(request);
		SecurityContextHolder.getContext().setAuthentication(authentication);

		Optional
			.ofNullable(authentication)
			.ifPresent(auth -> {
				List<String> authorities = auth.getAuthorities().stream()
					.map(GrantedAuthority::getAuthority)
					.collect(Collectors.toList());

				ContextHolder.set(SUPER_ADMIN, SecureContextHolder.hasSuperAdmin(authorities));
			});

		//TODO 对参数进行一些过滤校验
		/*ParameterRequestWrapper paramRequest = new ParameterRequestWrapper(request);
		chain.doFilter(paramRequest, response);*/

		chain.doFilter(request, response);
	}

	/**
	 * 获取认证信息
	 *
	 * @param request 请求信息
	 * @return 认证信息
	 */
	private Authentication getAuthentication(HttpServletRequest request) {
		String accessToken = obtainAccessToken(request);
		log.debug("[auth.filter] obtain access_token: {}", accessToken);

		if ($.isBlank(accessToken)) {
			return null;
		}

		LoginUser login = AccessTokenManager.getLoginUser(accessToken);
		return Optional
			.ofNullable(login)
			.map(user -> new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()))
			.orElse(null);
	}

	/**
	 * 获取访问凭证
	 *
	 * @param request 请求信息
	 * @return 访问凭证
	 */
	private String obtainAccessToken(HttpServletRequest request) {
		String token = request.getHeader(TOKEN_HEAD);
		return Optional.ofNullable(token)
			.orElseGet(() -> request.getParameter(TOKEN_PARAM));
	}
}

4、编写SecurityAutoConfiguration 把Handler和AccessTokenManager 注册成bean

import com.security.servlet.AccessTokenManager;
import com.security.servlet.handler.*;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
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.logout.LogoutSuccessHandler;

@EnableConfigurationProperties(SecurityProperties.class) // 如何不写这个注解可以采用写在第五步的 CommonConfig
@Configuration(proxyBeanMethods = false)
@Import(WebSecurityAutoConfiguration.class)
public class SecurityAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean
	public AccessTokenManager accessTokenManager() {
		return new AccessTokenManager();
	}

	@Bean
	@ConditionalOnMissingBean
	public AuthenticationEntryPoint authenticationEntryPoint() {
		return new JsonAuthenticationEntryPoint();
	}

	@Bean
	@ConditionalOnMissingBean
	public AccessDeniedHandler accessDeniedHandler() {
		return new JsonAccessDeniedHandler();
	}

	@Bean
	@ConditionalOnMissingBean
	public AuthenticationFailureHandler authenticationFailureHandler() {
		return new JsonAuthenticationFailureHandler();
	}

	@Bean
	@ConditionalOnMissingBean
	public AuthenticationSuccessHandler authenticationSuccessHandler() {
		return new JsonAuthenticationSuccessHandler();
	}

	@Bean
	@ConditionalOnMissingBean
	public PasswordEncoder passwordEncoder() {
		return PasswordEncoderFactories.createDelegatingPasswordEncoder();
	}

	@Bean
	@ConditionalOnMissingBean
	public LogoutSuccessHandler logoutSuccessHandler() {
		return new JsonLogoutSuccessHandler();
	}
}

5、编写 WebSecurityAutoConfiguration 注册SecurityFilterChain的bean

import com.security.configurers.AuthorizeRequestsCustomizer;
import com.security.servlet.filter.AuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
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.LogoutSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@ConditionalOnWebApplication(
	type = ConditionalOnWebApplication.Type.SERVLET
)
@Configuration(proxyBeanMethods = false)
@RequiredArgsConstructor
public class WebSecurityAutoConfiguration {

	/**
	 * 默认静态资源常量数组
	 */
	private static final String[] DEFAULT_STATIC_RESOURCES = new String[]{
		"/favicon.ico",
		"/**/*.css",
		"/**/*.js",
		"/doc.html",
		"/swagger-ui/**",
		"/swagger-ui.html",
		"/swagger-ui/index.html",
		"/swagger-resources/**",
		"/v3/api-docs",
		"/v3/api-docs/**"
	};

	private final SecurityProperties securityProperties;

	private final AuthenticationSuccessHandler authenticationSuccessHandler;
	private final AuthenticationFailureHandler authenticationFailureHandler;
	private final AuthenticationEntryPoint authenticationEntryPoint;
	private final AccessDeniedHandler accessDeniedHandler;
	private final LogoutSuccessHandler loggingSuccessHandler;

	private final List<AuthorizeRequestsCustomizer> authorizeRequestsCustomizers;

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			.formLogin(form -> form
				.successHandler(authenticationSuccessHandler)
				.failureHandler(authenticationFailureHandler)
			)
			// CSRF 禁用,因为不使用 Session
			.csrf(AbstractHttpConfigurer::disable)
			.sessionManagement(sessionManagement ->
				sessionManagement
					.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
			)
			// 异常处理
			.exceptionHandling(exceptionHandling ->
				exceptionHandling
					.accessDeniedHandler(accessDeniedHandler)
					.authenticationEntryPoint(authenticationEntryPoint)
			)
			// 忽略认证
			.authorizeRequests(auth -> auth
				.antMatchers(DEFAULT_STATIC_RESOURCES).permitAll()
				.antMatchers($.toArray(securityProperties.getIgnoreUrls())).permitAll()
			)
			// 兜底,必须通过认证才能访问
			.authorizeRequests().anyRequest().authenticated()
			.and()
			// 前置通过凭证获取登录信息
			.addFilterBefore(new AuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
			//退出逻辑
			.logout().logoutSuccessHandler(loggingSuccessHandler)
		;

		return http.build();
	}

	AntPathRequestMatcher[] convertRequestMatcher(Stream<String> stream) {
		return stream
			.map(AntPathRequestMatcher::new)
			.collect(Collectors.toList())
			.toArray(new AntPathRequestMatcher[]{});
	}
}

6 辅助类

6.1 公共配置注册(如何不在采用的地方用@EnableConfigurationProperties注解,可以写在公共配置文件里面)
import com..autoconfigure.ConfigProperties;
import com.boot.autoconfigure.security.TokenProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@EnableConfigurationProperties({
	SecurityProperties.class,
	TokenProperties.class
})
@Configuration
public class CommonConfig {
}
6.2忽略url写在配置文件
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.ArrayList;
import java.util.List;

@Data
@ConfigurationProperties("cfg.security")
public class SecurityProperties {

	/**
	 * 忽略 url
	 */
	private List<String> ignoreUrls = new ArrayList<>();
}

特定token配置在配置文件

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "ems.token")
public class TokenProperties {

	private String screen;
}

自定义LoginUser 继承UserDetails

import cn.hutool.core.map.MapUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;

import java.time.LocalDate;
import java.util.Collection;
import java.util.Map;
import java.util.Set;

@Data
@Accessors(chain = true)
public class LoginUser implements UserDetails, CredentialsContainer {

	/**
	 * 主键标识
	 */
	private String id;
	/**
	 * 用户名
	 */
	private String username;
	/**
	 * 密码
	 */
	private String password;
	/**
	 * 昵称
	 */
	private String nickname;

	/**
	 * 权限集合
	 */
	private Set<String> permissions;

	/**
	 * 访问凭证
	 */
	private String token;
	/**
	 * 扩展信息
	 */
	private Map<String, Object> extra;
	/**
	 * 上下文信息
	 */
	private Map<String, Object> context;

	/**
	 * 机构标识
	 */
	private String orgId;
	/**
	 * 机构类型
	 */
	private Integer orgType;
	/**
	 * 机构名称
	 */
	private String orgName;
	/**
	 * 有效期
	 */
	private LocalDate validityPeriod;

	@JsonIgnore
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return AuthorityUtils.createAuthorityList($.toArray(permissions, String.class));
	}

	@JsonIgnore
	@Override
	public String getPassword() {
		return password;
	}

	@JsonIgnore
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	@JsonIgnore
	@Override
	public boolean isAccountNonLocked() {
		return true;
	}

	@JsonIgnore
	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}

	@JsonIgnore
	@Override
	public boolean isEnabled() {
		return true;
	}

	@Override
	public void eraseCredentials() {
		this.password = null;
	}
}
6.3 自定义UserDetailsServiceImpl实现UserDetailsService
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

	private final OperatorService operatorService;
	private final RoleService roleService;
	private final MenuService menuService;
	private final ProjectService projectService;
	private final OrgService orgService;

	@Override
	public UserDetails loadUserByUsername(String username)
		throws UsernameNotFoundException {

		Operator operator = operatorService.lambdaQuery()
			.eq(Operator::getUsername, username)
			.oneOpt()
			.orElseThrow(() -> new UsernameNotFoundException("用户名或者密码错误"));

		List<Role> roles = roleService.listByOperator(operator.getId(), operator.getOrgId());
		boolean isSuperAdmin = roles.stream()
			.anyMatch(role -> $.equals(role.getId(), Constants.Default.SUPER_ADMIN));
		Set<String> roleIds = roles.stream()
			.map(Role::getId)
			.collect(Collectors.toSet());

		// 菜单权限
		Set<String> permissions = menuService.listByRole(
				isSuperAdmin, $.toArray(roleIds, String.class))
			.stream()
			.map(Menu::getPermission)
			.filter(Kit::isNotBlank)
			.collect(Collectors.toSet());

		permissions.addAll(
			roleIds.stream()
				.map(Constants.Default.ROLE_PREFIX::concat)
				.collect(Collectors.toList())
		);

		// 项目权限
		Set<String> projectIds = projectService.listByRole(operator.getOrgId(), isSuperAdmin, roles);
		permissions.addAll(
			projectIds.stream()
				.filter(Kit::isNotBlank)
				.map(Values.PROJECT_PREFIX::concat)
				.collect(Collectors.toList())
		);


		String password = Constants.Security.DEFAULT_ENCRYPTION_SIGNATURE.concat(operator.getPassword());
		Org org = orgService.getById(operator.getOrgId());
		//如果机构设置了有效期,则有效期过后不能登录
		LocalDate now = LocalDate.now();
		if ($.isNotNull(org.getValidityPeriod())) {
			if (now.isAfter(org.getValidityPeriod())) {
				throw new AccountExpiredException("账户过期,请联系管理员");
			}
		}
		LoginUser user = new LoginUser()
			.setUsername(username)
			.setPassword(password)
			.setPermissions(permissions);
		user.setId(operator.getId());
		user.setNickname(operator.getNickname());
		user.setOrgId(operator.getOrgId());
		user.setOrgType(org.getOrgType());
		user.setOrgName(org.getOrgName());
		user.setValidityPeriod(org.getValidityPeriod());
		return user;
	}


}
6.4 自定义redis操作
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.*;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@SuppressWarnings("unchecked")
@Slf4j
public class RedisCache {

	private static RedisCache redisCache;

	private final RedisTemplate<String, Object> redisTemplate;
	private final ValueOperations<String, Object> valueOperations;
	private final HashOperations<String, String, Object> hashOperations;
	private final ListOperations<String, Object> listOperations;
	private final SetOperations<String, Object> setOperations;
	private final ZSetOperations<String, Object> zSetOperations;

	private final String keyPrefix;

	public RedisCache(RedisTemplate<String, Object> redisTemplate, String keyPrefix) {
		this.redisTemplate = redisTemplate;
		this.keyPrefix = keyPrefix;

		this.valueOperations = redisTemplate.opsForValue();
		this.hashOperations = redisTemplate.opsForHash();
		this.listOperations = redisTemplate.opsForList();
		this.setOperations = redisTemplate.opsForSet();
		this.zSetOperations = redisTemplate.opsForZSet();

		redisCache = this;
	}


	/**
	 * 获取缓存
	 *
	 * @param key 键
	 * @param <T> 类型
	 * @return 指定类型值
	 */

	public static <T> T get(String key) {
		return key == null ? null : (T) opsValue().get(obtainKey(key));
	}

	/**
	 * 写入缓存
	 *
	 * @param key   键
	 * @param value 值
	 * @return true:成功
	 */
	public static boolean set(String key, Object value) {
		try {
			opsValue()
				.set(obtainKey(key), value);
			return true;
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return false;
		}
	}

	/**
	 * 写入缓存并设置过期时间
	 *
	 * @param key    键
	 * @param value  值
	 * @param second 过期时间(单位:秒)
	 * @return true:成功
	 */
	public static boolean set(String key, Object value, long second) {
		try {

			if (second > 0) {
				opsValue()
					.set(obtainKey(key), value, second, TimeUnit.SECONDS);
			} else {
				set(obtainKey(key), value);
			}
			return true;
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return false;
		}
	}

	/**
	 * 写入缓存并设置过期时间
	 *
	 * @param key   键
	 * @param value 值
	 * @param time  过期时间
	 * @param unit  时间单位
	 * @return true:成功
	 */
	public static boolean set(String key, Object value, long time, TimeUnit unit) {
		try {

			if (time > 0) {
				opsValue()
					.set(obtainKey(key), value, time, unit);
			} else {
				set(obtainKey(key), value);
			}
			return true;
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return false;
		}
	}

	/**
	 * 设置指定缓存的失效时间
	 *
	 * @param key    键
	 * @param second 过期时间(单位:秒)
	 * @return true:成功
	 */
	public static boolean expire(String key, long second) {
		try {
			if (second > 0) {
				template().expire(obtainKey(key), second, TimeUnit.SECONDS);
			}
			return true;
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return false;
		}
	}

	/**
	 * 获取指定缓存的过期时间
	 *
	 * @param key 键
	 * @return 过期时间;0:代表永久有效
	 */
	public static long getExpire(String key) {
		return Optional.ofNullable(template().getExpire(obtainKey(key), TimeUnit.SECONDS))
			.orElse(0L);
	}

	/**
	 * 判断缓存是否存在
	 *
	 * @param key 键
	 * @return true:存在
	 */
	public static boolean has(String key) {
		try {
			return Optional.ofNullable(template().hasKey(obtainKey(key)))
				.orElse(false);
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return false;
		}
	}

	/**
	 * 删除缓存
	 *
	 * @param key 键(支持多个)
	 */
	public static void del(String... key) {
		if (key != null && key.length > 0) {
			if (key.length == 1) {
				template().delete(obtainKey(key[0]));
			} else {
				template().delete(
					Arrays.stream(key)
						.map(RedisCache::obtainKey)
						.collect(Collectors.toList())
				);
			}
		}
	}

	/**
	 * 递增指定数值
	 *
	 * @param key   键
	 * @param delta 递增数值(大于0)
	 * @return 递增后的数值
	 */
	public static long incr(String key, long delta) {
		if (delta < 0) {
			throw new RuntimeException("递增数值必须大于 0");
		}
		return Optional
			.ofNullable(opsValue().increment(obtainKey(key), delta))
			.orElse(0L);
	}

	/**
	 * 递减指定数值
	 *
	 * @param key   键
	 * @param delta 递减数值(大于0)
	 * @return 递减后的数值
	 */
	public static long decr(String key, long delta) {
		if (delta < 0) {
			throw new RuntimeException("递减数值必须大于 0");
		}
		return Optional
			.ofNullable(opsValue().decrement(obtainKey(key), delta))
			.orElse(0L);
	}

	/**
	 * Hash
	 * 获取指定字段的值
	 *
	 * @param key   键
	 * @param field 字段
	 * @param <T>   类型
	 * @return 指定类型值
	 */

	public static <T> T hget(String key, String field) {
		return (T) opsHash().get(obtainKey(key), field);
	}

	/**
	 * Hash
	 * 设置指定键字段值
	 *
	 * @param key   键
	 * @param field 字段
	 * @param value 值
	 * @return true:成功
	 */
	public static boolean hset(String key, String field, Object value) {
		try {
			opsHash().put(obtainKey(key), field, value);
			return true;
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return false;
		}
	}

	/**
	 * Hash
	 * 设置指定键字段值
	 *
	 * @param key    键
	 * @param field  字段
	 * @param value  值
	 * @param second 过期时间(单位:秒)
	 * @return true:成功
	 */
	public static boolean hset(String key, String field, Object value, long second) {
		try {
			opsHash().put(obtainKey(key), field, value);
			if (second > 0) {
				expire(obtainKey(key), second);
			}
			return true;
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return false;
		}
	}

	/**
	 * Hash
	 * 获取所有的键值
	 *
	 * @param key 键
	 * @return 所有的键值
	 */
	public static <T> Map<String, T> hgetall(String key) {
		return (Map<String, T>) opsHash()
			.entries(obtainKey(key));
	}

	/**
	 * Hash
	 * 设置多个字段值
	 *
	 * @param key 键
	 * @param map 多个字段值
	 * @return true:成功
	 */
	public static <T> boolean hmset(String key, Map<String, T> map) {
		try {
			opsHash().putAll(obtainKey(key), map);
			return true;
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return false;
		}
	}

	/**
	 * Hash
	 * 设置多个字段值并设置过期时间
	 *
	 * @param key    键
	 * @param map    多个字段值
	 * @param second 过期时间(单位:秒)
	 * @return true:成功
	 */
	public static <T> boolean hmset(String key, Map<String, T> map, long second) {
		try {
			opsHash().putAll(obtainKey(key), map);
			if (second > 0) {
				expire(obtainKey(key), second);
			}
			return true;
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return false;
		}
	}

	/**
	 * Hash
	 * 删除指定键指定字段缓存
	 *
	 * @param key    键
	 * @param fields 字段(支持多个)
	 */
	public static void hdel(String key, Object... fields) {
		opsHash().delete(obtainKey(key), fields);
	}

	/**
	 * Hash
	 * 判断指定字段是否存在
	 *
	 * @param key   键
	 * @param field 字段
	 * @return true:成功
	 */
	public static boolean hhas(String key, String field) {
		return opsHash().hasKey(obtainKey(key), field);
	}

	/**
	 * Hash
	 * 递增指定字段数值
	 *
	 * @param key   键
	 * @param field 字段
	 * @param delta 递增数值
	 * @return 递增后的数值
	 */
	public static long hincr(String key, String field, long delta) {
		return opsHash().increment(obtainKey(key), field, delta);
	}

	/**
	 * Hash
	 * 递减指定字段数值
	 *
	 * @param key   键
	 * @param field 字段
	 * @param delta 递减数值
	 * @return 递减后的数值
	 */
	public static long hdecr(String key, String field, long delta) {
		return opsHash().increment(obtainKey(key), field, -delta);
	}

	/**
	 * Set
	 * 添加元素
	 *
	 * @param key    键
	 * @param values 值(支持多个)
	 * @return 成功个数
	 */
	public static long sadd(String key, Object... values) {
		try {
			return Optional
				.ofNullable(opsSet().add(obtainKey(key), values))
				.orElse(0L);
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return 0L;
		}
	}

	/**
	 * Set
	 * 添加元素
	 *
	 * @param key    键
	 * @param second 过期时间(单位:秒)
	 * @param values 值(支持多个)
	 * @return 成功个数
	 */
	public static long sadd(String key, long second, Object... values) {
		try {
			Long count = opsSet().add(obtainKey(key), values);
			if (second > 0) {
				expire(obtainKey(key), second);
			}
			return Optional.ofNullable(count).orElse(0L);
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return 0;
		}
	}

	/**
	 * Set
	 * 获取缓存大小
	 *
	 * @param key 键
	 * @return 大小
	 */
	public static long scard(String key) {
		try {
			return Optional
				.ofNullable(opsSet().size(obtainKey(key)))
				.orElse(0L);
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return 0;
		}
	}

	/**
	 * Set
	 * 移除元素
	 *
	 * @param key    键
	 * @param values 值(支持多个)
	 * @return 移除个数
	 */
	public static long srem(String key, Object... values) {
		try {
			return Optional
				.ofNullable(opsSet().remove(obtainKey(key), values))
				.orElse(0L);
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return 0;
		}
	}

	/**
	 * List
	 * 列表头部插入缓存
	 *
	 * @param key   键
	 * @param value 值
	 * @return true:成功
	 */
	public static boolean lpush(String key, Object value) {
		try {
			opsList().leftPush(obtainKey(key), value);
			return true;
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return false;
		}
	}

	/**
	 * List
	 * 列表头部插入多个缓存
	 *
	 * @param key    键
	 * @param second 过期时间(单位秒)
	 * @param values 值(支持多个)
	 * @return true:成功
	 */
	public static boolean lpush(String key, long second, Object... values) {
		try {
			opsList().leftPushAll(obtainKey(key), values);
			if (second > 0) {
				expire(obtainKey(key), second);
			}
			return true;
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return false;
		}
	}

	/**
	 * List
	 * 列表尾部插入缓存
	 *
	 * @param key   键
	 * @param value 值
	 * @return true:成功
	 */
	public static boolean lrpush(String key, Object value) {
		try {
			opsList().rightPush(obtainKey(key), value);
			return true;
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return false;
		}
	}

	/**
	 * List
	 * 列表尾部插入多个缓存
	 *
	 * @param key    键
	 * @param second 过期时间(单位秒)
	 * @param values 值(支持多个)
	 * @return true:成功
	 */
	public static boolean lrpush(String key, long second, Object... values) {
		try {
			opsList().rightPushAll(obtainKey(key), values);
			if (second > 0) {
				expire(obtainKey(key), second);
			}
			return true;
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return false;
		}
	}

	/**
	 * List
	 * 设置指定索引的值
	 *
	 * @param key   键
	 * @param index 索引
	 * @param value 值
	 * @return true:成功
	 */
	public static boolean lset(String key, long index, Object value) {
		try {
			opsList().set(obtainKey(key), index, value);
			return true;
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return false;
		}
	}

	/**
	 * List
	 * 获取指定范围内的元素
	 *
	 * @param key   键
	 * @param start 开始索引
	 * @param end   结束索引
	 * @param <T>   类型
	 * @return 元素列表
	 */

	public static <T> List<T> lrange(String key, long start, long end) {
		try {
			return (List<T>) opsList().range(obtainKey(key), start, end);
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return null;
		}
	}

	/**
	 * List
	 * 获取指定列表缓存的长度
	 *
	 * @param key 键
	 * @return 长度
	 */
	public static long llen(String key) {
		try {
			return Optional
				.ofNullable(opsList().size(obtainKey(key)))
				.orElse(0L);
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return 0;
		}
	}

	/**
	 * List
	 * 获取指定索引对应的值
	 *
	 * @param key   键
	 * @param index 索引
	 * @param <T>   类型
	 * @return 值
	 */

	public static <T> T lindex(String key, long index) {
		try {
			return (T) opsList().index(obtainKey(key), index);
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return null;
		}
	}

	/**
	 * List
	 * 移动指定个数的元素
	 *
	 * @param key   键
	 * @param count 移除个数
	 * @param value 值
	 * @return 成功移除个数
	 */
	public static long lrem(String key, long count, Object value) {
		try {
			return Optional
				.ofNullable(opsList().remove(obtainKey(key), count, value))
				.orElse(0L);
		} catch (Exception e) {
			log.error(e.getMessage(), e);
			return 0;
		}
	}

	private static String obtainKey(String key) {
		return Optional
			.ofNullable(redisCache.keyPrefix)
			.map(prefix -> prefix + key)
			.orElse(key);
	}

	public static RedisTemplate<String, Object> template() {
		return redisCache.redisTemplate;
	}

	public static ValueOperations<String, Object> opsValue() {
		return redisCache.valueOperations;
	}

	public static HashOperations<String, String, Object> opsHash() {
		return redisCache.hashOperations;
	}

	public static ListOperations<String, Object> opsList() {
		return redisCache.listOperations;
	}

	public static SetOperations<String, Object> opsSet() {
		return redisCache.setOperations;
	}

	@SuppressWarnings({"AlibabaLowerCamelCaseVariableNaming"})
	public static ZSetOperations<String, Object> opsZSet() {
		return redisCache.zSetOperations;
	}

	public static GeoOperations<String, Object> opsGeo() {
		return template().opsForGeo();
	}

	public static StreamOperations<String, Object, Object> opsStream() {
		return template().opsForStream();
	}

	public static ClusterOperations<String, Object> opsCluster() {
		return template().opsForCluster();
	}
}

小结

提示:这里可以添加总结

例如:

提供先进的推理,复杂的指令,更多的创造力。

;