个人改造过程记录粗略文档。
网关实现:认证、鉴权、登录/退出日志记录
登录方式:用户名密码模式、企业微信模式
1、POM文件部分:
<spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
<spring-boot.version>2.1.4.RELEASE</spring-boot.version>
<!-- SpringCloud Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
记得将引入tomcat的地方去掉,因为springcloudgateway 2.* 使用的是netty,并且springsecurity集成redis的时候,也有冲突。
<exclusion>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
</exclusion>
2、关键入口配置,SecurityConfig.java
@Slf4j
@EnableWebFluxSecurity
public class SecurityConfig {
private static final String ALLOWED_HEADERS = "*";
private static final Long MAX_AGE = 3600L;
@Autowired
private CustomizeAuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private CustomizeAuthenticationFaillHandler authenticationFaillHandler;
@Autowired
private CustomizeLogoutSuccessHandler logoutSuccessHandler;
@Autowired
private CustomHttpBasicServerAuthenticationEntryPoint customHttpBasicServerAuthenticationEntryPoint;
@Autowired
private MySecurityUserDetailsService mySecurityUserDetailsService;
@Autowired
private UrlFilterConfig urlFilterConfig;
@Value("${allowed.origin}")
private String ALLOWED_ORIGIN;
//访问域名 默认进入登录页 http://域名:端口/login(nginx控制的静态页面)
//未登录/超时访问 后台返回鉴权失败 页面跳转登录首页
//登录页
//登录成功 页面控制自行跳转
//登录失败 回跳登录页 http://域名:端口/login(nginx控制的静态页面)
//退出成功 跳转登录首页 默认的 http://域名:端口/login(nginx控制的静态页面)
@Bean
SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) {
http
//登出成功
//指定一个自定义LogoutSuccessHandler 需要操作清理、记录日志
.logout().logoutSuccessHandler(logoutSuccessHandler)
//登出接口
.logoutUrl(Constants.LOGOUT_URL)
.and()
// 登陆地址
.formLogin().loginPage(Constants.LOGIN_URL)
//认证成功
.authenticationSuccessHandler(authenticationSuccessHandler)
//登陆验证失败
.authenticationFailureHandler(authenticationFaillHandler)
//基于http的接口请求鉴权失败
.and().exceptionHandling().authenticationEntryPoint(customHttpBasicServerAuthenticationEntryPoint)
.and()
.authorizeExchange()
//无需进行权限过滤的请求路径
.pathMatchers(WhitePathMatchUtils.getWhiteList(urlFilterConfig)).permitAll()
//option 请求默认放行
.pathMatchers(HttpMethod.OPTIONS).permitAll()
//其他 均需要登录访问
.anyExchange().authenticated()
//必须支持跨
.and().csrf().disable()
;
//在这里过滤当前用户是否具有该URL的访问权限(登录状态的,在认证状态层判断)
http.addFilterAt(new CustomAuthorizationWebFilter(),SecurityWebFiltersOrder.AUTHENTICATION);
http.addFilterAt(new SwitchUserWebFilter(userDetailsService(), authenticationSuccessHandler, authenticationFaillHandler),SecurityWebFiltersOrder.LAST);
http.addFilterAt(corsFilter(),SecurityWebFiltersOrder.CORS);
SecurityWebFilterChain chain = http.build();
Iterator<WebFilter> weIterable = chain.getWebFilters().toIterable().iterator();
while(weIterable.hasNext()) {
WebFilter f = weIterable.next();
if(f instanceof AuthenticationWebFilter) {
AuthenticationWebFilter webFilter = (AuthenticationWebFilter) f;
//将自定义的AuthenticationConverter添加到过滤器中
webFilter.setServerAuthenticationConverter(new WorkXinAuthenticationConverter());
}
}
return chain;
}
@Bean
public PasswordEncoder md5PasswordEncoder() {
return Md5PasswordEncoder.getInstance();
}
@Bean
ReactiveAuthenticationManager reactiveAuthenticationManager() {
final ReactiveUserDetailsService detailsService = userDetailsService();
LinkedList<ReactiveAuthenticationManager> managers = new LinkedList<>();
managers.add(authentication -> {
if(authentication instanceof UsernamePasswordAuthenticationToken){
}else if(authentication instanceof WorkXinAccountAuthenticationToken){
//根据wxCode 去企业微信获取手机号,并去数据库查询是否存在该用户,如果存在则组装一个 loginUser
WorkXinAccountAuthenticationToken workXinAccountAuthenticationToken = mySecurityUserDetailsService.getAccoutAuthenticationToken(authentication.getName());
if(workXinAccountAuthenticationToken.getPrincipal() != null) {
//workXinAccountAuthenticationToken.setAuthenticated(true);
ReferenceCountUtil.release(workXinAccountAuthenticationToken);
return Mono.just(workXinAccountAuthenticationToken);
}
//return Mono.just(authentication);
}
// 其他登陆方式 (比如手机号验证码登陆) 可在此设置不得抛出异常或者 Mono.error
return Mono.empty();
});
// 必须放最后不然会优先使用用户名密码校验但是用户名密码不对时此 AuthenticationManager 会调用 Mono.error 造成后面的 AuthenticationManager 不生效
UserDetailsRepositoryReactiveAuthenticationManager reactiveAuthenticationManager = new UserDetailsRepositoryReactiveAuthenticationManager(detailsService);
reactiveAuthenticationManager.setPasswordEncoder(md5PasswordEncoder());
managers.add(reactiveAuthenticationManager);
return new DelegatingReactiveAuthenticationManager(managers);
}
/**
* 自定义获取用户信息,此处使用mysql基于RBAC模式
* @return
*/
@Bean
public ReactiveUserDetailsService userDetailsService() {
return new SecurityUserDetailsServiceImpl();
}
/**
* 重写 org.springframework.web.server.session.CookieWebSessionIdResolver
* 获取登录成功后的 session/cookie id重写 CustomCookieWebSessionIdResolver
* @return
*/
@Bean
CustomCookieWebSessionIdResolver cookieWebSessionIdResolver(){
return new CustomCookieWebSessionIdResolver();
}
/**
* 重写 org.springframework.web.server.session.DefaultWebSessionManager
* config类为 org.springframework.session.config.annotation.web.server.SpringWebSessionConfiguration
* 用于 WebHttpHandlerBuilder 中 context.getBean(WEB_SESSION_MANAGER_BEAN_NAME, WebSessionManager.class))
* 涉及到Bean覆盖需配置 spring.main.allow-bean-definition-overriding=true
* @return
*/
@Bean({"webSessionManager"})
public WebSessionManager webSessionManager(ReactiveSessionRepository<? extends Session> repository) {
ReactiveRedisOperationsSessionRepository redisOperationsSessionRepository = (ReactiveRedisOperationsSessionRepository) repository;
redisOperationsSessionRepository.setRedisKeyNamespace(CRM_SESSION_REDIS_KEY);
SpringSessionWebSessionStore<? extends Session> sessionStore = new SpringSessionWebSessionStore(redisOperationsSessionRepository);
CustomDefaultWebSessionManager manager = new CustomDefaultWebSessionManager();
manager.setSessionStore(sessionStore);
manager.setSessionIdResolver(cookieWebSessionIdResolver());
return manager;
}
@Bean
public WebFilter corsFilter() {
return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
if (CorsUtils.isCorsRequest(request)) {
ServerHttpResponse response = ctx.getResponse();
HttpHeaders headers = response.getHeaders();
headers.setAccessControlAllowCredentials(true);
headers.setAccessControlAllowOrigin(ALLOWED_ORIGIN);
List<HttpMethod> result = new ArrayList<>();
result.add(HttpMethod.GET);
result.add(HttpMethod.POST);
result.add(HttpMethod.OPTIONS);
result.add(HttpMethod.PUT);
result.add(HttpMethod.DELETE);
headers.setAccessControlAllowMethods(result);
headers.setAccessControlMaxAge(MAX_AGE);
List<String> allowedHeaders = new ArrayList<>();
allowedHeaders.add(ALLOWED_HEADERS);
headers.setAccessControlAllowHeaders(allowedHeaders);
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
}
return chain.filter(ctx);
};
}
}
3、关键 redis配置
@Configuration
@EnableRedisWebSession
public class RedisSessionConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.database}")
private int database;
@Value("${spring.redis.lettuce.pool.max-idle}")
private int maxIdle;
@Value("${spring.redis.lettuce.pool.min-idle}")
private int minIdle;
@Value("${spring.redis.lettuce.pool.max-active}")
private int maxActive;
@Value("${spring.redis.lettuce.pool.max-wait}")
private long maxWait;
/**
* 解决redis集群环境没有开启Keyspace notifications导致的
* <p>
* Error creating bean with name 'enableRedisKeyspaceNotificationsInitializer' defined in class path resource
*/
@Bean
public static ConfigureRedisAction configureRedisAction() {
return ConfigureRedisAction.NO_OP;
}
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
// 单机版配置
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setDatabase(database);
redisStandaloneConfiguration.setHostName(host);
redisStandaloneConfiguration.setPort(port);
redisStandaloneConfiguration.setPassword(RedisPassword.of(password));
LettuceClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
.commandTimeout(Duration.ofMillis(timeout))
.poolConfig(genericObjectPoolConfig())
.build();
LettuceConnectionFactory factory = new LettuceConnectionFactory(redisStandaloneConfiguration,clientConfig);
return factory;
}
/**
* GenericObjectPoolConfig 连接池配置
*
* @return
*/
@Bean
public GenericObjectPoolConfig genericObjectPoolConfig() {
GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
genericObjectPoolConfig.setMaxIdle(maxIdle);
genericObjectPoolConfig.setMinIdle(minIdle);
genericObjectPoolConfig.setMaxTotal(maxActive);
genericObjectPoolConfig.setMaxWaitMillis(maxWait);
genericObjectPoolConfig.setTestOnBorrow(true);
return genericObjectPoolConfig;
}
@Bean(name = "redisTemplate")
public RedisTemplate<Object, Object> initRedisTemplate(){
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
//使用StringRedisSerializer来序列化和反序列化redis的key值
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 值采用json序列化
redisTemplate.setValueSerializer(jacksonSeial);
// 设置hash key 和value序列化模式
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jacksonSeial);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
4、各种handler自定义实现
登录成功:CustomizeAuthenticationSuccessHandler,实现ServerAuthenticationSuccessHandler
@Slf4j
@Component
public class CustomizeAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
ServerWebExchange exchange = webFilterExchange.getExchange();
ServerHttpResponse response = exchange.getResponse();
//设置body
byte[] dataBytes = {};
ObjectMapper mapper = new ObjectMapper();
try {
//编写业务逻辑、根据请求实现返回/直接重定向跳转
dataBytes = mapper.writeValueAsBytes(Result.buildSuccess());
} catch (Exception ex) {
ex.printStackTrace();
try {
dataBytes = mapper.writeValueAsBytes(Result.buildFailure(ErrorStatus.AUTHORIZATION_EXCEPTION.getValue(), ErrorStatus.AUTHORIZATION_EXCEPTION.getMessage()));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(dataBytes);
ReferenceCountUtil.release(bodyDataBuffer);
return response.writeWith(Mono.just(bodyDataBuffer));
}
}
登录失败:CustomizeAuthenticationFaillHandler实现ServerAuthenticationFailureHandler
@Slf4j
@Component
public class CustomizeAuthenticationFaillHandler implements ServerAuthenticationFailureHandler {
@Override
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) {
ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
if (exception instanceof UsernameNotFoundException) {
return writeErrorMessage(response, ErrorStatus.USER_NOT_EXISTS);
} else if (exception instanceof BadCredentialsException) {
return writeErrorMessage(response, ErrorStatus.USERNAME_PASSWORD_ERROR);
} else if (exception instanceof LockedException) {
return writeErrorMessage(response, ErrorStatus.USER_LOCKED);
} else if(exception instanceof WorkWxUsernameNotFoundException){
return redirectMessage(response, ErrorStatus.WORKWX_USER_NOT_EXISTS);
}
return writeErrorMessage(response, ErrorStatus.INTERNAL_SERVER_ERROR);
}
/**
* 用户名 密码模式登录失败 返回JSON格式错误
* @param response
* @param errorEnum
* @return
*/
private Mono<Void> writeErrorMessage(ServerHttpResponse response, ErrorStatus errorEnum) {
//设置headers
response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
response.getHeaders().setCacheControl(CACHE_CONTRTOL_VALUE);
//设置body
byte[] dataBytes={};
try {
ObjectMapper mapper = new ObjectMapper();
dataBytes=mapper.writeValueAsBytes(Result.buildFailure(errorEnum.getValue(),errorEnum.getMessage()));
}
catch (Exception ex){
ex.printStackTrace();
}
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(dataBytes);
log.info("LOGIN FAILD");
return response.writeWith(Mono.just(bodyDataBuffer));
}
/**
* 企业微信登录失败 重定向静态登录页
* @param response
* @param errorEnum
* @return
*/
private Mono<Void> redirectMessage(ServerHttpResponse response, ErrorStatus errorEnum) {
//设置Location (重定向地址)
response.getHeaders().set(HttpHeaders.LOCATION,Constants.LOGIN_FAIL_URL);
//设置状态码(302 重定向)
response.setStatusCode(HttpStatus.FOUND);
//设置body
byte[] dataBytes={};
try {
ObjectMapper mapper = new ObjectMapper();
dataBytes=mapper.writeValueAsBytes(Result.buildFailure(errorEnum.getValue(),errorEnum.getMessage()));
}
catch (Exception ex){
ex.printStackTrace();
}
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(dataBytes);
log.info("LOGIN FAILD");
return response.writeWith(Mono.just(bodyDataBuffer));
}
}
除登录、退出、匿名访问外的其他请求,访问失败:CustomHttpBasicServerAuthenticationEntryPoint实现HttpBasicServerAuthenticationEntryPoint
@Component
public class CustomHttpBasicServerAuthenticationEntryPoint extends HttpBasicServerAuthenticationEntryPoint {
private static String WWW_AUTHENTICATE_FORMAT = "Basic realm=\"%s\"";
private String headerValue = createHeaderValue("Realm");
public CustomHttpBasicServerAuthenticationEntryPoint() {
}
@Override
public void setRealm(String realm) {
this.headerValue = createHeaderValue(realm);
}
private static String createHeaderValue(String realm) {
Assert.notNull(realm, "realm cannot be null");
return String.format(WWW_AUTHENTICATE_FORMAT, new Object[]{realm});
}
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
//设置body
byte[] dataBytes={};
try {
ObjectMapper mapper = new ObjectMapper();
dataBytes=mapper.writeValueAsBytes(Result.buildFailure(ErrorStatus.AUTHORIZED_FAILURE.getValue(),ErrorStatus.AUTHORIZED_FAILURE.getMessage()));
}
catch (Exception ex){
ex.printStackTrace();
}
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(dataBytes);
return response.writeWith(Mono.just(bodyDataBuffer));
}
}
退出成功:CustomizeLogoutSuccessHandler实现ServerLogoutSuccessHandler
@Slf4j
@Component
public class CustomizeLogoutSuccessHandler implements ServerLogoutSuccessHandler {
@Override
public Mono<Void> onLogoutSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
ServerWebExchange exchange = webFilterExchange.getExchange();
ServerHttpResponse response = exchange.getResponse();
// 设置header
response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
response.getHeaders().setCacheControl(CACHE_CONTRTOL_VALUE);
//设置body
byte[] dataBytes={};
try {
if(null != authentication) {
if(authentication.isAuthenticated()){
authentication.setAuthenticated(false);
}
}
ObjectMapper mapper = new ObjectMapper();
dataBytes=mapper.writeValueAsBytes(Result.buildSuccess());
}
catch (Exception ex){
ex.printStackTrace();
}
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(dataBytes);
log.info("LOGOUT SUCCESS");
return response.writeWith(Mono.just(bodyDataBuffer));
}
}
5、自定义的AuthenticationConverter,处理多种登录方式,自行解决用户名、密码对应关系
@Slf4j
public class WorkXinAuthenticationConverter extends ServerFormLoginAuthenticationConverter {
private static final String usernameParameter = "username";
private static final String passwordParameter = "password";
private static final String workWxCode = "code";
@Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
//根据请求类型中 参数不同 来区分
String logintype = exchange.getRequest().getQueryParams().getFirst(workWxCode);
String wxCode = exchange.getRequest().getQueryParams().getFirst(workWxCode);
return exchange.getFormData()
.map( data -> createAuthentication(data,logintype,wxCode));
}
private AbstractAuthenticationToken createAuthentication(
MultiValueMap<String, String> data,String logintype, String wxCode) {
String username = data.getFirst(usernameParameter);
String password = data.getFirst(passwordParameter);
if(StringUtils.isEmpty(logintype)){
return new UsernamePasswordAuthenticationToken(username, password);
}else{
return new WorkXinAccountAuthenticationToken(wxCode, null);
}
}
}
6、自定义用户信息LoginUser 扩展 org.springframework.security.core.userdetails.User,自定义所需字段。
7、用户名密码模式的信息查询:SecurityUserDetailsServiceImpl实现ReactiveUserDetailsService
/**
* 用户名 密码模式
* 自定义获取用户信息,此处使用mysql基于RBAC模式
*/
@Slf4j
public class SecurityUserDetailsServiceImpl implements ReactiveUserDetailsService {
@Autowired
private AuthUserService authUserService;
@Autowired
private AssemblyInfoServiceImpl customUserDetailsService;
/**
* 获取用户信息,并组装登录用户信息
* @param username
* @return
* @throws AuthenticationException
*/
@Override
public Mono<UserDetails> findByUsername(String username) throws AuthenticationException {
AuthUserDO authUserDO = authUserService.getByAuthUid(username);
if (StringUtils.isNull(authUserDO)) {
return Mono.error(new UsernameNotFoundException("User Not Found"));
}
if(StringUtils.isEmpty(username)){
return Mono.error(new UsernameNotFoundException(ErrorStatus.MISSING_USERNAME.getMessage()));
}
if(null == authUserDO){
return Mono.error(new UsernameNotFoundException(ErrorStatus.USER_NOT_EXISTS.getMessage()));
}
return Mono.just(customUserDetailsService.getUserDetails(authUserDO,""));
}
}
企业微信模式:自定义:MySecurityUserDetailsServiceImpl,实现自定义接口MySecurityUserDetailsService
@Slf4j
@Service
public class MySecurityUserDetailsServiceImpl implements MySecurityUserDetailsService {
@Autowired
private AuthUserService authUserService;
@Autowired
private AssemblyInfoServiceImpl customUserDetailsService;
@Override
public WorkXinAccountAuthenticationToken getAccoutAuthenticationToken(String wxCode) {
//再根据手机号去数据库查询用户信息,再根据用户信息获取菜单权限
AuthUserDO authUserDO = authUserService.getByWechatId(wxCode);
//做一些 用户信息校验
//用户信息 (取头像)
String avatar = "";
UserDetails userDetails = findByUsername(authUserDO,avatar);
WorkXinAccountAuthenticationToken workXinAccountAuthenticationToken = new WorkXinAccountAuthenticationToken(userDetails, null, userDetails.getAuthorities());
return workXinAccountAuthenticationToken;
}
/**
* 获取用户信息,并组装登录用户信息
* @param authUserDO
* @return
* @throws AuthenticationException
*/
@Override
public UserDetails findByUsername(AuthUserDO authUserDO, String avatar) throws AuthenticationException {
return customUserDetailsService.getUserDetails(authUserDO,avatar);
}
}
公用组装UserDetails的位置:
/**
* 统一组装登录用户信息
* @param authUserDO
* @return
*/
public UserDetails getUserDetails(AuthUserDO authUserDO,String avatar){
//根据用户信息 获取权限列表、角色列表
UserInfoVo info = assemblyUserInfo(authUserDO);
Set<String> dbAuthsSet = new HashSet<String>();
if (StringUtils.isNotEmpty(info.getRoles()))
{
dbAuthsSet.addAll(info.getPermissions());
}
Collection<? extends GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(dbAuthsSet.toArray(new String[0]));
AuthUserDO user = info.getAuthUserDO();
if (user != null) {
Md5PasswordEncoder encoder = Md5PasswordEncoder.getInstance();
//这里将XXX作为盐值设置到 Md5PasswordEncoder
encoder.setSalt(user.getSalt());
}
return new LoginUser(user.getUserId(), user.getSalt(), user.getPassword(), true, true, true, true,
authorities, info.getRoles(),info.getRoleName(), user,avatar);
}
新建企业微信TOKEN,可参照默认用户名密码的Token
WorkXinAccountAuthenticationToken扩展AbstractAuthenticationToken
8、redis配置
################# Redis 基础配置 #################
spring.redis.host=127.0.0.1
spring.redis.password=redis
spring.redis.port=6379
spring.redis.database=0
#连接超时时间 单位 ms(毫秒)
spring.redis.timeout=3000
# reids托管 session信息使用redis存储
spring.session.store-type=Redis
# redis命名空间 注意阿里云集群时,redis的key使用{},避免落在不同片上
#spring.session.redis.namespace={XXX}spring:session
#redis刷新模式
spring.data.redis.repositories.enabled=true
spring.session.redis.flush-mode=immediate
spring.redis.lettuce.pool.max-active=30
spring.redis.lettuce.pool.max-idle=30
spring.redis.lettuce.pool.max-wait=3000
spring.redis.lettuce.pool.min-idle=5
redis操作文件request_rate_limiter.lua问题:新建自定义的脚本文件
--踩坑 https://blog.csdn.net/qq_33996921/article/details/107204362
--错误
--NOSCRIPT No matching script. Please use EVAL.
--ERR bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS array, and KEYS should not be in expression
--注释掉 tokens_key、timestamp_key 在引用的地址直接填写数组
--local tokens_key = KEYS[1]
--local timestamp_key = KEYS[2]
--redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)
--redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
--redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
--redis.log(redis.LOG_WARNING, "now " .. ARGV[3])
--redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
--redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
--redis.log(redis.LOG_WARNING, "ttl " .. ttl)
local last_tokens = tonumber(redis.call("get", KEYS[1]))
if last_tokens == nil then
last_tokens = capacity
end
--redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)
local last_refreshed = tonumber(redis.call("get", KEYS[2]))
if last_refreshed == nil then
last_refreshed = 0
end
--redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)
local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
new_tokens = filled_tokens - requested
allowed_num = 1
end
--redis.log(redis.LOG_WARNING, "delta " .. delta)
--redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
--redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
--redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)
redis.call("setex", KEYS[1], ttl, new_tokens)
redis.call("setex", KEYS[2], ttl, now)
return { allowed_num, new_tokens }
其他:
1、如果项修改cookieid:
自定义:CustomCookieWebSessionIdResolver,实现WebSessionIdResolver,修改参数private String cookieName=“XXXX”