基于SpringBoot的Web端网页分享平台

463 阅读7分钟

简介

基于SpringBoot框架,设计开发了一个网页的分享平台,能通过浏览器或微信小程序进行访问。基本功能包括用户注册和登录、图片上传、分享网页、私信、评论、点赞点踩。

信息加密

注册时,数据库user表中的password字段不能明文存储,我们使用md5算法和salt进行对密码加密后再存入数据库。

如果只使用md5算法对密码进行加密,可能通过数据库暴力查询md5值获得密码的原始数据(彩虹表攻击)。将密码和随机生成的一段字符(salt)进行组合后再使用md5算法进行加密,可以降低被查询到的可能,提高数据的安全性。

String salt = UUID.randomUUID().toString().substring(0, 5);
user.setSalt(salt);
String encryptedPassword = DigestUtils.md5DigestAsHex((user.getPassword() + salt).getBytes());
user.setPassword(encryptedPassword);

登录时,将用户输入的密码和用户对应的salt组合并使用md5加密获得uncertainPassword。将uncertainPassword与数据库中存储的realPassword进行对比,相同则登录成功,反之则失败。

String realPassword = realUser.getPassword();
String uncertainPassword = DigestUtils.md5DigestAsHex((user.getPassword() + realUser.getSalt()).getBytes());
if (!realPassword.equals(uncertainPassword)) {
    map.put("msg", "密码不正确");
    return map;
}

MD5信息摘要算法

将MD5算法看做一个机器,计算机上的任何内容被丢进去,都将输出一个长度固定为128比特的MD5值。

  • MD5算法生成散列值的过程统共分为三步
    • 填充对齐
    • 分块
    • 多轮压缩
  • 由于信息的损失,MD5是一个一个不可逆的过程,也就不存在用MD5值反推原始消息的可能。
  • MD5算法的输入为“计算机上的任何内容”(无穷),输出的MD5值为128位(有穷),所以一定会出现两个不同内容的MD5值相同,即“碰撞”。
  • 碰撞攻击:找出任意两个MD5值相同的消息

md5在线解密破解,md5解密加密

MD5 - 百度百科

MD5为何不再安全

Bcrypt

可以使用BCryptPasswordEncoder加密方式代替MD5加密,BCryptPasswordEncoder每次encode的值都是不一样的,也就是同一个明文,每次加密之后都是不一样的。每次不一样的话,怎么做密码校验呢。BCryptPasswordEncoder提供了match方法,可以对明文和加密后的字符串进行match匹配,匹配上了就返回true,否则返回false。同一个明文,尽管它可以有多个加密串,但是它跟它的加密串一定是match的。有一种攻击叫彩虹表攻击_帅到被人砍啊的博客-CSDN博客_彩虹表攻击

我们应该如何保护用户的密码

BCrypt入门_木林森屿城的博客-CSDN博客_bcrypt

密码合法性检测

  • 长度8-20位
  • 包含数字、大写字母、小写字母中的两项
  • 不包含空格
private boolean validatePasswdComplexity(String password) {
    int length = password.length();
    if (length < 8 || length > 20) {
        return false;
    }

    if (password.trim().length() != length) {//含有空格
        return false;
    }

    int count = 0;
    if (password.length() - password.replaceAll("[A-Z]", "").length() > 0) {
        count++;
    }
    if (password.length() - password.replaceAll("[a-z]", "").length() > 0) {
        count++;
    }
    if (password.length() - password.replaceAll("[0-9]", "").length() > 0) {
        count++;
    }

    return count > 1;
}

ThreadLocal

项目里的HostHolser类里有一个静态的ThreadLocal<User>,在其他类里autowired这个HostHolser,就可以设置和获取当前登录用户。用ThreadLocal可以把数据进行隔离,保证线程安全。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

【多线程】关于ThreadLocal的,进来看看,你想知道的我都告诉你~_蛙广志的博客-CSDN博客

public class HostHolder {
    private static ThreadLocal<User> users = new ThreadLocal<>();
    public User getUser() {
        return users.get();
    }
    public void setUser(User user) {
        users.set(user);
    }
    public void clear() {
        users.remove();
    }
}

CDN

利用CDN(Content Delivery Network)的缓存机制,让用户无论在哪里都能快速打开网站,避免原始服务器因为宕机而导致的服务瘫痪。

通常情况下,我们所要的数据都是从主服务器中获取,但假如我们的主服务器在南方,而访问用户在北方,那么访问速度就会相对变慢,变慢的原因有很多,例如传输距离,运营商,带宽等等因素,而使用CDN技术的话,我们会将CDN节点分布在各地,当用户发送请求到达服务器时,服务器会根据用户的区域信息,为用户分配最近的CDN服务器。

简述负载均衡&CDN技术 - 吴纹羽 - 博客园 (cnblogs.com)

CDN是什么?使用CDN有什么优势? - 青岚的回答 - 知乎

登录

  1. 用户名和密码的校验

  2. 生成ticket ticken与用户是相关联的

@Data
public class Ticket {
    private Integer id;
    private Integer userId;
    private Date expired;
    private Integer status;//0 有效;1 无效
    private String ticket;
}
public String addLoginTicket(int userId, int expiredSeconds) {
    Ticket ticket = new Ticket();
    ticket.setUserId(userId);
    Date date = new Date();
    date.setTime(date.getTime() + 1000 * expiredSeconds);
    ticket.setExpired(date);//设置过期时间
    ticket.setStatus(0);//默认为0
    ticket.setTicket(UUID.randomUUID().toString().replace("-", ""));

    TicketDao.insert(ticket);
    return ticket.getTicket();
}
  1. 添加cookie
Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
cookie.setPath("/");//设置为全站有效
if ("true".equals(rememberme)) {
    cookie.setMaxAge(expiredSeconds);
}
response.addCookie(cookie);
  1. 自动登录
int expiredSeconds = "true".equals(rememberme) ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
/**
 * 默认状态的登录凭证的超时时间
 */
int DEFAULT_EXPIRED_SECONDS = 3600 * 12;

/**
 * 记住状态的登录凭证超时时间
 */
int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 5;
  1. 设置登录用户
hostHolder.setUser(user);
  1. 返回响应 到此已经完成登录

  2. 鉴权

  • 设置Interceptor拦截器(实现spring中的HandlerInterceptor接口),在preHandle(在业务处理器处理请求之前被调用)中request.getCookies()并对cookie进行判定。如果cookie不合法就就return true;
  • 如果以前选择了“记住登录”,就应该可以通过cookie的内容从数据库中获得用户对象。
  • 把获取到的用户对象添加进HostHolder的users中,即完成了自动登录。
@Override
public boolean preHandle(HttpServletRequest request,
                         HttpServletResponse response,
                         Object handler) throws Exception {

    String ticket = null;//ticket default is null
    if (request.getCookies() == null) {
        return true;
    }
    for (Cookie cookie : request.getCookies()) {//遍历req中的cookies
        if ("ticket".equals(cookie.getName())) {//如果cookie名称为【ticket】
            ticket = cookie.getValue();//获取【ticket】对应的值
            break;
        }
    }

    if (ticket != null) {//如果ticket不为空 去数据库中检索 防止伪造
        Ticket realLoginTicket = ticketDao.getByTicket(ticket);

        boolean illegal = realLoginTicket == null //数据库查不到
                || realLoginTicket.getExpired().before(new Date()) //ticket过期
                || realLoginTicket.getStatus() != 0;//ticket失效
        if (illegal) {
            return true;
        }
        //根据 从ticket中获取的userid 获取user对象
        User user = userDao.selectById(realLoginTicket.getUserId());

        hostHolder.setUser(user);//设置当前登录用户
    }
    return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler,
                            Exception ex) throws Exception {
    hostHolder.clear();
}
  • 其他功能检查登录权限时,从HostHolder中获取user,如果能获取到,则能确定现在为登录状态。
public String loginStatus(){
    if(hostHolder.getUser()==null){
        //未登录返回1
        return ToutiaoUtil.getJSONString(1);
    }
    //已登录返回0
    return ToutiaoUtil.getJSONString(0);
}

Cookie和Session

HTTP协议是无状态的,每次浏览器向服务器请求时,服务器都会将该请求视为新的请求,会话跟踪用于在同一次会话的多次请求间共享数据。

你真的了解 Cookie 和 Session 吗

聊一聊session和cookie

  • 存储位置:cookie是将数据存储在客户端,session将数据存储在服务端
  • 安全性:cookie不安全,session安全
  • 数据大小:cookie最大3kb,session无大小限制
  • 存储时间:cookie可以长期存储,session默认30分钟
  • 服务器:cookie不占服务器资源,session占用服务器资源

Redis实现点赞和点踩

使用NoSQL数据库redis中的set数据结构作为新闻的“喜欢列表”和“不喜欢列表”,存储用户id。

private static final String SPLIT=":";
private static final String BIZ_LIKE="LIKE";
private static final String BIZ_DISLIKE="DISLIKE";

public static String getLikeKey(int entityId,int entityType){
    return BIZ_LIKE+SPLIT+String.valueOf(entityType)+SPLIT+String.valueOf(entityId);
}


public static String getDisLikeKey(int entityId,int entityType){
    return BIZ_DISLIKE+SPLIT+String.valueOf(entityType)+SPLIT+String.valueOf(entityId);
}

controller层

  1. 获取当前登录用户对点赞操作的新闻的喜欢状态,如果已经是喜欢的状态,就直接返回
  2. 调用service层完成点赞逻辑
  3. 更新mysql中被点赞新闻的likeCount
  4. 将本次点赞的行为加入异步队列中,后续异步队列会通知本条新闻的创建者本条新闻被点赞了
  5. 返回响应

service层

  1. 获取喜欢的状态
  • 如果用户id出现在本条新闻的“喜欢列表”里,表示该用户喜欢本条新闻使用
  • 如果用户id出现在本条新闻的“不喜欢列表”里,表示该用户不喜欢本条新闻
  • 如果用户id不在“喜欢列表”和“不喜欢列表”中,表示该用户没有对本条新闻进行评价 使用sismember完成
  1. 喜欢
  • 将用户id加入“喜欢列表”
  • 将用户id从“不喜欢列表”中移除。 使用sadd和srem完成