Spring boot 搭建個人博客系統(二)——登錄註冊功能

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中,等用戶下次訪問時候隨請求傳輸至服務器進行驗證。這樣就能直接根據憑證識別用戶身份,無需再經過用戶名密碼的驗證。

版權聲明:本文爲博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/u013967175/article/details/77435178

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章