1. 发送邮件
在自己邮箱开启SMTP服务,pom.xml中引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-mailartifactId>
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);
@Autowired
private 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("[email protected]","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.getActivatio
context.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 {
@Autowired
private UserService userService;
@Autowired
private HostHolder hostHolder;
/**
* 请求开始前
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public 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
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
// 从ThreadLocal 中得到当前线程持有的user
User user = hostHolder.getUser();
if (user != null && modelAndView != null) {
// 登录用户的信息存入modelAndView
modelAndView.addObject("loginUser", user);
}
}
*~~每次请求时都要新建一个ThreadLocal来放User,这样性能影响会大吗?
ThreadLocal创建一个线程局部变量,可以说就是个变量,不会很大影响性能的。~~ *
/**
* 请求结束后
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 从ThreadLocal清除数据
hostHolder.clear();
}
}
LoginRequiredInterceptor.java 登录请求拦截器
@Component
public class LoginRequiredInterceptor implements HandlerInterceptor {
@Autowired
private HostHolder hostHolder;
/**
* 请求开始前
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public 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 {
@Autowired
private LoginTicketInterceptor loginTicketInterceptor;
@Autowired
private LoginRequiredInterceptor loginRequiredInterceptor;
@Override
public 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容器托管
@Bean
public 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);
@Autowired
private OSS ossClient;// 注入阿里云oss文件服务器客户端
@Autowired
private 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的AccessKeySecret
String accessKeySecret = aliyunOssConfig.getAccessKeySecret();
// 获取oss的AccessKeyId
String accessKeyId = aliyunOssConfig.getAccessKeyId();
// 获取oss目标文件夹
String filehost = aliyunOssConfig.getFileHost();
// 返回图片上传后返回的url
String 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");
//文件上传至阿里云OSS
ossClient.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用于描述注解的生命周期,表示需要在什么级别保存该注解,即保留的时间长短