1. 程式人生 > >Java Web 開發 springboot 前後端分離以及身份驗證

Java Web 開發 springboot 前後端分離以及身份驗證

我先接觸的前後端分離是.Net的webapi,特性路由什麼的,所以想知道java中的webapi是什麼樣的,在網上直接查java webapi

得不到類似於C# 的webapi的資料,但是查java 前後端分離,就能找到類似於C# webapi的東西。

看了一篇文章,根據文章中提供的github地址拉取了原始碼,原始碼和文章中的程式碼很不一樣,然後我就綜合原文和拉取的程式碼,以

及執行的過程中發現的問題以及最終的解決方法整理了下面的文章。不過我看的那篇文章對前後端分離的部分的幾乎沒說什麼,

主要講了為什麼要前後端分離以及不少身份驗證的知識,生成token,驗證token,攔截器什麼的。


以前服務端為什麼能識別使用者呢?對,是session,每個session都存在服務端,瀏覽器每次請求都帶著sessionId(就是一個字串),於是伺服器根據這個sessionId就知道是哪個使用者了。 
那麼問題來了,使用者很多時,伺服器壓力很大,如果採用分散式儲存session,又可能會出現不同步問題,那麼前後端分離就很好的解決了這個問題。

前後端分離思想: 
在使用者第一次登入成功後,服務端返回一個token回來,這個token是根據userId進行加密的,金鑰只有伺服器知道,然後瀏覽器每次請求都把這個token放在Header裡請求,這樣伺服器只需進行簡單的解密就知道是哪個使用者了。這樣伺服器就能專心處理業務,使用者多了就加機器。當然,如果非要討論安全性,那又有說不完的話題了。

下面通過SpringBoot框架搭建一個後臺,進行token構建

 

1.專案的概覽

目錄結構:

為了儘可能簡單,就不連資料庫了,登陸時用固定的。

原文並沒有從頭開始講建立專案的過程,但是既然是建立springboot專案所以基本過程應該是:

File——New——Project——Spring Initializer,

點選next:

然後接著填寫資訊或更改資訊

點選next:

然後選擇依賴,這一步就很重要,整合mybatis,使用thymeleaf,開發web特性的專案等都可以在此時選好依賴。

建立前後端分離的springboot的後端專案,要新增哪些依賴呢?後面會提到pom.xml,那個程式碼是從拉取的原始碼中貼上過來的,

依賴選好了,就可以直接點選next:

然後填寫好專案名稱以及專案位置,就可以點選finish了。

 

pom.xml裡的依賴:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.jimo</groupId>
    <artifactId>auth-jimo</artifactId>
    <version>2.0.0</version>
    <packaging>jar</packaging>

    <name>AuthServer</name>
    <description>auth server</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- JJWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

2.UserController類程式碼:
這裡的加密金鑰是:base64EncodedSecretKey

package com.jimo.controller;

import com.jimo.model.User;
import com.jimo.model.common.Result;
import com.jimo.security.JwtUtil;
import org.springframework.web.bind.annotation.*;

import javax.servlet.ServletException;

/**
 * @author jimo
 * @func controller
 * @date 2018/8/24 22:44
 */
@RestController
@RequestMapping("/user")//類似於C# Webapi中的特性路由
public class UserController {

    /**
     * @func 測試時先用死的使用者名稱密碼,請求使用JSON格式資料
     * @author wangpeng
     * @date 2018/8/24 22:45
     */
    @PostMapping("/login")     //類似於C# Webapi中的特性路由
    public Result login(@RequestBody User user) throws ServletException {
        if (!"admin".equals(user.getUsername())) {
            throw new ServletException("no such user");
        }
        if (!"1234".equals(user.getPassword())) {
            throw new ServletException("wrong password");
        }
        return new Result(JwtUtil.getToken(user.getUsername()));
    }

    /**
     * @func 用於客戶端檢查token是否合法
     * @author wangpeng
     * @date 2018/8/27 16:58
     */
    @PostMapping("/checkToken")
    public Result checkToken(String token) {
        return new Result(JwtUtil.isTokenOk(token));
    }

    @GetMapping("/success")
    public Result success() {
        return new Result("login success");
    }

    @GetMapping("/getEmail")
    public Result getEmail() {
        return new Result("[email protected]");
    }
}

3.GlobalExceptionHandler類程式碼:

package com.jimo.exp;

import com.jimo.model.common.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * @author jimo
 * @func 全域性異常處理
 * @date 2018/8/24 22:44
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e) {
        return new Result(false, e.getMessage());
    }
}

4.Result類程式碼:

package com.jimo.model.common;

/**
 * @author jimo
 * @func 封裝統一的返回資料
 * @date 2018/8/24 22:46
 */
public class Result {
    /**
     * 成功為true
     */
    private boolean ok;
    /**
     * 錯誤訊息或其他提示
     */
    private String msg;
    /**
     * 資料
     */
    private Object data;

    public Result() {
        this(true, "", null);
    }

    public Result(Object data) {
        this(true, "", data);
    }

    public Result(boolean ok, String msg) {
        this(ok, msg, null);
    }

    public Result(boolean ok, String msg, Object data) {
        this.ok = ok;
        this.msg = msg;
        this.data = data;
    }

    public boolean isOk() {
        return ok;
    }

    public void setOk(boolean ok) {
        this.ok = ok;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

5.User類程式碼:

package com.jimo.model;

public class User {
   private String username;
   private String password;

   public String getUsername() {
      return username;
   }

   public void setUsername(String username) {
      this.username = username;
   }

   public String getPassword() {
      return password;
   }

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

6.JwtInterceptor類程式碼:

package com.jimo.security;

import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author jimo
 * @func 攔截token並驗證,不通過則丟擲異常
 * @date 2018/8/24 22:38
 */
public class JwtInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("prehandle");
        final String authorization = request.getParameter("Authorization");
        /*String authHeader = request.getHeader("Authorization");*/
        if (authorization == null || !authorization.startsWith("Bearer ")) {
            throw new ServletException("invalid Authorization header,請重新登陸");
        }
        //取得token
        String token = authorization.substring(7);
        try {
            JwtUtil.checkToken(token);
            return true;
        } catch (Exception e) {
            throw new ServletException(e.getMessage());
        }
    }
}

7.JwtUtil類程式碼:

package com.jimo.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import javax.servlet.ServletException;
import java.util.Date;

/**
 * @author jimo
 * @func Jwt相關
 * @date 17-12-12 下午5:28
 */
public class JwtUtil {
    /**
     * 私鑰
     */
    final static String base64EncodedSecretKey = "base64EncodedSecretKey";
    /**
     * 過期時間,測試使用20分鐘
     */
    final static long TOKEN_EXP = 1000 * 60 * 20;

    public static String getToken(String userName) {
        return Jwts.builder()
                .setSubject(userName)
                .claim("roles", "user")
                .setIssuedAt(new Date())
                /*過期時間*/
                .setExpiration(new Date(System.currentTimeMillis() + TOKEN_EXP))
                .signWith(SignatureAlgorithm.HS256, base64EncodedSecretKey)
               .compact();

       //return  Jwts.builder().setSubject(userName).claim("roles", "user").setIssuedAt(new Date())
       //         .signWith(SignatureAlgorithm.HS256, "base64EncodedSecretKey").compact();


    }

    /**
     * @func 檢查token, 只要不正確就會丟擲異常
     * @author jimo
     * @date 17-12-12 下午6:21
     */
    static void checkToken(String token) throws ServletException {
        try {
            final Claims claims = Jwts.parser().setSigningKey(base64EncodedSecretKey).parseClaimsJws(token).getBody();
        } catch (ExpiredJwtException e1) {
            throw new ServletException("token expired");
        } catch (Exception e) {
            throw new ServletException("other token exception");
        }
    }

    /**
     * @func token ok返回true
     * @author wangpeng
     * @date 2018/8/27 16:59
     */
    public static boolean isTokenOk(String token) {
        try {
            Jwts.parser().setSigningKey(base64EncodedSecretKey).parseClaimsJws(token).getBody();
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

在呼叫login()方法後,會生成一個token,然後用這個token呼叫success方法應該會提示"login success",但是事情沒有想的那麼

順利:

我用生成的token去呼叫success方法時,總是提示"invalid Authorization header,請重新登陸"。然後我發現,

上述程式碼在postman中輸入http://localhost:8081/user/login以及admin和1234,每一次請求都會產生不同的Token,

所以我認為當輸入http://localhost:8081/user/login,且填入新生成的token時,驗證失敗。而且我以為要想解決這個問題要先解決

下述問題:

怎麼能在生成一個token後可以在一段相對較長的時間內使用這個token呼叫其他方法?

 

不過,很快我就發現還有一個問題,我發現或者說注意到,每次返回的資訊都是:

{
    "ok": false,
    "msg": "invalid Authorization header,請重新登陸",
    "data": null
}

為什麼不是提示token過期呢?

通過檢視程式碼可以知道,“invalid Authorization header,請重新登陸”這條資訊只有在token為空或者格式不正確的時候才會提示,

token過期應該提示的是“token expired”。而上述問題應該屬於token過期啊。

 

上述兩個問題有沒有什麼關係?

我添加了兩行程式碼(都是在控制檯輸出token的),我想要看看能不能取到前端傳來的token,如果能取到,是什麼 樣的?

public class JwtInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("prehandle");
        final String authorization = request.getParameter("Authorization");
        /*String authHeader = request.getHeader("Authorization");*/
        //test
        System.out.println(authorization);//輸出的是null

        if (authorization == null || !authorization.startsWith("Bearer ")) {
            throw new ServletException("invalid Authorization header,請重新登陸");
        }
        //取得token
        String token = authorization.substring(7);
        //test
        System.out.println(token);//程式沒能執行到這裡
        try {
            JwtUtil.checkToken(token);
            return true;
        } catch (Exception e) {
            throw new ServletException(e.getMessage());
        }
    }
}

為什麼會這樣?輸出是null沒有得到資料!

程式碼裡獲取資料的程式碼是:request.getParameter("Authorization")

還要註釋掉的獲取資料的程式碼:request.getHeader("Authorization")

“Authorization”是在Parameter還是在Header,還是兩者都不是,而是其他?

postman是這樣的,Authorization是放在Headers裡面的:

而程式碼裡呼叫的確實getParameter()方法,如果改成getHeader()會如何?

問題解決了!而且是兩個問題都解決了。