1. 程式人生 > >springmvc使用JWT實現鑑權並防止流只能讀取一次的ERROR

springmvc使用JWT實現鑑權並防止流只能讀取一次的ERROR

為了保證介面的安全性,在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的問題!