牛客社区论坛项目(二)

注册登录

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);
...

牛客社区论坛项目(二)_第1张图片
牛客社区论坛项目(二)_第2张图片
牛客社区论坛项目(二)_第3张图片

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信息,流程如图:
牛客社区论坛项目(二)_第4张图片

这一流程需要借助拦截器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;
    }
}

注册用户
牛客社区论坛项目(二)_第5张图片
随机字符串生成: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处理完以后。牛客社区论坛项目(二)_第6张图片

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

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

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

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

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

你可能感兴趣的:(java,spring,spring,boot)