為什么要告別session?有這樣一個場景,系統的數據量達到千萬級,需要幾台服務器部署,當一個用戶在其中一台服務器登錄后,用session保存其登錄信息,其他服務器怎么知道該用戶登錄了?(單點登錄),當然解決辦法有,可以用spring-session。如果該系統同時為移動端服務呢?移動端通過url向后台要數據,如果用session,通過sessionId識別用戶,萬一sessionId被截獲了,別人可以利用sessionId向后台要數據,就有安全隱患了。所以有必要跟session說拜拜了。服務端不需要存儲任何用戶的信息,用戶的驗證應該放在客戶端,jwt就是這種方式!
什么是jwt?
最詳細的是官網:https://jwt.io/
這里以java的ssm框架為例,集成jwt。
1.pom.xml 導入jwt的包
<!-- jwt -->
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>2.2.0</version>
</dependency>
2.編寫jwt的工具類,有加密解密功能就好
import com.auth0.jwt.JWTSigner;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.internal.com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
public class JWT {
private static final String SECRET = "XX#$%()(#*!()!KL<><MQLMNQNQJQK sdfkjsdrow32234545fdf>?N<:{LWPW";
private static final String EXP = "exp";
private static final String PAYLOAD = "payload";
//加密,傳入一個對象和有效期
public static <T> String sign(T object, long maxAge) {
try {
final JWTSigner signer = new JWTSigner(SECRET);
final Map<String, Object> claims = new HashMap<String, Object>();
ObjectMapper mapper = new ObjectMapper();
String jsonString = mapper.writeValueAsString(object);
claims.put(PAYLOAD, jsonString);
claims.put(EXP, System.currentTimeMillis() + maxAge);
return signer.sign(claims);
} catch(Exception e) {
return null;
}
}
//解密,傳入一個加密后的token字符串和解密后的類型
public static<T> T unsign(String jwt, Class<T> classT) {
final JWTVerifier verifier = new JWTVerifier(SECRET);
try {
final Map<String,Object> claims= verifier.verify(jwt);
if (claims.containsKey(EXP) && claims.containsKey(PAYLOAD)) {
long exp = (Long)claims.get(EXP);
long currentTimeMillis = System.currentTimeMillis();
if (exp > currentTimeMillis) {
String json = (String)claims.get(PAYLOAD);
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(json, classT);
}
}
return null;
} catch (Exception e) {
return null;
}
}
}
3.jwt有了,ssm要如何去利用,用戶驗證的第一步是登錄,登錄時根據用戶傳來的username和password到數據庫驗證身份,如果合法,便給該用戶jwt加密生成token
//處理登錄
@RequestMapping(value="login", produces = "application/json; charset=utf-8")
public @ResponseBody ResponseData login(HttpServletRequest request, @RequestParam( "email") String email,@RequestParam("password") String password) {
Login login = new Login();
login.setEmail(email);
login.setPassword(password);
ResponseData responseData = ResponseData.ok();
//先到數據庫驗證
Integer loginId = userService.checkLogin(login);
if(null != loginId) {
User user = userService.getUserByLoginId(loginId);
login.setId(loginId);
//給用戶jwt加密生成token
String token = JWT.sign(login, 60L* 1000L* 30L);
//封裝成對象返回給客戶端
responseData.putDataValue("loginId", login.getId());
responseData.putDataValue("token", token);
responseData.putDataValue("user", user);
}else{
responseData = ResponseData.customerError();
}
return responseData;
}
4.在用戶登錄時,把loginId和token返回給前台
以后用戶每次請求時,都得帶上這兩個參數,后台拿到token后解密出loginId,與用戶傳遞過來的loginId比較,如果相同,則說明用戶身份合法。因為是每個登錄過后的每個請求,這里用springmvc的攔截器做
<mvc:interceptors> <mvc:interceptor> <!-- 匹配的是url路徑, 如果不配置或/**,將攔截所有的Controller --> <mvc:mapping path="/**" /> <!-- /register 和 /login 不需要攔截--> <mvc:exclude-mapping path="/register" /> <mvc:exclude-mapping path="/login" /> <bean class="com.xforce.charles.interceptor.TokenInterceptor"></bean> </mvc:interceptor> <!-- 當設置多個攔截器時,先按順序調用preHandle方法,然后逆序調用每個攔截器的postHandle和afterCompletion方法 --> </mvc:interceptors>
5.攔截器代碼
import java.io.PrintWriter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import com.alibaba.fastjson.JSONObject;
import com.xforce.charles.model.Admin;
import com.xforce.charles.model.Login;
import com.xforce.charles.util.JWT;
import com.xforce.charles.util.ResponseData;
public class TokenInterceptor implements HandlerInterceptor{
public void afterCompletion(HttpServletRequest request,HttpServletResponse response, Object handler, Exception arg3)throws Exception {}
public void postHandle(HttpServletRequest request, HttpServletResponse response,Object handler, ModelAndView model) throws Exception {}
//攔截每個請求
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws Exception {
response.setCharacterEncoding("utf-8");
String token = request.getParameter("token");
ResponseData responseData = ResponseData.ok();
//token不存在
if(null != token) {
Login login = JWT.unsign(token, Login.class);
String loginId = request.getParameter("loginId");
//解密token后的loginId與用戶傳來的loginId不一致,一般都是token過期
if(null != loginId && null != login) {
if(Integer.parseInt(loginId) == login.getId()) {
return true;
}
else{
responseData = ResponseData.forbidden();
responseMessage(response, response.getWriter(), responseData);
return false;
}
}else{
responseData = ResponseData.forbidden();
responseMessage(response, response.getWriter(), responseData);
return false;
}
}else{
responseData = ResponseData.forbidden();
responseMessage(response, response.getWriter(), responseData);
return false;
}
}
//請求不通過,返回錯誤信息給客戶端
private void responseMessage(HttpServletResponse response, PrintWriter out, ResponseData responseData) {
responseData = ResponseData.forbidden();
response.setContentType("application/json; charset=utf-8");
String json = JSONObject.toJSONString(responseData);
out.print(json);
out.flush();
out.close();
}
}
6.注意點:用@ResponseBody返回json數據時,有時會有亂碼,需要在springmvc的配置文件里面加以下配置(spring4以上)
<mvc:annotation-driven> <mvc:message-converters register-defaults="true"> <bean class="org.springframework.http.converter.StringHttpMessageConverter"> <property name="supportedMediaTypes" value = "text/plain;charset=UTF-8" /> </bean> </mvc:message-converters> </mvc:annotation-driven>
7.最后分享一個類,用於返回給客戶端的萬能類,我覺得它可以滿足一般的接口
import java.util.HashMap;
import java.util.Map;
public class ResponseData {
private final String message;
private final int code;
private final Map<String, Object> data = new HashMap<String, Object>();
public String getMessage() {
return message;
}
public int getCode() {
return code;
}
public Map<String, Object> getData() {
return data;
}
public ResponseData putDataValue(String key, Object value) {
data.put(key, value);
return this;
}
private ResponseData(int code, String message) {
this.code = code;
this.message = message;
}
public static ResponseData ok() {
return new ResponseData(200, "Ok");
}
public static ResponseData notFound() {
return new ResponseData(404, "Not Found");
}
public static ResponseData badRequest() {
return new ResponseData(400, "Bad Request");
}
public static ResponseData forbidden() {
return new ResponseData(403, "Forbidden");
}
public static ResponseData unauthorized() {
return new ResponseData(401, "unauthorized");
}
public static ResponseData serverInternalError() {
return new ResponseData(500, "Server Internal Error");
}
public static ResponseData customerError() {
return new ResponseData(1001, "customer Error");
}
}
