注册登录

1. 发送邮件
在自己邮箱开启SMTP服务,pom.xml中引入依赖

 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId></dependency>

邮箱参数配置
spring 相关配置
spring:
#发送者邮箱相关配置
mail:
# SMTP服务器域名
host: smtp.163.com
# 编码集
default-encoding: UTF-8
# 邮箱用户名
username: csp******@163.com
# 授权码(注意不是邮箱密码!)
password: WDS*******XCQA
# 协议:smtps
protocol: smtps
# 详细配置
properties:
mail:
smtp:
# 设置是否需要认证,如果为true,那么用户名和密码就必须的,
# 如果设置false,可以不设置用户名和密码
# (前提要知道对接的平台是否支持无密码进行访问的)
auth: true
# STARTTLS[1] 是对纯文本通信协议的扩展。
# 它提供一种方式将纯文本连接升级为加密连接(TLS或SSL)
# 而不是另外使用一个端口作加密通信。
starttls:
enable: true
required: true
邮件发送工具类 JavaMailSender createMimeMessage,MimeMessageHelper,setTo,setFrom,setSubject,setText,mailSender.send(helper.getMimeMessage)

@Component
public class MailClient {private static final Logger logger = LoggerFactory.getLogger(MailClient.class);@Autowiredprivate JavaMailSender mailSender;@Value("${spring.mail.username}")private String from;/*** 发送邮件* @param to 收件人* @param subject 邮件主题* @param content 邮件内容*/public void sendMail(String to,String subject,String content){try {MimeMessage message = mailSender.createMimeMessage();MimeMessageHelper helper = new MimeMessageHelper(message);helper.setFrom(from);// 发送者helper.setTo(to);// 接收者helper.setSubject(subject);// 邮件主题helper.setText(content,true);// 邮件内容,第二个参数true表示支持html格式mailSender.send(helper.getMimeMessage());} catch (MessagingException e) {logger.error("发送邮件失败: " + e.getMessage());}}
}

测试发送

@Autowired
private MailClient mailClient;@Test
void test02(){mailClient.sendMail("11xxxxxxx@qq.com","TEST","测试邮件发送!");
}

使用Thymleaf模板引擎发送html格式邮件

 // 激活邮件发送Context context = new Context();// org.thymeleaf.context.Context 包下context.setVariable("email", user.getEmail());// http://csp1999.natapp1.cc/community/activation/用户id/激活码String url = path + contextPath + "/activation/" + user.getId() + "/" + user.getActivatiocontext.setVariable("url", url);String content = templateEngine.process("/mail/activation", context);mailClient.sendMail(user.getEmail(), "激活账号", content);
...



2. 登录
1、登录注册功能的验证码目前是存放在Session中,之后要存入Redis,提高性能,同时也可以解决分布式部署时的Session共享问题!
Session在分布式服务器遇到的问题:分布式服务器一般会使用负载均衡策略,当浏览器首次访问时,假设此时在服务器A上创建了一个Session,而浏览器再次访问时可能会由其他服务器B进行处理,而服务器B可能没有session,此时就会出错。
解决办法:

(1)对于同一个浏览器请求,每次由同一个服务器处理。缺点:有悖“负载均衡”
(2)在所有服务器上都有一个Session备份。缺点:同步耗时、占用空间
(3)把所有Session存在一个单体服务器上。缺点:有悖“分布式”,只能访问一个服务器不仅会产生性能瓶颈,还有挂掉的风险
(4)将数据放到数据库上,同时为了减小例如Mysql数据库要读硬盘的性能影响,用NoSql数据库,如Redis。
2、注册功能的邮件发送,比较费时,用户只能干等待邮件发送成功,这种方式不太友好,因此在后端以多线程的方式,分一个线程去处理邮件发送,进而不影响客户端正常给用户的响应问题,不用让用户在页面卡太长时间!
3、对于登录用户信息判定(比如,账号密码是否错误,用户名是否存在,用户是否激活)等问题,如果每次都查询数据库,效率比较低,为此我们在客户端发送请求——>后端调用数据库,之间加一层 Redis 缓存,来验证用户登录信息是否合法!
拦截器
当很多请求都需要完成相同的功能时,可以用spring中的拦截器拦截请求进行处理。实现接口HandlerInterceptor,有三个方法preHandle、postHandle、afterCompletion,分别是进入控制器前,控制器完成后而视图解析前,全部完成后。

登录功能
注册功能
通过cookie获取user登录信息
客户端通过cookie携带登录凭证向服务器换取user信息,流程如图:

这一流程需要借助拦截器LoginTicketInterceptor 和 LoginRequiredInterceptor实现!
LoginTicketInterceptor.java 登录凭证拦截器

@Component
public class LoginTicketInterceptor implements HandlerInterceptor {@Autowiredprivate UserService userService;@Autowiredprivate HostHolder hostHolder;/*** 请求开始前* @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request,HttpServletResponse response, Object handler) throws Exception {// 从cookie中获取凭证String ticket = CookieUtil.getValue(request, "ticket");if (ticket != null) {// 查询凭证LoginTicket loginTicket = userService.findLoginTicket(ticket);// 检查凭证是否有效if (loginTicket != null && loginTicket.getStatus() == 0&& loginTicket.getExpired().after(new Date())) {// 根据凭证查询用户User user = userService.findUserById(loginTicket.getUserId());// 在本次请求中(当前线程)持有该用户信息(要考虑多线程并发的情况,所以借助ThreadLocal)hostHolder.setUser(user);}}return true;}/*** 执行请求时* @param request* @param response* @param handler* @param modelAndView* @throws Exception*/@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {// 从ThreadLocal 中得到当前线程持有的userUser user = hostHolder.getUser();if (user != null && modelAndView != null) {// 登录用户的信息存入modelAndViewmodelAndView.addObject("loginUser", user);}}*~~每次请求时都要新建一个ThreadLocal来放User,这样性能影响会大吗?
ThreadLocal创建一个线程局部变量,可以说就是个变量,不会很大影响性能的。~~ */*** 请求结束后* @param request* @param response* @param handler* @param ex* @throws Exception*/@Overridepublic void afterCompletion(HttpServletRequest request,HttpServletResponse response, Object handler, Exception ex) throws Exception {// 从ThreadLocal清除数据hostHolder.clear();}
}

LoginRequiredInterceptor.java 登录请求拦截器

@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {@Autowiredprivate HostHolder hostHolder;/*** 请求开始前** @param request* @param response* @param handler* @return* @throws Exception*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 判断handler 是否是 HandlerMethod 类型if (handler instanceof HandlerMethod) {HandlerMethod handlerMethod = (HandlerMethod) handler;// 获取到方法实例Method method = handlerMethod.getMethod();// 从方法实例中获得其 LoginRequired 注解LoginRequired loginRequired = method.getAnnotation(LoginRequired.class);// 如果方法实例上标注有 LoginRequired 注解,但 hostHandler中没有 用户信息则拦截if (loginRequired != null && hostHolder.getUser() == null) {response.sendRedirect(request.getContextPath() + "/login");return false;}}return true;}

将拦截器注册到spring容器中

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {@Autowiredprivate LoginTicketInterceptor loginTicketInterceptor;@Autowiredprivate LoginRequiredInterceptor loginRequiredInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginTicketInterceptor)// 除了静态资源不拦截,其他都拦截.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");registry.addInterceptor(loginRequiredInterceptor)// 除了静态资源不拦截,其他都拦截.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");}
}

文件/头像上传服务器
阿里云OSS文件存储
springboot操作阿里云OSS实现文件上传,下载,删除(附源码)
AliyunOssConfig

// 声明配置类,放入Spring容器
@Configuration
// 指定配置文件位置
@PropertySource(value = {"classpath:application-aliyun-oss.properties"})
// 指定配置文件中自定义属性前缀
@ConfigurationProperties(prefix = "aliyun")
@Data// lombok
@Accessors(chain = true)// 开启链式调用
public class AliyunOssConfig {private String endPoint;// 地域节点private String accessKeyId;private String accessKeySecret;private String bucketName;// OSS的Bucket名称private String urlPrefix;// Bucket 域名private String fileHost;// 目标文件夹// 将OSS 客户端交给Spring容器托管@Beanpublic OSS OSSClient() {return new OSSClient(endPoint, accessKeyId, accessKeySecret);}
}

FileUploadService

@Service("fileUploadService")
public class FileUploadService {// 允许上传文件(图片)的格式private static final String[] IMAGE_TYPE = new String[]{".bmp", ".jpg",".jpeg", ".gif", ".png"};private static final Logger logger = LoggerFactory.getLogger(FileUploadService.class);@Autowiredprivate OSS ossClient;// 注入阿里云oss文件服务器客户端@Autowiredprivate AliyunOssConfig aliyunOssConfig;// 注入阿里云OSS基本配置类/*** 文件上传* 注:阿里云OSS文件上传官方文档链接:https://help.aliyun.com/document_detail/84781.html?spm=a2c4g.11186623.6.749.11987a7dRYVSzn** @param: uploadFile* @return: string* @create: 2020/10/31 14:36* @author: csp1999*/public String upload(MultipartFile uploadFile) {// 获取oss的Bucket名称String bucketName = aliyunOssConfig.getBucketName();// 获取oss的地域节点String endpoint = aliyunOssConfig.getEndPoint();// 获取oss的AccessKeySecretString accessKeySecret = aliyunOssConfig.getAccessKeySecret();// 获取oss的AccessKeyIdString accessKeyId = aliyunOssConfig.getAccessKeyId();// 获取oss目标文件夹String filehost = aliyunOssConfig.getFileHost();// 返回图片上传后返回的urlString returnImgeUrl = "";// 校验图片格式boolean isLegal = false;for (String type : IMAGE_TYPE) {if (StringUtils.endsWithIgnoreCase(uploadFile.getOriginalFilename(), type)) {isLegal = true;break;}}if (!isLegal) {// 如果图片格式不合法logger.info("图片格式不符合要求...");}// 获取文件原名称String originalFilename = uploadFile.getOriginalFilename();// 获取文件类型String fileType = originalFilename.substring(originalFilename.lastIndexOf("."));// 新文件名称String newFileName = UUID.randomUUID().toString() + fileType;// 构建日期路径, 例如:OSS目标文件夹/2020/10/31/文件名String filePath = new SimpleDateFormat("yyyy/MM/dd").format(new Date());// 文件上传的路径地址String uploadImgeUrl = filehost + "/" + filePath + "/" + newFileName;// 获取文件输入流InputStream inputStream = null;try {inputStream = uploadFile.getInputStream();} catch (IOException e) {e.printStackTrace();}/*** 下面两行代码是重点坑:* 现在阿里云OSS 默认图片上传ContentType是image/jpeg* 也就是说,获取图片链接后,图片是下载链接,而并非在线浏览链接,* 因此,这里在上传的时候要解决ContentType的问题,将其改为image/jpg*/ObjectMetadata meta = new ObjectMetadata();meta.setContentType("image/jpg");//文件上传至阿里云OSSossClient.putObject(bucketName, uploadImgeUrl, inputStream, meta);/*** 注意:在实际项目中,文件上传成功后,数据库中存储文件地址*/// 获取文件上传后的图片返回地址returnImgeUrl = "http://" + bucketName + "." + endpoint + "/" + uploadImgeUrl;return returnImgeUrl;}
}

注册用户

随机字符串生成:UUID
密码加密:对字符串后面接一个salt,再用MD5加密,MD5特点是无法解密;Spring中有对应方法实现了md5
RequestMapping的路径书写
关于开头斜线的问题。之前写Servlet的时候好像一定得加,但是在Spring中可加可不加。因为在Spring中,web容器启动的时候spring会扫描并根据Controller注解找到所有Handler类,并且会遍历这些类找到所有带RequestMapping的方法,在这个过程中会对没有加斜线的路径自动拼接斜线。
密码、邮箱的合法性验证应在前端还是后端
在实现模块的时候,感觉合法性验证在开发中应该在前端的位置,这样用户体验感更好(能马上收到反馈),那这样一般在后端是不是就不需要实现了?一般在前端和后端都需要实现。前端为了用户体验,后端是数据处理的环节,必须对数据合法性进行保证以满足业务要求。
常量定义在类还是接口中?
在开发项目的时候需要为数字定义一个有含义的名字,就需要定义常量。我看的视频中老师用的方法是“常量接口模式”,但这种方法可能会导入一些用不到的常量。
而接口中声明的默认就是public static final的常量类型,所以代码会更简洁。而且生成的.class文件比类更小。
最佳实践:将常量定义在一个接口中,然后直接用接口名.常量名进行调用即可
Value注解的作用就是将配置中的属性读出来。有@Value(“${}”)和@Value(“#{}”)两种方式,前者读的是配置文件中的内容,后者是SpEL表达式对应的内容(如,某个bean的属性)
还可以优化的地方:验证码过期失效问题,激活成功后跳转应该自动登录了。
登录
Cookie是HTTP的标准,Session是JavaEE的标准,Session还是基于Cookie的。

设置Cookie:一个cookie是一个键值对,需要对response添加cookie才会送到浏览器去。生存时间默认是关了浏览器就不在了。

设置Session:SpringMVC的控制器方法中作为参数直接获得,首次访问服务器时会生成一个sessionId并作为cookie保存到浏览器,下一次访问服务器时会携带这个sessionId对session内容进行读取
需求:用户输入用户名、密码、验证码,进行验证与登录。

设计一个数据结构来记录登录状态,ticket作为登录凭证,用expired记录过期时间,login_ticket表结构
字段名:id, user_id, ticket, status, expired
验证码在controller处判断即可,如果有错可以提前返回;
“记住我”选项,无非就是控制登录有效时间的长短
前端向后端传输时的密码依然可以被抓包
后端的加密是必须的,不然一旦数据泄露,数据库信息就是裸露的,密码就透露出去了。后端的操作聚焦于密码存储的安全性。
虽然使用https传输时是密文传输,但中间人可以通过伪造证书来抓包。此时可以在前端用一个加密,比如RSA
拦截器与过滤器的区别
过滤器的配置比较简单,直接实现Filter 接口即可,也可以通过@WebFilter注解实现对特定URL拦截,看到Filter 接口中定义了三个方法。

init() :该方法在容器启动初始化过滤器时被调用,它在 Filter 的整个生命周期只会被调用一次。注意:这个方法必须执行成功,否则过滤器会不起作用。
doFilter() :容器中的每一次请求都会调用该方法, FilterChain 用来调用下一个过滤器 Filter。
destroy(): 当容器销毁 过滤器实例时调用该方法,一般在方法中销毁或关闭资源,在过滤器 Filter 的整个生命周期也只会被调用一次
拦截器它是链式调用,一个应用中可以同时存在多个拦截器Interceptor, 一个请求也可以触发多个拦截器 ,而每个拦截器的调用会依据它的声明顺序依次执行。
将自定义好的拦截器处理类进行注册,并通过addPathPatterns、excludePathPatterns等属性设置需要拦截或需要排除的 URL。
实现原理不同过滤器和拦截器 底层实现方式大不相同,过滤器 是基于函数回调的,拦截器 则是基于Java的反射机制(动态代理)实现的。
使用范围不同过滤器 实现的是 javax.servlet.Filter 接口,而这个接口是在Servlet规范中定义的,也就是说过滤器Filter 的使用要依赖于Tomcat等容器,导致它只能在web程序中使用。而拦截器(Interceptor) 它是一个Spring组件,并由Spring容器管理,并不依赖Tomcat等容器,是可以单独使用的。不仅能应用在web程序中,也可以用于Application、Swing等程序中。
触发时机不同
过滤器 和 拦截器的触发时机也不同过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。

拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。
拦截的的请求范围不同
过滤器几乎可以对所有进入容器的请求起作用,而拦截器只会对Controller中请求或访问static目录下的资源请求起作用
上传头像
一些细节:

在表单处声明文件类型,multipart/form-data
用SpringMVC的Multipartfile方法,然后这个方法是视图层的,所以不要写到Service层了。
图像存在服务器,但是访问时是根据web访问路径。根据web访问路径的话需要有特定的控制器来处理。
在读取图像的时候,可以设置一个缓冲区来输出,能快一点
图像合法性验证

密码修改
Service:输入userid、原密码和新密码,判断原密码和数据库中的是否相等;相等再继续,向数据库中修改新密码。

检查登录状态
需求:有的页面如果没登录是不能访问的,如账号设置等,一个一个页面添加规则有点麻烦,可以对需要拦截的方法进行注解。然后在拦截器中对这些被注解的方法进行拦截。

项目中有多个拦截器时,按照注册顺序来执行
自定义注解时用元注解来注解。
常用元注解:@Target, @Retenion, @Document, @Inherited。前两个是必须的,Target描述注解的范围,即注解可以用在哪。Retention用于描述注解的生命周期,表示需要在什么级别保存该注解,即保留的时间长短

牛客社区论坛项目(二)相关推荐

  1. 牛客社区论坛项目(一)

    环境搭建与技术栈说明 CDN的全称是Content Delivery Network,即内容分发网络.CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡. ...

  2. 仿牛客社区项目笔记-帖子模块(核心)

    仿牛客社区项目笔记-帖子模块(核心) 1. 帖子模块 1.1 过滤敏感词 1.2 发布帖子 1.3 帖子详情 1.4 显示评论 1.5 添加评论 1.6 私信列表 1.7 发送私信 1. 帖子模块 分 ...

  3. 仿牛客社区项目(第一章)

    文章目录 第一章:初始 SpringBoot,开发社区首页 仿牛客社区项目开发首页功能 一. 实体引入 1. User类 2. DiscussPost 类 3. Page类 二. 配置文件 三. da ...

  4. run(牛客2018多校二国庆欢乐派对 )

    run(牛客2018多校二国庆欢乐派对 ) 原题: White Cloud is exercising in the playground. White Cloud can walk 1 meters ...

  5. 牛客网SQL实战二刷 | Day10

    「牛客网SQL实战二刷」是个系列学习笔记博文,今天解析7道SQL题目- 第55 - 61题. 每篇笔记的格式大致为,三大板块: 大纲 题目(题目描述.思路.代码.相关参考资料/答疑) 回顾 ❤️「往期 ...

  6. 社区java视频大宝库_Java大牛手把手带你实现社区论坛项目实战课程

    Java大牛手把手带你实现社区论坛项目实战课程 Mr李 Java 2019-12-18 https://www.jsdaima.com/video/900.html Java大牛手把手带你实现社区论坛 ...

  7. 牛客网SQL实战二刷 | Day2

    「牛客网SQL实战二刷」是个系列学习笔记博文,每天解析6道SQL题目- 今天是第7-12 题!该系列的其他博文,可在「我的博客」 中查看- 每篇笔记的格式大致为,三大板块: 大纲 题目(题目描述.思路 ...

  8. 牛客网论坛最具争议的 Java 面试成神笔记,GitHub 已下载量已过百万

    程序员内部一直流传这一句话:面试看牛客 刷题看力扣牛客网作为国内最牛的程序员面试网站,一直在程序员内部颇负盛名,其中用户更是卧虎藏龙! 有国内一线大厂的企业招聘 还有一些低调的互联网大牛实力就和天龙八 ...

  9. 牛客网SQL实战二刷 | 完整解析 -- 目录索引

    「牛客网SQL实战二刷」是个系列学习笔记博文,Day1 - Day10,每天解析6道SQL题目- 初衷是留给自己一份笔记,也希望能分享给「一起学习SQL的你」? 每篇笔记的格式大致为,三大板块: 大纲 ...

最新文章

  1. python 怎么将数组转为列表_图片转换成pdf格式怎么操作?什么软件能将图片转为pdf?...
  2. “亚信科技杯”南邮第七届大学生程序设计竞赛之网络预赛 (K L题解)
  3. 基本语法及基本概念概述(标识符、访问修饰符、变量、数组、枚举、注释、空行、继承、接口、(对象、类、方法、实例变量)、关键字表)
  4. 谈谈Memcached与Redis
  5. 【学习生活杂谈】学习记录
  6. Ajax 1.0 中使用web控件调用后台方法的用法.
  7. Linux下替代grep高效文本搜索工具
  8. linux 进程学习
  9. MyBatis(三)------MyBatis的核心组件
  10. 怎么查看java的源码
  11. 电脑qq音乐显示无法代理服务器,电脑QQ音乐软件无法登录如何解决
  12. VS2019 无法打开源文件“stdafx.h“ 问题
  13. 3D相机技术 | 立体视觉传感器+TOF相机
  14. mysql怎么设计抽奖表_Access设计抽奖系统
  15. 如何使用报表工具设置页眉与页脚
  16. 【进阶】数位DP详解
  17. 什么是SEO?搜索引擎优化是什么意思?
  18. 美国会议签证——我是正当理由去美国,我能支付(或有人为我支付)我在美国期间的所有费用,办完事我肯定回来, 邀请信,行程表这些材料齐全即可...
  19. #华为推送# 游戏类应用如何利用推送能力实现用户运营精细化
  20. 高并发分布式下生成全局ID的几种方法

热门文章

  1. Excel之UPPER、LOWER、IFERROR和SUBSTITUTE函数
  2. 超级天才尹希:31岁成哈佛史上最年轻教授,却因国籍引发争议
  3. 不用找,你想要的行业icon图标素材都在这里
  4. 使用opencv查找两张图片不同的部分
  5. 从古到今 回顾苹果Mac OS的前世今生
  6. OpenMV入门(下)
  7. opencv人脸检测+美颜
  8. 如何自己开发App?如何快速生成App?
  9. 一元线性回归python示例——房价预测
  10. 学习js在线html(富文本)编辑器