一个基本功能完整的论坛项目。项目主要功能有:基于邮件激活的注册方式,基于 MD5 加密与加盐的密码存储方式,登陆功能加入了随机验证码的验证。实现登陆状态的检查、为游客和已登录用户展示不同界面与功能。实现不同用户的权限控制和网站数据统计(UV、DAU),管理员可以查看网站数据统计和网站监控信息。支持用户上传头像,实现发布帖子、评论帖子、热帖排行、发送私信与敏感词过滤等功能。实现了点赞关注与系统通知功能。支持全局搜索帖子信息的功能。
项目仓库地址:https://github.com/SageSang/community.git

JDK 17.0.6 + apache-maven-3.9.1 + Spring Boot 3.1.0 + Spring Security 6.1.0 + Redis7.0.11 3 主 3 从集群 + kafka_2.12-2.4.1 集群 + Elasticsearch 7.17.10 + kaptcha2.3.2(验证码工具) + wkhtmltopdf(长图生成工具) + MySQL 8.0.32
SpringBoot + MyBatis + Spring Email + Kaptcha + Redis + Kafka + Elasticsearch + Spring Security + Quartz + wkhtmltopdf + caffeine + Spring Boot Actuator
在 init-sql 中有数据库建表脚本:
在 IDEA 中新增配置类:
@Configuration
public class RedisConfig {
/**
* *redis序列化的工具定置类,下面这个请一定开启配置
* *127.0.0.1:6379> keys *
* *1) “ord:102” 序列化过
* *2)“\xaclxedlxeelx05tixeelaord:102” 野生,没有序列化过
* *this.redisTemplate.opsForValue(); //提供了操作string类型的所有方法
* *this.redisTemplate.opsForList();// 提供了操作List类型的所有方法
* *this.redisTemplate.opsForset(); //提供了操作set类型的所有方法
* *this.redisTemplate.opsForHash(); //提供了操作hash类型的所有方认
* *this.redisTemplate.opsForZSet(); //提供了操作zset类型的所有方法
* param LettuceConnectionFactory
* return
*/
@Bean
public RedisTemplate redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
// 设置key序列化方式string
redisTemplate.setKeySerializer(RedisSerializer.string()); // RedisSerializer.string() 等价于 new StringRedisSerializer()
// 设置value的序列化方式json,使用GenericJackson2JsonRedisSerializer替换默认序列化
redisTemplate.setValueSerializer(RedisSerializer.json()); // RedisSerializer.json() 等价于 new GenericJackson2JsonRedisSerializer()
// 设置hash的key的序列化方式
redisTemplate.setHashKeySerializer(RedisSerializer.string());
// 设置hash的value的序列化方式
redisTemplate.setHashValueSerializer(RedisSerializer.json());
// 使配置生效
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
如果在 Reids 命令行中,可以在启动命令后加-raw来解决序列化问题,例如:
redis-cli -a 123456 -p 6379 -c -raw
浅谈 Redis 的 setNX 分布式锁redisconnection.setnx叁柚木的博客-CSDN 博客
报错:java.lang.IllegalStateException: availableProcessors is already set to [4], rejecting [4]
原因:SpringBoot 的 spring-boot-starter-data-redis 默认是以 lettuce 作为连接池的, 而在 lettuce,elasticsearch transport 中都会依赖 netty, 二者的 netty 版本不一致,不能够兼容。NettyRuntime 类中有下面的方法,启动的时候 Redis 和 ElasticSearch 都会调用,然后就会报下面绿字错误。即 Redis 先设置好了 availableProcessors 处理器,es 又来设置,系统就会认为重复了,就不会启动。

是由 es 调用这段代码所产生的错误!在 es 底层代码 Netty4Utils 类中能看到下面代码,只要调用了红框内的代码,因为 Redis 已经初始化过 availavleProcessors 了,所以不为 0,则 es 就会报错。

解决方案:es 中的处理比较狭隘,别人也可以依赖 netty 呀,所以我们可以修改源码位置留的开关,来达到不报错的目的。这个开关可以在在启动类初始化的时候进行配置,设置为 false 后,就会跳过下面会报错的检查了。

启动类初始化的时候进行配置来解决问题:

还有一种解决方法,直接使用 es7.x ,升级 es7.x 后不会遇到这个问题了。当然,es7 与 es6 的操作差距很大,有很多变化。我采用的是使用 es7 来解决这个问题。
官网文档地址:Persisting Authentication :: Spring Security
原因
springsecurity 持久化分为两个步骤:
而在 springsecurity6.1.0 中使用 SecurityContextHolder 更改 SercurityContext 时,没有上述的第二步,即虽然更改了但是没有保存,下次访问时无法识别更改的内容。
故需要在更改后自己手动保存 SercurityContext 到 securityContextRepository 中(持久化认证)
修改过程
// 在SecurityConfig中增加配置SecurityContextRepository
@Bean
public SecurityContextRepository securityContextRepository() {
return new HttpSessionSecurityContextRepository();
}
// 在LoginTicketInterceptor中注入这个Bean
@Autowired
private SecurityContextRepository securityContextRepository;
// 在LoginTicketInterceptor中preHandle里,修改Context内容后增加保存SercurityContext
SecurityContextHolder.setContext(new SecurityContextImpl(authentication));
securityContextRepository.saveContext(SecurityContextHolder.getContext(), request, response);
原因分析
这是因为在退出的时候也只是清理了 SecurityContextHolder,而认证信息已经存在了 session 里,没有被清理(securityContextRepository 是基于 session 的)
解决措施一(不优雅)
在 logout 里清理 SecurityContextHolder 后,给浏览器的 response 里增加一个对应访问认证信息的 cookie,赋予随机值,覆盖掉原本的 cookie,让浏览器无法访问原本的信息
Cookie cookie = new Cookie("JSESSIONID", CommunityUtil.generateUUID());
response.addCookie(cookie);
解决措施二
在自定义的 logout 功能里调用 LogoutHandler 彻底地清理授权信息。
参考文档地址:https://docs.spring.io/spring-security/reference/servlet/authentication/logout.html#creating-custom-logout-endpoint
具体做法:
在 SecurityConfig 中配置一个 LogoutHandler
@Bean
public SecurityContextLogoutHandler securityContextLogoutHandler() {
return new SecurityContextLogoutHandler();
}
在 LoginController 里注入 securityContextLogoutHandler(代码略)
修改我们的 logout 功能,调用 securityContextLogoutHandler
@GetMapping("/logout")
public String logout(@CookieValue("ticket") String ticket, HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
userService.logout(ticket);
// 加入下面这一句
securityContextLogoutHandler.logout(request, response, authentication);
return "redirect:/login";
}
即执行 redisTemplate.opsForHyperLogLog().union(unionKey, redisKey2, redisKey3, redisKey4); 时报错。
Java 上报错:org.springframework.dao.InvalidDataAccessApiUsageException: All keys must map to same slot for pfmerge in cluster mode
查了很久没有找到有关报错的讨论,于是在 redis 命令行上用命令 PFmerge test:hll:union test:hll:02 test:hll:03 复刻 IDEA 上的操作,也报错了。
Redis 命令行上报错:CROSSSLOT Keys in request don’t hash to the same slot
终于找到原因了,由于不在一个哈希槽的数据不能一起操作,这是为集群的安全性着想。我们可以使用 {} 来解决问题。
在启用集群模式的集群上创建由多密钥操作使用的密钥时,请使用哈希标签将密钥强制放入同一哈希槽中。当密钥包含“{…}”这种样式时,只有大括号“{”和“}”之间的子字符串得到哈希以获得哈希槽。
例如,密钥 {user1}:myset 和 {user1}:myset2 被哈希到相同的哈希槽,因为只有大括号“{”和“}”内的字符串,即“user1”,用于计算哈希槽。
es7 中废除了ElasticsearchTemplate ,需要使用 RestHighLevelClient 来操作。具体见:SpringBoot3 整合 ElasticSearch7 示例springboot 集成 elasticsearch7叁柚木的博客-CSDN 博客
Spring Security 6.1.0 中废除了 WebSecurityConfigurerAdapter。
Spring Security in Spring Boot 3 - Stack Overflow
查询光放文档获得解决方案:
@Configuration
@EnableWebSecurity
public class SecurityConfig implements CommunityConstant {
/**
* 静态资源不做认证
*
* @return
*/
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/resources/**");
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 授权
http.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests
.requestMatchers(
"/user/setting",
"/user/upload",
"/discuss/add",
"/comment/add/**",
"/letter/**",
"/notice/**",
"/like",
"/follow",
"/unfollow"
)
.hasAnyAuthority(
AUTHORITY_USER,
AUTHORITY_ADMIN,
AUTHORITY_MODERATOR
)
.anyRequest()
.permitAll()
);
// 权限不够的时候处理
http.exceptionHandling((exceptionHandling) ->
exceptionHandling
.authenticationEntryPoint(new AuthenticationEntryPoint() {
// 没有登陆
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
String xRequestedWith = request.getHeader("x-requested-with");
if ("XMLHttpRequest".equals(xRequestedWith)) {
response.setContentType("application/plain;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJSONString(403, "请您先登陆呢~"));
} else {
response.sendRedirect(request.getContextPath() + "/login");
}
}
})
.accessDeniedHandler(new AccessDeniedHandler() {
// 权限不足
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
String xRequestedWith = request.getHeader("x-requested-with");
if ("XMLHttpRequest".equals(xRequestedWith)) {
response.setContentType("application/plain;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJSONString(403, "你没有访问此功能的权限!"));
} else {
response.sendRedirect(request.getContextPath() + "/denide");
}
}
})
);
// Security 底层默认会拦截 /logout 请求,进行退出的处理。
// 我们覆盖它默认的逻辑,才能执行我们自己退出的代码
http.logout((logout) ->
logout.logoutUrl("/securitylogout")
);
return http.build();
}
}
刚开始引入的时候突然报 There is no DataSource named 'null’的错误
然后把注释去掉就又能正常执行,一度认为自己哪里写错了,对着视频看了挺久还是没法解决
然后就想到了配置的问题,配置了数据源还是报错找了好久才想到会不会是版本问题,把代码贴一份到以前 2.5 以下的工程里面又能正常执行 orz
2.6.0 spring 以上需把配置数据源实现的 class 从 org.quartz.impl.jdbcjobstore.JobStoreTX 改为 org.springframework.scheduling.quartz.LocalDataSourceJobStore。
# Quartz spring.quartz.job-store-type=jdbc spring.quartz.scheduler-name=communityScheduler spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO #spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX 老版本的设置,2.5.6之后的版本改为下面的配置项了。 spring.quartz.properties.org.quartz.jobStore.class=org.springframework.scheduling.quartz.LocalDataSourceJobStore spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate spring.quartz.properties.org.quartz.jobStore.isClustered=true spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool spring.quartz.properties.org.quartz.threadPool.threadCount=5
如果有帮助到大家的话,留下你的赞和收藏呗 ~