文章目錄

JSON Web Token(JWT)是一個開放的标準(RFC 7519),它定義了一個緊湊且自包含的方式,用于在各方之間作為JSON對象安全地傳輸資訊。由于此資訊是經過數字簽名的,是以可以被驗證和信任。
JSON Web Token(縮寫 JWT)是目前最流行的跨域認證解決方案。
傳統的session認證一般是這樣的流程:
- 1、使用者向伺服器發送使用者名和密碼。
- 2、伺服器驗證通過後,在目前對話(session)裡面儲存相關資料,比如使用者角色、登入時間等等。
- 3、伺服器向使用者傳回一個 session_id,寫入使用者的 Cookie。
- 4、使用者随後的每一次請求,都會通過 Cookie,将 session_id 傳回伺服器。
- 5、伺服器收到 session_id,找到前期儲存的資料,由此得知使用者的身份。
session認證流程
這種模式的問題在于,擴充性(scaling)不好。單機當然沒有問題,如果是伺服器叢集,或者是跨域的服務導向架構,就要求 session 資料共享,每台伺服器都能夠讀取 session。
一種解決方案是 session共享,将session持久化或者存入緩存。各種服務收到請求後,都向持久層或緩存請求資料。這種方案的優點是架構清晰,缺點是工程量比較大。另外,持久層或者緩存萬一挂了,就會認證失敗。
另一種方案是伺服器索性不儲存 session 資料了,所有資料都儲存在用戶端,每次請求都發回伺服器。JWT 就是這種方案的一個代表。
JWT認證流程:
- 1、 使用者使用賬号和密碼發出post請求;
- 2、 伺服器使用私鑰建立一個jwt;
- 3、 伺服器傳回這個jwt給浏覽器;
- 4、 浏覽器将該jwt串在請求頭中像伺服器發送請求;
- 5、 伺服器驗證該jwt;
- 6、 傳回響應的資源給浏覽器。
jwt認證流程
JWT資訊
從上圖可以看到,JWT含有三部分:頭部(header)、載荷(payload)、簽名(signature)。
JWT的頭部有兩部分資訊:
- 聲明類型,這裡是JWT
- 聲明加密的算法,通常直接使用HMAC SHA256
頭部示例如下:
{
"alg": "HS256",
"typ": "JWT"
}
頭部一般使用base64加密(該加密是可以對稱解密的),構成了第一部分:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
這部分一般存放一些有效的資訊。JWT的标準定義包含五個字段:
- iss:該JWT的簽發者
- sub: 該JWT所面向的使用者
- aud: 接收該JWT的一方
- exp(expires): 什麼時候過期,這裡是一個Unix時間戳
- iat(issued at): 在什麼時候簽發的
載荷示例如下:
{
"iss": "kkjwt",
"iat": 1441593502,
"exp": 1441594722,
"aud": "www.example.com",
"sub": "kk"
}
Signature 部分是對前兩部分的簽名,防止資料篡改。
首先,需要指定一個密鑰(secret)。這個密鑰隻有伺服器才知道,不能洩露給使用者。然後,使用 Header 裡面指定的簽名算法(預設是 HMAC SHA256),按照下面的公式産生簽名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出簽名以後,把 Header、Payload、Signature 三個部分拼成一個字元串,每個部分之間用"點"(.)分隔,就可以傳回給使用者。
用戶端收到伺服器傳回的 JWT,可以儲存在 Cookie 裡面,也可以儲存在 localStorage。
此後,用戶端每次與伺服器通信,都要帶上這個 JWT。你可以把它放在 Cookie 裡面自動發送,但是這樣不能跨域,是以更好的做法是放在 HTTP 請求的頭資訊Authorization字段裡面。
Authorization: Bearer <token>
另一種做法是,跨域的時候,JWT 就放在 POST 請求的資料體裡面。
引入jwt的依賴
<!--jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.2</version>
</dependency>
Jwt工具類進行token的生成和認證:
/**
* JWT工具類
* 用于生成和校驗token
*/
public class JwtUtil {
private static final Logger logger = LoggerFactory.getLogger(JwtUtil.class);
/**
* 秘鑰
*/
private static final String SECRET = "my_secret";
/**
* 過期時間
**/
private static final long EXPIRATION = 1800L;//機關為秒
/**
* 生成使用者token,設定token逾時時間
*/
public static String createToken(User user){
//過期時間
Date expireDate = new Date(System.currentTimeMillis() + EXPIRATION * 1000);
Map<String, Object> map = new HashMap<>();
map.put("alg", "HS256");
map.put("typ", "JWT");
String token= JWT.create()
.withHeader(map) //添加頭部
//可以把資料存在claim中
.withClaim("id",user.getId()) //userId
.withClaim("name",user.getName())
.withClaim("userName",user.getUserName())
.withExpiresAt(expireDate) //逾時設定,設定過期的日期
.withIssuedAt(new Date()) //簽發時間
.sign(Algorithm.HMAC256(SECRET)); //SECRET加密
return token;
}
/**
* 檢驗token并解析token
*/
public static Map<String, Claim> verifyToken(String token){
DecodedJWT jwt=null;
try {
JWTVerifier verifier=JWT.require(Algorithm.HMAC256(SECRET)).build();
jwt=verifier.verify(token);
}catch (Exception e){
logger.error(e.getMessage());
logger.error("解析編碼異常");
}
return jwt.getClaims();
}
}
JWT過濾器中進行token的校驗和判斷,,token不合法直接傳回,合法則解密資料并把資料放到request中供後續使用。
為了使過濾器生效,需要在啟動類添加注解@ServletComponentScan(basePackages = “edu.hpu.filter”):
@WebFilter(filterName = "jwtFilter",urlPatterns = "/secure/*")
public class JwtFilter implements Filter{
private final Logger logger = LoggerFactory.getLogger(JwtFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setCharacterEncoding("UTF-8");
//擷取header裡的token
String token=request.getHeader("authorization");
if ("OPTIONS".equals(request.getMethod())) { //除了 OPTIONS請求以外, 其它請求應該被JWT檢查
response.setStatus(HttpServletResponse.SC_OK);
filterChain.doFilter(request, response);
}else {
if (token == null) {
response.getWriter().write("沒有token!");
return;
}
}
Map<String, Claim> userData = JwtUtil.verifyToken(token);
if (userData==null){
response.getWriter().write("token不合法!");
return;
}
Integer id = userData.get("id").asInt();
String name = userData.get("name").asString();
String userName = userData.get("userName").asString();
//攔截器 拿到使用者資訊,放到request中
request.setAttribute("id", id);
request.setAttribute("name", name);
request.setAttribute("userName", userName);
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
}
- 登入Controller進行登入操作,登入成功後生産token并傳回:
/**
* 登入Controller
*/
@RestController
public class LoginController {
private final Logger logger = LoggerFactory.getLogger(LoginController.class);
//模拟資料庫
static Map<Integer, User> userMap = new HashMap<>();
static {
User user1 = new User(1, "zhangsan", "張三", "123456");
userMap.put(1, user1);
User user2 = new User(2, "lisi", "李四", "123123");
userMap.put(2, user2);
}
/**
* 模拟使用者登入
*/
@RequestMapping("/login")
public String login(User user){
for (User dbUser : userMap.values()) {
if (dbUser.getName().equals(user.getName()) && dbUser.getPassword().equals(user.getPassword())) {
logger.info("登入成功!生成token!");
String token = JwtUtil.createToken(dbUser);
return token;
}
}
return "";
}
}
- SecureController中的請求會被JWT過濾器攔截,合法後才能通路:
**
* 需要登入後才可通路
*/
@RestController
public class SecureController {
private final Logger logger = LoggerFactory.getLogger(SecureController.class);
/**
* 查詢使用者資訊,登入後才可通路
*/
@RequestMapping("/secure/getUserInfo")
public String getUserInfo(HttpServletRequest request){
Integer id = (Integer) request.getAttribute("id");
String name = request.getAttribute("name").toString();
String userName = request.getAttribute("userName").toString();
return "目前使用者資訊id=" + id + ",name=" + name + ",userName=" + userName;
}
}
測試時先通路登入接口,根據使用者名和密碼生成token,再将token放在請求頭裡,去通路需要登入才能通路的接口。
直接通路登入接口:http://localhost:8080/login?name=zhangsan&password=123456
登入成功則傳回token:
通路http://localhost:8080/secure/getUserInfo,請求頭裡需要攜帶token:
成功擷取使用者資訊。