JWT,即JSON Web Tokens,是用來解決web項目登錄時的token問題的一個解決方案,目前使用的也比較多。官網(wǎng)顯示用于java的jwt庫一共有6個,這里我們以其中auth0的使用為例,說明一下如何在Springboot項目中用jwt做登錄攔截。
依舊是在我們的schoolmanager項目上做修改。首先在build.gradle的dependencies中添加依賴包
implementation 'com.auth0:java-jwt:3.8.3'
接著需要新建一個jwt的工具類,先新建一個utils的工具包,在里面新建一個JWTUtil的工具類,代碼如下
public class JWTUtil {
private static final String SECRET = "euitrydbnseotu9347857025620";
private static String ISSUER = "sys_user";
/**
* 生成token
* @param claims
* @param expireDatePoint 過期時間點
* @return
*/
public static String genToken(Map<String, String> claims, Date expireDatePoint){
try {
//使用HMAC256進行加密
Algorithm algorithm = Algorithm.HMAC256(SECRET);
//創(chuàng)建jwt
JWTCreator.Builder builder = JWT.create().
withIssuer(ISSUER). //發(fā)行人
withExpiresAt(expireDatePoint); //過期時間點
//傳入?yún)?shù)
claims.forEach((key,value)-> {
builder.withClaim(key, value);
});
//簽名加密
return builder.sign(algorithm);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
}
}
/**
* 解密jwt
* @param token
* @return
* @throws RuntimeException
*/
public static Map<String,String> verifyToken(String token) throws RuntimeException{
Algorithm algorithm = null;
try {
//使用HMAC256進行加密
algorithm = Algorithm.HMAC256(SECRET);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
}
//解密
JWTVerifier verifier = JWT.require(algorithm).withIssuer(ISSUER).build();
Map<String, String> resultMap = new HashMap<>();
try {
DecodedJWT jwt = verifier.verify(token);
Map<String, Claim> map = jwt.getClaims();
map.forEach((k,v) -> resultMap.put(k, v.asString()));
} catch (TokenExpiredException e){
}
return resultMap;
}
}
這里有兩個方法,一個加密生成token,一個解密。其中SECRET和ISSUER都是可以自己定義的加密參數(shù),還能傳入過期時間。解密的時候要注意的是捕獲了TokenExpiredException,就是token過期了,這時會返回一個空的resultMap。
然后就是寫攔截器了。新建一個interceptor的包,在里面新建一個TokenInterceptor類,代碼如下
@Component
public class TokenInterceptor implements HandlerInterceptor {
/**
* 預處理回調(diào)方法,實現(xiàn)處理器的預處理(如檢查登陸),第三個參數(shù)為響應的處理器,自定義Controller
* 返回值:true表示繼續(xù)流程(如調(diào)用下一個攔截器或處理器);false表示流程中斷
(如登錄檢查失敗),不會繼續(xù)調(diào)用其他的攔截器或處理器,此時我們需要通過response來產(chǎn)生響應;
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//前端可能會發(fā)送OPTIONS請求,要先過濾
if (request.getMethod().equals("OPTIONS")){
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
response.setCharacterEncoding("utf-8");
String token = request.getHeader("token");
String responseData = "{\"code\":401,\"message\":\"Unauthorized\"}";
//token不存在
if(null != token) {
Map<String, String> login = JWTUtil.verifyToken(token);
String username = request.getHeader("username");
//解密token后的loginId與用戶傳來的loginId不一致,一般都是token過期
if(null != username && null != login) {
if(username.equals(login.get("username"))) {
return true;
}
else{
responseMessage(response, response.getWriter(), responseData);
return false;
}
}
else
{
responseMessage(response, response.getWriter(), responseData);
return false;
}
}
else
{
responseMessage(response, response.getWriter(), responseData);
return false;
}
}
private void responseMessage(HttpServletResponse response, PrintWriter out, String json) {
response.setContentType("application/json; charset=utf-8");
out.print(json);
out.flush();
out.close();
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
這個類繼承了HandlerInterceptor,它是Spring框架中的攔截器,用于對請求進行預處理和后處理,所以我們用它來攔截請求,檢驗token。預處理在preHandle中,這里會接收header中傳入的username和token,先看是否存在,然后解密token,看username是否一致,如果token過期,解密的resultMap為空,這里我們直接也返回false,提示前端重新登錄。實際項目中我們可能還需要提供token的刷新機制,這個我們后面再講。
之后要把這個攔截器配置到Springboot中,在interceptor包中新建一個WebConfigurer類,代碼如下
@Configuration
public class WebConfigurer implements WebMvcConfigurer {
@Autowired
private TokenInterceptor loginInterceptor;
// 這個方法用來注冊攔截器,我們自己寫好的攔截器需要通過這里添加注冊才能生效
@Override
public void addInterceptors(InterceptorRegistry registry) {
// addPathPatterns("/**") 表示攔截所有的請求,
// excludePathPatterns("/login", "/register") 表示除了登陸與注冊之外,因為登陸注冊不需要登陸也可以訪問
registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/login", "/register");
}
}
WebMvcConfigurer是一個spring的配置類,用來代替原來的xml的配置方式。這里只使用了它的注冊攔截器的方法addInterceptors,其余方法的使用可以參考https://blog.csdn.net/zhangpower1993/article/details/89016503。這里我們攔截了除了login和register之外的其他所有路徑請求,因為這兩個請求肯定是不需要驗證token的。
最后我們寫一個簡單的登錄方法,之后來測試這個登錄攔截是否可用。我們直接在之前創(chuàng)建過的UserController中修改,添加一個login的方法。
@RestController
public class UserController {
private String admin = "admin";
private String psd = "qwertyuiop";
@PostMapping(value = "/login")
public ResponseData login(final String username, final String password)
{
ResponseData responseData = ResponseData.ok();
if(username.equals(admin) && password.equals(psd)){
Map<String, String> map = new HashMap<String, String>();
map.put("username", username);
String token = JWTUtil.genToken(map, new Date(System.currentTimeMillis() + 60L* 1000L * 30L));
//封裝成對象返回給客戶端
responseData.putDataValue("username", username);
responseData.putDataValue("token", token);
}
else{
responseData = ResponseData.customerError();
}
return responseData;
}
}
為了簡單測試登錄功能,我們使用了固定的管理員賬號,用戶名和密碼都是固定的。這里設置了token過期時間是30分鐘。
然后我們就可以運行項目了,使用postman測試,先訪問addSubject接口,發(fā)現(xiàn)返回了401,證明攔截成功。

之后訪問登錄接口,獲得token

然后把username和token添加到header中,再次訪問addSubject接口,可以看到請求成功,數(shù)據(jù)庫中添加了新數(shù)據(jù)

最后如果需要測試token過期的情況,可以將過期時間調(diào)短,再進行測試,會發(fā)現(xiàn)token過期后再次返回401,提示用戶需要重新登錄。
本篇教程中的代碼依舊可以參考我在github上面的代碼https://github.com/ahuadoreen/studentmanager