[牛客网中级项目]第四课 注册 登录 浏览 安全性
目录
1. 注册
1.1 注册实现功能:
1. 用户名合法性检测
2. 密码长度要求
3. 密码Salt加密,密码强度检测
4. 用户邮件/短信激活
1.2 代码功能实现:
UserDAO部分
UserDAO里添加向数据库查询用户是否被注册的功能
@Select({"select ", SELECT_FIELDS, " from ", TABLE_NAME, " where name=#{name}"})
User selectByName(String name);
UserService部分
UserService里添加register函数,用于实现检验用户名、密码是否为空,该用户是否已注册,密码生成(加盐算法)。返回给Controller的信息用Map存放,最后Controller生成统一的JSON格式返回给前端
检验用户名和密码:
Map<String, Object> map = new HashMap<String, Object>();
if (StringUtils.isBlank(username)) {
map.put("msgname", "用户名不能为空");
return map;
}
if (StringUtils.isBlank(password)) {
map.put("msgpwd", "密码不能为空");
return map;
}
向数据库查询用户是否已经被注册:
User user = userDAO.selectByName(username);
if (user != null) {
map.put("msgname", "用户名已经被注册");
return map;
}
密码生成:
密码生成采用MD5+盐的加密算法。普通的md5算法是不能通过解码来获取原来的字符串,如果需要验证密码是否正确,需要对验证密码也进行md5加密后与数据库中的密码进行比对才行。
但是安全起见我们需要给密码加salt(盐),salt本质上就是一段随机生成的字符串,然后和密码进行拼接,最后整体采用MD5加密算法加密。
我们设置一个工具类ToutiaoUtil
MD5算法如下:
public static String MD5(String key) {
char hexDigits[] = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
try {
byte[] btInput = key.getBytes();
// 获得MD5摘要算法的 MessageDigest 对象
MessageDigest mdInst = MessageDigest.getInstance("MD5");
// 使用指定的字节更新摘要
mdInst.update(btInput);
// 获得密文
byte[] md = mdInst.digest();
// 把密文转换成十六进制的字符串形式
int j = md.length;
char str[] = new char[j * 2];
int k = 0;
for (int i = 0; i < j; i++) {
byte byte0 = md[i];
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
logger.error("生成MD5失败", e);
return null;
}
}
密码加盐:
// 密码强度
user = new User();
user.setName(username);
//UUID库随机生成一段字符串然后截取前5位
user.setSalt(UUID.randomUUID().toString().substring(0, 5));
String head = String.format("http://images.nowcoder.com/head/%dt.png", new Random().nextInt(1000));
user.setHeadUrl(head);
user.setPassword(ToutiaoUtil.MD5(password+user.getSalt()));
userDAO.addUser(user);
//注册成功后台下发ticket 登录
String ticket = addLoginTicket(user.getId());
map.put("ticket", ticket);
return map;
注册成功即用户拥有能证明自身身份的token,需要下发给服务器,之后服务器认证用户身份就凭借的是token,token是和用户id绑定在一起的,详细实现参照登录层。
LoginController部分
Controller层在收到UserService层的Map信息后,进行判断。如果包含有ticket表登录成功,将ticket保存在cookie里并response返回,向前端返回注册成功的JSON格式的信息。
代码如下:
@RequestMapping(path = {"/reg/"}, method = {RequestMethod.GET, RequestMethod.POST})
@ResponseBody
public String reg(Model model, @RequestParam("username") String username,
@RequestParam("password") String password,
@RequestParam(value="rember", defaultValue = "0") int rememberme,
HttpServletResponse response) {
try {
Map<String, Object> map = userService.register(username, password);
if (map.containsKey("ticket")) {
Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
//全站有效
cookie.setPath("/");
//选'remberme'cookie生存期为5天
if (rememberme > 0) {
cookie.setMaxAge(3600*24*5);
}
response.addCookie(cookie);
return ToutiaoUtil.getJSONString(0, "注册成功");
} else {
return ToutiaoUtil.getJSONString(1, map);
}
} catch (Exception e) {
//出现异常返回前端msg
logger.error("注册异常" + e.getMessage());
return ToutiaoUtil.getJSONString(1, "注册异常");
}
2. 登录
2.1 登录实现功能:
1. 服务器密码校验/三方校验回调,token登记
1.1 服务器端token关联userid
1.2 客户端存储token
2. 服务端/客户端token有效期设置
注:token可以是sessionid或者是cookie里的一个Key
2.2 代码功能实现:
LoginTicketDAO部分
创建LoginTicketDAO类来实现以下功能:用户登录时添加ticket、浏览网页时查找ticket、以及登出时更新ticket。
@Insert({"insert into ", TABLE_NAME, "(", INSERT_FIELDS,
") values (#{userId},#{expired},#{status},#{ticket})"})
int addTicket(LoginTicket ticket);
@Select({"select ", SELECT_FIELDS, " from ", TABLE_NAME, " where ticket=#{ticket}"})
LoginTicket selectByTicket(String ticket);
@Update({"update ", TABLE_NAME, " set status=#{status} where ticket=#{ticket}"})
void updateStatus(@Param("ticket") String ticket, @Param("status") int status);
UserService部分
UserService部分添加login函数,主要的功能有验证用户名和密码是否为空,用户名是否存在,验证密码是否正确。登录成功后token要关联userid
首先要在model新建一个LoginTicket类,类的成员变量有id,用户ID userid,有效期expired,状态status(0表示有效,1表示无效)
登录成功向数据库添加ticket:
LoginTicket ticket = new LoginTicket();
ticket.setUserId(userId);
Date date = new Date();
date.setTime(date.getTime() + 1000*3600*24);
ticket.setExpired(date);
ticket.setStatus(0);
ticket.setTicket(UUID.randomUUID().toString().replaceAll("-", ""));
loginTicketDAO.addTicket(ticket);
return ticket.getTicket();
LoginController部分
与注册类似,将ticket保存到cookie中
public String login(Model model, @RequestParam("username") String username,
@RequestParam("password") String password,
@RequestParam(value="rember", defaultValue = "0") int rememberme) {
try {
Map<String, Object> map = userService.register(username, password);
if (map.containsKey("ticket")) {
Cookie cookie = new Cookie("ticket", map.get("ticket").toString());
cookie.setPath("/");
if (rememberme > 0) {
cookie.setMaxAge(3600*24*5);
}
return ToutiaoUtil.getJSONString(0, "注册成功");
} else {
return ToutiaoUtil.getJSONString(1, map);
}
} catch (Exception e) {
logger.error("注册异常" + e.getMessage());
return ToutiaoUtil.getJSONString(1, "注册异常");
}
3. 浏览页面(重点)
3.1注册实现功能:
1. 客户端:带token的HTTP请求
客户端用户登录浏览网页
2. 服务端:
服务端根据token认证用户并进行权限控制
1.根据token获取用户id
2.根据用户id获取用户的具体信息
3.用户和界面访问权限处理
4.渲染界面/跳转页面
3.2代码功能实现:
拦截器(Interceptor)
Spring MVC允许通过处理程序拦截器拦截web请求。处理程序拦截器必须实现HandleInterceptor包含的三个方法:
1.preHandle():在提交HTTP请求之后,到达Controller之前,返回布尔值,true表示继续处理程序执行链,false表示停止执行。
2.postHandle():Controller之后,渲染页面之前
3.afterCompletion():在整个请求结束后进行收尾。
新建PassportInterceptor实现HandlerInterceptor三个方法
public class PassportInterceptor implements HandlerInterceptor {
@Autowired
private LoginTicketDAO loginTicketDAO;
@Autowired
private UserDAO userDAO;
@Autowired
private HostHolder hostHolder;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,Object o) throws Exception{
String ticket = null;
//用户发送请求后,从request中查找cookie里的ticket
if(httpServletRequest.getCookies()!=null) {
for(Cookie cookie:httpServletRequest.getCookies()){
if(cookie.getName().equals("ticket")){
ticket = cookie.getValue();
break;
}
}
}
//存在有ticket但是也不能相信(可能伪造ticket),要查数据库
if(ticket != null) {
LoginTicket loginTicket = loginTicketDAO.selectByTicket(ticket);
}
//数据库查询不到,ticket有效期失效,ticket的status失效
if(loginTicket == null || loginTicket.getExpired().before(new Date()) || loginTicket.getStatus()!=0) {
return true;
}
//根据ticket确定用户身份,hostHolder保存用户
User user = userDAO.selectById(loginTicket.getUserId());
hostHolder.setUser(user);
}
//渲染页面前,modelAndView把user添加到前端
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
if (modelAndView != null && hostHolder.getUser() != null) {
modelAndView.addObject("user", hostHolder.getUser());
}
}
//清除hostHolder该用户进程
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
hostHolder.clear();
}
新建一个hostHolder类来保存该线程用户
@Component
public class HostHolder {
private static ThreadLocal<User> users = new ThreadLocal<User>();
public User getUser() {
return users.get();
}
public void setUser(User user) {
users.set(user);
}
public void clear() {
users.remove();
}
}
未登录跳转
我们还要对未登录用户进行权限控制,限制访问某些页面,设置LoginRequireInterceptor
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
if (hostHolder.getUser() == null) {
httpServletResponse.sendRedirect("/?pop=1");
return false;
}
return true;
}
注册拦截器
将整个链路挂起,注册拦截器,新建configuration里ToutiaoWebConfiguration继承WebMvcConfigurerAdapter
@Autowired
PassportInterceptor passportInterceptor;
@Autowired
LoginRequiredInterceptor loginRequiredInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(passportInterceptor);
//指定loginRequiredInterceptor的作用范围
registry.addInterceptor(loginRequiredInterceptor).addPathPatterns("/setting*");
super.addInterceptors(registry);
}
4.用户数据安全性
多种渠道:
1.HTTP注册页
2.公钥加密私钥解密,支付宝h5页面的支付密码加密
3.用户密码salt防止破解(CSDN,网易邮箱未加密密码泄露)
4.token有效期
5.单一平台的单点登录,登录IP异常检验
6.用户状态的权限判断
7.添加验证码机制,防止爆破和批量注册
5.AJAX异步数据交互
比如:评论翻页,不刷新页面,只加载评论并更新评论区
好处:
1.页面不刷新
2.体验更好
3.传输数据更少
4.APP/网站通用
扩展:统一的数据格式:{code:0,msg:",data:"}
例子:牛客投递登录框,点赞登录框
6.SpringBoot Dev Tools
添加springbootdevtools 依赖,可以不重启整个项目,而只修改运行某个文件