Spring boot 搭建个人博客系统(二)——登录注册功能

时间:2022-08-31 11:09:14

Spring boot 搭建个人博客系统(二)——登录注册功能

一直想用Spring boot 搭建一个属于自己的博客系统,刚好前段时间学习了叶神的牛客项目课受益匪浅,乘热打铁也主要是学习,好让自己熟悉这类项目开发的基本流程。系统采用Spring boot+MyBatis+MySQL的框架进行项目开发。

项目源码:Jblog
个人主页:tuzhenyu’s page
原文地址:Spring boot 搭建个人博客系统(二)——登录注册功能

0. 思路

  用户登录注册功能主要实现用户的添加,验证和记住密码一段时间内的免密码登录。用户的注册是往数据库中插入用户的用户名和密码等信息,用户的验证是从数据库中取出用户的用户名和密码等信息进行比对。明文密码存储有很大的风险,采用在密码后加salt再经过MD5加密的形式存储,这样一方面避免了用户密码信息泄露的风险,同时也防止了暴力破解密码的可能。
  登录成功后生成一串字符作为ticket存储在数据库和cookie中,用于下次登录的免密码登录验证。一段时间后数据库中的ticket失效,用户需要重新登录。

1. 数据模型

  系统登录注册功能需要验证用户信息,需要操纵数据库的user表和login_ticket表,使用MyBatis作为系统的ORM框架用来简化数据操作。
  

1.1 引入MyBatis ORM框架

(1) 在pom.xml中添加MyBatis依赖jar包和MySQL连接相关的jar包,引入MyBatis相关类库和数据库连接相关类库。

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>

(2) 添加MyBatis配置文件,设置MyBatis相关参数

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="defaultStatementTimeout" value="3000"/>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="useGeneratedKeys" value="true"/>
</settings>

</configuration>

(3) 在系统配置文件application.properities中配置数据库URL,用户名密码等信息,同时设置MyBatis配置文件位置。

spring.datasource.url=
spring.datasource.username=
#spring.datasource.password=
spring.datasource.password=

mybatis.config-location=classpath:mybatis-config.xml

1.2 添加数据库表实体类

  根据user表的具体字段设置实体类的对应字段,MyBatis的mapUnderscoreToCamelCase参数设定为true,说明数据库字段的下划线分割自动对应实体类的驼峰形式。

public class User {
private int id;
private String name;
private String password;
private String salt;
private String headUrl;
private String role;

public User(){}

public User(String name){
this.name = name;
this.password = "";
this.salt = "";
this.headUrl = "";
this.role = "user";
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getSalt() {
return salt;
}

public void setSalt(String salt) {
this.salt = salt;
}

public String getHeadUrl() {
return headUrl;
}

public void setHeadUrl(String headUrl) {
this.headUrl = headUrl;
}

public String getRole() {
return role;
}

public void setRole(String role) {
this.role = role;
}
}

根据login_ticket表的具体字段设置实体类的对应字段

public class LoginTicket {
private int id;
private int userId;
private Date expired;
private int status;
private String ticket;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public int getUserId() {
return userId;
}

public void setUserId(int userId) {
this.userId = userId;
}

public Date getExpired() {
return expired;
}

public void setExpired(Date expired) {
this.expired = expired;
}

public int getStatus() {
return status;
}

public void setStatus(int status) {
this.status = status;
}

public String getTicket() {
return ticket;
}

public void setTicket(String ticket) {
this.ticket = ticket;
}
}

1.3 添加数据库操作DAO类

  根据MyBatis的使用特点,创建数据库操作接口UserDao.class和LoginTicket.class,用作对数据库执行具体的操作。

@Mapper
public interface UserDao {
String TABLE_NAEM = " user ";
String INSERT_FIELDS = " name, password, salt, head_url ,role ";
String SELECT_FIELDS = " id, " + INSERT_FIELDS;

@Insert({"insert into",TABLE_NAEM,"(",INSERT_FIELDS,") values (#{name},#{password},#{salt},#{headUrl},#{role})"})
public void insertUser(User user);

@Select({"select",SELECT_FIELDS,"from",TABLE_NAEM,"where id=#{id}"})
public User seletById(int id);

@Select({"select",SELECT_FIELDS,"from",TABLE_NAEM,"where name=#{name}"})
public User seletByName(@Param("name") String name);

@Delete({"delete from",TABLE_NAEM,"where id=#{id}"})
public void deleteById(int id);
}
@Mapper
public interface LoginTicketDao {
String TABLE_NAEM = " login_ticket ";
String INSERT_FIELDS = " user_id, ticket, expired, status ";
String SELECT_FIELDS = " id, " + INSERT_FIELDS;

@Insert({"insert into",TABLE_NAEM,"(",INSERT_FIELDS,") values (#{userId},#{ticket},#{expired},#{status})"})
void insertLoginTicket(LoginTicket loginTicket);

@Select({"select",SELECT_FIELDS,"from",TABLE_NAEM,"where id=#{id}"})
LoginTicket seletById(int id);

@Select({"select",SELECT_FIELDS,"from",TABLE_NAEM,"where ticket=#{ticket}"})
LoginTicket seletByTicket(String ticket);

@Update({"update",TABLE_NAEM,"set status = #{status} where ticket = #{ticket}"})
void updateStatus(@Param("ticket") String ticket, @Param("status") int status);

@Delete({"delete from",TABLE_NAEM,"where id=#{id}"})
void deleteById(int id);
}

2. 用户注册

  ORM框架的作用就是将对象与关系型数据库的表进行关联性绑定,根据插入的对象解析出对应的字段插入数据库中。用户注册是在验证注册用户名密码可用的情况下生成一个User对象插入数据库中,同时为了保护用户信息安全在密码存储时,随机生成固定长度的字符串作为salt与密码组合后讲过MD5加密存储在数据库字段中。
  用户密码如果直接散列后存储在数据库中,黑客可以通过获得这个密码散列值,然后通过查散列值字典(彩虹表)的方式暴力破解,得到用户的密码;通过加salt加密的方式可以一定程度上解决这一问题,因为salt值由系统随机生成,也只有系统知道。即便黑客获取了密码的散列值但在不知道salt值的前提下暴力破解散列值的几率大大降低。
 加salt加密并不能完全杜绝用户密码的泄露,因为一旦密码散列值和salt值同时泄露,黑客通过salt值重建彩虹表,依旧能够获取用户密码。只是这样的计算成本会大大增加,假设每个用户一个salt, 散列值字典的字段有10万, 一次基于salt值的字典重建就要重新生成10万个字段,那么10个密码就需要生成10个散列字典,也就是100万 个字段。因此,从某种意义上来讲,增加salt的长度也就增加了散列值字典字段的数目,也可以提高安全性。还有一种提高安全性的方式,就是salt值的动态生成。通过一定算法动态生成salt值,这样可以大大降低salt值泄露的风险。

public Map<String,String> register(String username, String password){
Map<String,String> map = new HashMap<>();
Random random = new Random();
if (StringUtils.isBlank(username)){
map.put("msg","用户名不能为空");
return map;
}

if (StringUtils.isBlank(password)){
map.put("msg","密码不能为空");
return map;
}

User u = userDao.seletByName(username);
if (u!=null){
map.put("msg","用户名已经被占用");
return map;
}

User user = new User();
user.setName(username);
user.setSalt(UUID.randomUUID().toString().substring(0,5));
user.setHeadUrl(String.format("https://images.nowcoder.com/head/%dm.png",random.nextInt(1000)));
user.setPassword(JblogUtil.MD5(password+user.getSalt()));
user.setRole("user");
userDao.insertUser(user);

String ticket = addLoginTicket(user.getId());
map.put("ticket",ticket);

return map;
}

3. 用户登录

  用户登录主要是进行用户名和密码的验证,由于用户在注册时候会生成随机的salt值进行密码存储加密,在密码验证时需要读取用户对应的salt值组合进行散列化后与数据库中用户密码进行比对,如果一致则登录成功。
  用户登录时明文传输密码存在风险,黑客可以通过抓包的方式截取用户信息。一般为了降低这种Web应用登录密码传输过程中泄露的风险,可以采用https方式传输和通过公匙和私匙的非对称加密的方式。

public Map<String,String> login(String username, String password){
Map<String,String> map = new HashMap<>();
Random random = new Random();
if (StringUtils.isBlank(username)){
map.put("msg","用户名不能为空");
return map;
}

if (StringUtils.isBlank(password)){
map.put("msg","密码不能为空");
return map;
}

User u = userDao.seletByName(username);
if (u==null){
map.put("msg","用户名不存在");
return map;
}

if (!JblogUtil.MD5(password+u.getSalt()).equals(u.getPassword())){
map.put("msg","密码错误");
return map;
}

String ticket = addLoginTicket(u.getId());
map.put("ticket",ticket);

return map;
}

4. 免密码登录

  免密码登录功能主要是通过一种自动身份验证的方式实现。用户登录或注册成功后,在一定时间内(如2个小时)再次访问同一个Web程序的任一个页面时都无需再次登录,而是直接进入界面(仅限于本机)。实现这个功能关键就是服务端要识别客户的身份。使用Cookie是最简单的身从验证。
  Cookie是web服务器存放在客户端的一个文件,客户端访问特定URL时会查询该文件,将与该URL相关的Cookie字段传输至服务端用作特定处理。Cookie可以设置失效时间,当Cookie过了失效时间后会自动消失不再随请求传输到服务器。
  用户在登录成功或注册成功后随机生成一个ticket作为用户后续操作无需密码验证的凭证,往login_ticket表中插入一条记录将ticket与具体的用户绑定,这样用户在操作时候就能通过ticket凭证辨别身份。登录成功或注册成功后将ticket凭证放入Cookie,保存在用户浏览器中,在下次访问时候会随请求传输到服务器用作身份验证。
(1) 往login_ticket表中插入一条记录用于绑定ticket凭证和用户身份

public String addLoginTicket(int userId){
LoginTicket loginTicket = new LoginTicket();
loginTicket.setUserId(userId);
Date date = new Date();
date.setTime(date.getTime()+1000*3600*30);
loginTicket.setExpired(date);
loginTicket.setStatus(0);
loginTicket.setTicket(UUID.randomUUID().toString().replaceAll("-",""));

loginTicketDao.insertLoginTicket(loginTicket);

return loginTicket.getTicket();
}

(2) 登录成功后将ticket凭证放入cookie,保存在用户浏览器;ticket有时效,过凭证期后用户需重新登录。

@RequestMapping("/login")
public String login(Model model, HttpServletResponse httpResponse,
@RequestParam String username,@RequestParam String password,@RequestParam(value = "next",required = false)String next){
Map<String,String> map = userService.login(username,password);
if (map.containsKey("ticket")) {
Cookie cookie = new Cookie("ticket",map.get("ticket"));
cookie.setPath("/");
httpResponse.addCookie(cookie);

if (StringUtils.isNotBlank(next)){
return "redirect:"+next;
}

return "redirect:/";
}else {
model.addAttribute("msg", map.get("msg"));
return "login";
}
}

(3) 注册成功后将ticket凭证放入cookie,保存在用户浏览器;ticket有时效,过凭证期后用户需重新登录。

@RequestMapping("/register")
public String register(Model model, HttpServletResponse httpResponse,
@RequestParam String username, @RequestParam String password
,@RequestParam(value = "next",required = false)String next){
Map<String,String> map = userService.register(username,password);
if (map.containsKey("ticket")){
Cookie cookie = new Cookie("ticket",map.get("ticket"));
cookie.setPath("/");
httpResponse.addCookie(cookie);

if (StringUtils.isNotBlank(next))
return "redirect:"+next;
else
return "redirect:/";
}else {
model.addAttribute("msg",map.get("msg"));
return "login";
}
}

5. 总结

  系统的注册登录功能主要是将用户注册的用户名密码存储在数据库,在下次登录时进行比对。同时为了免密码登录,在登录成功或注册成功时后生成用户凭证ticket与用户身份绑定在一起放入cookie中,等用户下次访问时候随请求传输至服务器进行验证。这样就能直接根据凭证识别用户身份,无需再经过用户名密码的验证。