简介
基于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值相同的消息
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为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
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是什么?使用CDN有什么优势? - 青岚的回答 - 知乎
登录
-
用户名和密码的校验
-
生成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();
}
- 添加cookie
Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
cookie.setPath("/");//设置为全站有效
if ("true".equals(rememberme)) {
cookie.setMaxAge(expiredSeconds);
}
response.addCookie(cookie);
- 自动登录
int expiredSeconds = "true".equals(rememberme) ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;
/**
* 默认状态的登录凭证的超时时间
*/
int DEFAULT_EXPIRED_SECONDS = 3600 * 12;
/**
* 记住状态的登录凭证超时时间
*/
int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 5;
- 设置登录用户
hostHolder.setUser(user);
-
返回响应 到此已经完成登录
-
鉴权
- 设置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将数据存储在服务端
- 安全性: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层
- 获取当前登录用户对点赞操作的新闻的喜欢状态,如果已经是喜欢的状态,就直接返回
- 调用service层完成点赞逻辑
- 更新mysql中被点赞新闻的likeCount
- 将本次点赞的行为加入异步队列中,后续异步队列会通知本条新闻的创建者本条新闻被点赞了
- 返回响应
service层
- 获取喜欢的状态
- 如果用户id出现在本条新闻的“喜欢列表”里,表示该用户喜欢本条新闻使用
- 如果用户id出现在本条新闻的“不喜欢列表”里,表示该用户不喜欢本条新闻
- 如果用户id不在“喜欢列表”和“不喜欢列表”中,表示该用户没有对本条新闻进行评价 使用sismember完成
- 喜欢
- 将用户id加入“喜欢列表”
- 将用户id从“不喜欢列表”中移除。 使用sadd和srem完成