SpringBoot 整合 JWT
1 什麼是 JWT
JSON Web Token(JWT)是一個非常輕巧的規範,這個規範允許我們使用 JWT 在用戶和服務器之間傳遞安全可靠的信息,在 Java 世界中通過 JJWT 實現 JWT 創建和驗證。
2 快速上手
2.1 pom.xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1<version>
</dependency>
2.2 測試類
import io.jsonwebtoken.*;
import java.util.Date;
/**
* @author PkyShare
* @date 2020/1/3 0003 16:06
*/
public class JwtTest {
private final static String key = "share"; // 祕鑰
public static void main(String[] args) {
String token = created();
parse(token);
}
/**
* 創建token
*/
public static String created() {
JwtBuilder jwtBuilder = Jwts.builder().setId("123456") // 設置ID
.setSubject("PkyShare") // 存放的內容
.setIssuedAt(new Date()) // 簽名簽發時間
.signWith(SignatureAlgorithm.HS256, key) // 加密算法以及祕鑰
.claim("school", "gcd"); // 自定義內容,key-value 形式
String token = jwtBuilder.compact();
System.out.println(token); // 創建 JwtBuilder 對象並打印
return token;
}
/**
* 解析 token
* @param token
*/
public static void parse(String token) {
Claims claims = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
System.out.println(claims.getId());
System.out.println(claims.getSubject());
System.out.println(claims.getIssuedAt());
String school = (String) claims.get("school");
System.out.println("school----" + school);
}
}
2.3 測試結果
3 SpringBoot 整合 JWT
以上簡單的使用我們基本瞭解了 token 的生成,接下來完成一個簡單的登錄邏輯。
3.1 pom.xml
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
3.2 JwtUtil 工具類
import com.huanda.chetaijitoc.commons.domain.userdb.UserInfo;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* JWT 工具類
* @author PkyShare
* @date 2020/1/3 0003 16:39
*/
public class JwtUtil {
//token 密鑰
private static final String TOKEN_SECRET = "dfsjeo329safei22kdfeiajdeie1";
//15分鐘超時時間
private static final long OUT_TIME = 150 * 60 * 1000;
/**
* 用戶登錄成功後生成Jwt
* 使用Hs256算法 私匙使用用戶密碼
* @param user 登錄成功的user對象
* @return
*/
public static String createJWT(UserInfo user) {
//指定簽名的時候使用的簽名算法,也就是header那部分,jjwt已經將這部分內容封裝好了。
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//生成JWT的時間
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//創建payload的私有聲明(根據特定的業務需要添加,如果要拿這個做驗證,一般是需要和jwt的接收方提前溝通好驗證方式的)
Map<String, Object> claims = new HashMap<>();
claims.put("id", user.getId());
claims.put("username", user.getUsername());
claims.put("password", user.getPassword());
//生成簽名的時候使用的祕鑰secret,這個方法本地封裝了的,一般可以從本地配置文件中讀取,切記這個祕鑰不能外露哦。它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。
// String key = user.getPassword();
//生成簽發人
String subject = user.getUsername();
//下面就是在爲payload添加各種標準聲明和私有聲明瞭
//這裏其實就是new一個JwtBuilder,設置jwt的body
JwtBuilder builder = Jwts.builder()
//如果有私有聲明,一定要先設置這個自己創建的私有的聲明,這個是給builder的claim賦值,一旦寫在標準的聲明賦值之後,就是覆蓋了那些標準的聲明的
.setClaims(claims)
//設置jti(JWT ID):是JWT的唯一標識,根據業務需要,這個可以設置爲一個不重複的值,主要用來作爲一次性token,從而回避重放攻擊。
.setId(UUID.randomUUID().toString())
//iat: jwt的簽發時間
.setIssuedAt(now)
//代表這個JWT的主體,即它的所有人,這個是一個json格式的字符串,可以存放什麼userid,roldid之類的,作爲什麼用戶的唯一標誌。
.setSubject(subject)
//設置簽名使用的簽名算法和簽名使用的祕鑰
.signWith(signatureAlgorithm, TOKEN_SECRET);
long expMillis = nowMillis + OUT_TIME;
Date exp = new Date(expMillis);
//設置過期時間
builder.setExpiration(exp);
return builder.compact();
}
/**
* Token的解密
* @param token 加密後的token
* @return
*/
public static Claims parseJWT(String token) {
//簽名祕鑰,和生成的簽名的祕鑰一模一樣
//得到DefaultJwtParser
Claims claims = Jwts.parser()
//設置簽名的祕鑰
.setSigningKey(TOKEN_SECRET)
//設置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}
/**
* 校驗token
* 在這裏可以使用官方的校驗,我這裏校驗的是token中攜帶的密碼於數據庫一致的話就校驗通過
* @param claims Payload(載荷)
* @param user
* @return
*/
public static Boolean isVerify(Claims claims, UserInfo user) {
if (claims.get("password").equals(user.getPassword())) {
return true;
}
return false;
}
}
3.3 攔截器
除了登錄、註冊請求外,其他請求都需要攜帶 token,因此需要設置一個攔截器進行攔截。
3.3.1 @LoginToken 登錄註解
方法上用到該註解的則跳過 token 驗證。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author PkyShare
* @date 2020/1/3 0003 17:25
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginToken {
boolean required() default true;
}
3.3.2 @CheckToken 校驗註解
方法上用到該註解的則校驗 token。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author PkyShare
* @date 2020/1/3 0003 17:24
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckToken {
boolean required() default true;
}
3.3.3 攔截器配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author PkyShare
* @date 2020/1/3 0003 17:49
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**"); // 攔截所有請求,通過判斷是否有 @LoginRequired 註解 決定是否需要登錄
}
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}
3.3.4 Token 攔截器
import com.alibaba.fastjson.JSONObject;
import com.huanda.chetaijitoc.admin.annotation.CheckToken;
import com.huanda.chetaijitoc.admin.annotation.LoginToken;
import com.huanda.chetaijitoc.admin.annotation.QueryToken;
import com.huanda.chetaijitoc.commons.constant.HttpStatus;
import com.huanda.chetaijitoc.commons.domain.userdb.UserInfo;
import com.huanda.chetaijitoc.commons.service.userdb.UserInfoService;
import com.huanda.chetaijitoc.commons.service.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.SignatureException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Method;
/**
* token 攔截器
* @author PkyShare
* @date 2020/1/3 0003 17:28
*/
public class AuthenticationInterceptor implements HandlerInterceptor{
@Autowired
UserInfoService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception {
// 從 http 請求頭中取出 token
String token = request.getHeader("token");
// 如果不是映射到方法直接通過
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//檢查是否有LoginToken註釋,有則跳過認證
if (method.isAnnotationPresent(LoginToken.class)) {
LoginToken loginToken = method.getAnnotation(LoginToken.class);
if (loginToken.required()) {
return true;
}
}
//檢查是否有QueryToken註釋,有則判斷token
if(method.isAnnotationPresent(QueryToken.class)) {
QueryToken queryToken = method.getAnnotation(QueryToken.class);
if(queryToken.required()) {
return setRequest(request, response, token);
}
}
//檢查有沒有需要用戶權限的註解
if (method.isAnnotationPresent(CheckToken.class)) {
CheckToken checkToken = method.getAnnotation(CheckToken.class);
if (checkToken.required()) {
return checkToken(request, response, token);
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) throws Exception {
}
/**
* 違章查詢時設置request
* @param request
* @param response
* @param token
* @return
* @throws Exception
*/
private boolean setRequest(HttpServletRequest request, HttpServletResponse response, String token) throws Exception {
if(StringUtils.isBlank(token)) {
return true;
}
return checkToken(request, response, token);
}
/**
* 校驗 token
* @param request
* @param response
* @param token
* @return
*/
private boolean checkToken(HttpServletRequest request, HttpServletResponse response, String token) throws Exception {
// 執行認證
if (StringUtils.isBlank(token)) {
setResultJson(response, HttpStatus.MISSING_PARAMS.getCode(), "Token 不可爲空");
return false;
}
// 獲取 token 中的 user id
Long userId;
Claims claims;
try {
claims = JwtUtil.parseJWT(token);
userId = (Long) claims.get("id");
}
catch (ExpiredJwtException e) { // 簽名過期
setResultJson(response, HttpStatus.TOKEN_EXPIRED.getCode(), HttpStatus.TOKEN_EXPIRED.getTitle());
return false;
}
catch (SignatureException e) { // 簽名錯誤
setResultJson(response, HttpStatus.TOKEN_ERROR.getCode(), HttpStatus.TOKEN_ERROR.getTitle());
return false;
}
catch (Exception e) { // 其他未知異常
setResultJson(response, HttpStatus.ABNORMAL_ACCESS.getCode(), HttpStatus.ABNORMAL_ACCESS.getTitle());
return false;
}
UserInfo user = userService.getById(userId);
if (user == null) {
setResultJson(response, HttpStatus.USER_NOT_EXIT.getCode(), HttpStatus.USER_NOT_EXIT.getTitle());
return false;
}
Boolean verify = JwtUtil.isVerify(claims, user);
if (!verify) {
setResultJson(response, HttpStatus.USER_INFO_ERROR.getCode(), HttpStatus.USER_INFO_ERROR.getTitle());
return false;
}
request.setAttribute("userInfo", user);
return true;
}
/**
* 設置響應json數據
* @param response
* @param code 狀態碼
* @param title 返回說明
* @return
*/
private void setResultJson(HttpServletResponse response, Integer code, String title) throws Exception{
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
JSONObject resultJson = new JSONObject();
resultJson.put("code", code);
resultJson.put("count", 0);
resultJson.put("title", title);
writer.append(resultJson.toJSONString());
}
}
3.4 自定義返回狀態碼
/**
* HTTP 狀態碼
*/
public enum HttpStatus {
OK(20000, "請求成功"),
SUCCESS(20001, "保存成功"),
DELETE(20004, "刪除成功"),
FORBIDDEN(40003, "權限不足"),
NOT_FOUND(40004, "資源未找到"),
USER_NOT_EXIT(40081, "用戶不存在"),
TOKEN_ERROR(40082, "簽名錯誤"),
TOKEN_EXPIRED(40083, "Token 過期,請重新登錄"),
USER_INFO_ERROR(40084, "賬號或密碼錯誤"),
ABNORMAL_ACCESS(40091, "異常訪問"),
private Integer code;
private String title;
HttpStatus(Integer code, String title) {
this.code = code;
this.title = title;
}
public Integer getCode() {
return code;
}
public String getTitle() {
return title;
}
}
3.5 Controller
package com.huanda.chetaijitoc.admin.controller.userdb;
import com.huanda.chetaijitoc.admin.annotation.LoginToken;
import com.huanda.chetaijitoc.admin.controller.base.AbstractBaseController;
import com.huanda.chetaijitoc.commons.constant.HttpStatus;
import com.huanda.chetaijitoc.commons.domain.userdb.UserInfo;
import com.huanda.chetaijitoc.commons.dto.AbstractBaseResult;
import com.huanda.chetaijitoc.commons.dto.LoadReturnData;
import com.huanda.chetaijitoc.commons.service.userdb.UserInfoService;
import com.huanda.chetaijitoc.commons.utils.BeanValidator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
/**
* 用戶基本信息表控制器
*/
@RestController
@RequestMapping(value = "users")
public class UserInfoController extends AbstractBaseController<UserInfo> {
@Autowired
UserInfoService userInfoService;
/**
* 登錄
* @param userInfo 用戶信息
* @return
*/
@PostMapping(value = "login")
@LoginToken
public AbstractBaseResult login(@RequestBody UserInfo userInfo) {
LoadReturnData<UserInfo> loadReturnData = new LoadReturnData<>();
loadReturnData = userInfoService.getByUsername(loadReturnData, userInfo);
return result(loadReturnData.getCode(), loadReturnData.getMsg(), loadReturnData.getToken());
}
/**
* 註冊
* @param userInfo 用戶信息
* @return
*/
@LoginToken
@PostMapping(value = "register")
public AbstractBaseResult register(@RequestBody UserInfo userInfo) {
// 數據校驗
String message = BeanValidator.validator(userInfo);
if(StringUtils.isNotBlank(message)) {
return result(HttpStatus.MISSING_PARAMS.getCode(), 0, message, null);
}
LoadReturnData<UserInfo> loadReturnData = new LoadReturnData<>();
loadReturnData = userInfoService.registe(loadReturnData, userInfo);
return result(loadReturnData.getCode(), 0, loadReturnData.getMsg(), null);
}
@CheckToken
@PostMapping(value = "/test")
public AbstractBaseResult test() {
return result(HttpStatus.OK.getCode(), 0, HttpStatus.OK.getTitle(), null);
}
}
注:上述 AbstractBaseResult(統一返回結果)、LoadReturnData、BeanValidator 和 AbstractBaseController 是自己封裝的,這裏可以暫不理會,用自己的寫的返回即可。
3.6 UserInfoserviceImpl
/**
* 通過用戶名獲取用戶信息並設置 token
* @param loadReturnData 承載數據模型
* @param userInfo 登錄用戶信息
* @return
*/
@Override
public LoadReturnData<UserInfo> getByUsername(LoadReturnData<UserInfo> loadReturnData, UserInfo userInfo){
Example example = new Example(UserInfo.class);
example.createCriteria().andEqualTo("username", userInfo.getUsername());
UserInfo userDB = userInfoMapper.selectOneByExample(example);
if(userDB == null) {
loadReturnData.setMsg(HttpStatus.USER_NOT_EXIT.getTitle());
loadReturnData.setCode(HttpStatus.USER_NOT_EXIT.getCode());
return loadReturnData;
}
if(!userDB.getPassword().equals(userInfo.getPassword())) {
loadReturnData.setMsg(HttpStatus.USER_INFO_ERROR.getTitle());
loadReturnData.setCode(HttpStatus.USER_INFO_ERROR.getCode());
return loadReturnData;
}
loadReturnData.setCode(HttpStatus.OK.getCode());
loadReturnData.setMsg("登錄成功");
loadReturnData.setToken(JwtUtil.createJWT(userDB));
return loadReturnData;
}
3.7 Token 校驗測試
- 登錄測試
- 需要校驗 token
至此,springboot 整合 JWT 基本完成。