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()會如何?
問題解決了!而且是兩個問題都解決了。