springmvc使用JWT實現鑑權並防止流只能讀取一次的ERROR
阿新 • • 發佈:2019-02-15
為了保證介面的安全性,在restful服務介面中我們常常使用JWT進行登陸鑑權,JWT的原理很簡單:
登陸成功後用JWT根據登陸資訊生成一個token返回給呼叫者,呼叫者下次呼叫其它介面把登入資訊和相關token一起傳給服務,使用springmvc攔截器進行攔截驗證,看token是否有效,有效則跳轉到指定的controller進行處理!
以ssm框架為例
一.pom.xml 匯入jwt的包:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId >
<version>2.2.0</version>
</dependency>
二. 編寫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;
}
}
}
三. jwt有了,ssm要如何去利用,使用者驗證的第一步是登入,登入時根據使用者傳來的username和password到資料庫驗證身份,如果合法,便給該使用者jwt加密生成token
import javax.annotation.Resource;
import javax.ws.rs.core.MediaType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import com.alibaba.fastjson.JSONObject;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiParam;
import cn.com.wavenet.Execution.UniteUserExecution;
import cn.com.wavenet.enums.UniteUserStateEnum;
import cn.com.wavenet.execption.ErrorInfoException;
import cn.com.wavenet.execption.NoInfoException;
import cn.com.wavenet.result.Result;
import cn.com.wavenet.service.UniteUserService;
import cn.com.wavenet.service.impl.DataManagerImpl;
import cn.com.wavenet.service.impl.UniteUserServiceImpl;
import cn.com.wavenet.util.JSONUtil;
import cn.com.wavenet.util.JWT;
import cn.com.wavenet.util.NormalUtil;
@Api(value = "uniteUser", description = "太湖局統一使用者API", produces = MediaType.APPLICATION_JSON)
@Controller
@RequestMapping(value = "/services/common")
public class TbaUserController {
private Logger log = LoggerFactory.getLogger(this.getClass());
@Resource
NormalUtil normalUtil;
@Resource
DataManagerImpl dataManagerImpl;
@Resource
UniteUserService uniteUserService;
@ApiOperation(value = "checkLogin", notes = "驗證使用者", httpMethod = "POST", produces = MediaType.APPLICATION_JSON)
@ResponseBody
@RequestMapping(value = "/checkLogin",method = RequestMethod.POST,
produces = {MediaType.APPLICATION_JSON,"application/json;charset=UTF-8"})
public Result<UniteUserExecution> checkLogin(@ApiParam(value = "使用者名稱及密碼", required = true)@RequestBody String str){
if(!JSONUtil.isJson(str)){
return new Result<>(false,"請求格式非json");
}
JSONObject patrolJson = JSONObject.parseObject(str);
UniteUserExecution execution = null;
String token = null;
try {
execution = uniteUserService.checkLogin(patrolJson);
//給使用者jwt加密生成token
token = JWT.sign(execution, 60L* 1000L* 300L);
} catch (ErrorInfoException e) {
execution = new UniteUserExecution("錯誤資訊",UniteUserStateEnum.FAULTPASS);
} catch (NoInfoException e) {
execution = new UniteUserExecution("錯誤資訊",UniteUserStateEnum.NOUSER);
} catch (Exception e) {
execution = new UniteUserExecution("錯誤資訊",UniteUserStateEnum.INNER);
}
return new Result<>(true,token,execution);
}
}
4.在使用者登入時,把loginName和token返回給前臺,以後使用者每次請求時,都得帶上這兩個引數,後臺拿到token後解密出loginName,與使用者傳遞過來的loginName比較,如果相同,則說明使用者身份合法。因為是每個登入過後的每個請求,這裡用springmvc的攔截器實現:
<mvc:interceptors>
<mvc:interceptor>
<!-- 匹配的是url路徑, 如果不配置或/**,將攔截所有的Controller -->
<mvc:mapping path="/**" />
<!-- /login 不需要攔截-->
<mvc:exclude-mapping path="/register" />
<mvc:exclude-mapping path="/login" />
<bean class="com.xforce.charles.interceptor.TokenInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
五.攔截器程式碼
import java.io.PrintWriter;
import java.util.Scanner;
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 cn.com.wavenet.Execution.UniteUserExecution;
import cn.com.wavenet.util.JWT;
import cn.com.wavenet.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");
Scanner s = new Scanner(request.getInputStream(), "UTF-8").useDelimiter("\\A");
if(true){
return true;
}
String jsonString = null;
if(s.hasNext()){
jsonString = s.next();
}
System.out.println(jsonString);
JSONObject patrolJson = JSONObject.parseObject(jsonString);
JSONObject reqInfoJson = patrolJson.getJSONObject("reqInfo");
ResponseData responseData = ResponseData.ok();
String token = reqInfoJson.getString("token");
//token不存在
if(null != token) {
UniteUserExecution execution = JWT.unsign(token, UniteUserExecution.class);
String loginName = patrolJson.getString("loginName");
//解密token後的loginId與使用者傳來的loginId不一致,一般都是token過期
if(null != loginName && null != execution) {
if(loginName.equals(execution.getInfo())){
System.out.println(loginName.equals(execution.getInfo()));
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();
}
}
但是如果我們接收的是一個@RequestBody json字串就需要通過流來讀取內容,但是在上面的攔截器讀取一次流後,controller就不能再次讀取了,解決辦法是用過濾器提前儲存流
六.web.xml中新增過濾器
<!-- 流過濾器 -->
<filter>
<filter-name>sessionFilter</filter-name>
<filter-class>cn.com.wavenet.filter.SessionFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>sessionFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
七.過濾器處理:
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.apache.log4j.Logger;
/**
* 過濾器把請求流儲存起來
*
*/
public class SessionFilter implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// 防止流讀取一次後就沒有了, 所以需要將流繼續寫出去
ServletRequest requestWrapper = new BodyReaderHttpServletRequestWrapper(httpServletRequest);
chain.doFilter(requestWrapper, response);
}
@Override
public void destroy() {
}
/**
* 儲存流
*
* @author lyj 2015年12月16日
*/
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
private Logger log = Logger.getLogger(this.getClass());
private final byte[] body;
public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
String sessionStream = getBodyString(request);
body = sessionStream.getBytes(Charset.forName("UTF-8"));
log.debug("儲存的流" + sessionStream);
}
/**
* 獲取請求Body
*
* @param request
* @return
*/
public String getBodyString(final ServletRequest request) {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = cloneInputStream(request.getInputStream());
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
}
catch (IOException e) {
e.printStackTrace();
}
finally {
if (inputStream != null) {
try {
inputStream.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
if (reader != null) {
try {
reader.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
return sb.toString();
}
/**
* Description: 複製輸入流</br>
*
* @param inputStream
* @return</br>
*/
public InputStream cloneInputStream(ServletInputStream inputStream) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
try {
while ((len = inputStream.read(buffer)) > -1) {
byteArrayOutputStream.write(buffer, 0, len);
}
byteArrayOutputStream.flush();
}
catch (IOException e) {
e.printStackTrace();
}
InputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
return byteArrayInputStream;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
};
}
}
}
這樣就解決了springmvc攔截器讀取流不能跳轉到controller的問題!