關於SpringBoot創建存儲令牌的媒介類和過濾器的問題
之所以需要創建存儲令牌的媒介類,是因為後面的filter界面要使用。
一、創建ThreadLocalToken類
創建ThreadLocalToken類的目的:
在com.example.emos.wx.config.shiro
中創建ThreadLocalToken
類。
寫入如下代碼:
package com.example.emos.wx.config.shiro; import org.springframework.stereotype.Component; @Component public class ThreadLocalToken { private ThreadLocal local=new ThreadLocal(); //因為要在ThreadLocal中保存令牌,所以需要setToken。 public void setToken(String token){ local.set(token); } public String getToken(){ return (String) local.get(); } public void clear(){ local.remove();//把綁定的數據刪除瞭 } }
下圖為創建目錄的層級關系:
二、創建OAuth2Filter類
創建過濾器的目的:
因為OAuth2Filter
類要讀寫ThreadLocal
中的數據,所以OAuth2Filter
類必須要設置成多例的,否則ThreadLocal
將無法使用。
在配置文件中,添加JWT需要的密匙,過期時間和緩存過期時間。
emos: jwt: #密鑰 secret: abc123456 #令牌過期時間(天) expire: 5 #令牌緩存時間(天數) cache-expire: 10
在com.example.emos.wx.config.shiro
中創建OAuth2Filter
類。
package com.example.emos.wx.config.shiro; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.TokenExpiredException; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.web.filter.authc.AuthenticatingFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Scope; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.RequestMethod; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.concurrent.TimeUnit;
在寫好@Scope("prototype")
後,就表明以後Spring使用OAuth2Filter
類默認是多例。
@Value(“${emos.jwt.cache-expire}”)
考察的一個知識點,從xml文件中獲取屬性文件的屬性值。
因為要在Redis中操作,所以要聲明private RedisTemplate redisTemplate;
申明好這個對象後,就可以對redis中的數據進行讀寫操作瞭。
filter類用來區分哪些請求應該被shiro處理,哪些請求不該被shiro處理。
如果請求被shiro處理的話,那麼createToken方法就被執行瞭,
createToken從請求中獲取令牌字符串,然後封裝成令牌對象OAuth2Token,交給shiro框架去處理。
getRequestToken是一個自定義方法,用來獲取令牌字符串,然後傳遞給字符串Token對象。
@Component @Scope("prototype") public class OAuth2Filter extends AuthenticatingFilter { @Autowired private ThreadLocalToken threadLocalToken; @Value("${emos.jwt.cache-expire}") private int cacheExpire; @Autowired private JwtUtil jwtUtil; @Autowired private RedisTemplate redisTemplate; /** * 攔截請求之後,用於把令牌字符串封裝成令牌對象 */ @Override protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception { //獲取請求token String token = getRequestToken((HttpServletRequest) request); if (StringUtils.isBlank(token)) { return null; } return new OAuth2Token(token); }
filter過濾這一塊細講一下:
isAccessAllowed
是判斷哪些請求可以被shiro處理,哪些不可以被shiro處理。
由於isAccessAllowed
方法中request
是ServletRequest
,所以需要進行轉換HttpServletRequest
,
然後判斷這次request
請求是不是options請求。如果不是,就需要被shiro處理。
/** * 攔截請求,判斷請求是否需要被Shiro處理 */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { HttpServletRequest req = (HttpServletRequest) request; // Ajax提交application/json數據的時候,會先發出Options請求 // 這裡要放行Options請求,不需要Shiro處理 if (req.getMethod().equals(RequestMethod.OPTIONS.name())) { return true; } // 除瞭Options請求之外,所有請求都要被Shiro處理 return false; }
那麼,shiro是怎麼處理的呢?
onAccessDenied 方法
設置響應的字符集,和響應的請求頭。setHeader
方法用來設置跨域請求。
/** * 該方法用於處理所有應該被Shiro處理的請求 */ @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; resp.setHeader("Content-Type", "text/html;charset=UTF-8"); //允許跨域請求 resp.setHeader("Access-Control-Allow-Credentials", "true"); resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin")); //clear方法用來清理threadLocal類中的方法, threadLocalToken.clear(); //獲取請求token,如果token不存在,直接返回401 String token = getRequestToken((HttpServletRequest) request); if (StringUtils.isBlank(token)) { resp.setStatus(HttpStatus.SC_UNAUTHORIZED); resp.getWriter().print("無效的令牌"); return false; }
然後驗證令牌是否過期。
如果驗證出現問題,就會拋出異常。
通過捕獲異常,就知道是令牌有問題,還是令牌過期瞭。
JWTDecodeException
是內容異常。
通過redisTemplate
的hasKey
查詢Redis
是否存在令牌。
如果存在令牌,就刪除老令牌,重新生成一個令牌,給客戶端。
executeLogin
方法,讓shiro
執行realm
類。
try { jwtUtil.verifierToken(token); //檢查令牌是否過期 } catch (TokenExpiredException e) { //客戶端令牌過期,查詢Redis中是否存在令牌,如果存在令牌就重新生成一個令牌給客戶端 if (redisTemplate.hasKey(token)) { redisTemplate.delete(token);//刪除老令牌 int userId = jwtUtil.getUserId(token); token = jwtUtil.createToken(userId); //生成新的令牌 //把新的令牌保存到Redis中 redisTemplate.opsForValue().set(token, userId + "", cacheExpire, TimeUnit.DAYS); //把新令牌綁定到線程 threadLocalToken.setToken(token); } else { //如果Redis不存在令牌,讓用戶重新登錄 resp.setStatus(HttpStatus.SC_UNAUTHORIZED); resp.getWriter().print("令牌已經過期"); return false; } } catch (JWTDecodeException e) { resp.setStatus(HttpStatus.SC_UNAUTHORIZED); resp.getWriter().print("無效的令牌"); return false; } boolean bool = executeLogin(request, response); return bool; }
登錄失敗後輸出的信息。
@Override protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; resp.setStatus(HttpStatus.SC_UNAUTHORIZED); resp.setContentType("application/json;charset=utf-8"); resp.setHeader("Access-Control-Allow-Credentials", "true"); resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin")); try { resp.getWriter().print(e.getMessage());//捕獲認證失敗的消息 } catch (IOException exception) { } return false; }
獲取請求頭裡面的token
/** * 獲取請求頭裡面的token */ private String getRequestToken(HttpServletRequest httpRequest) { //從header中獲取token String token = httpRequest.getHeader("token"); //如果header中不存在token,則從參數中獲取token if (StringUtils.isBlank(token)) { token = httpRequest.getParameter("token"); } return token; }
doFilterInternal
方法從父類doFilterInternal中繼承,掌管攔截請求和響應的。這裡不覆寫。
@Override public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { super.doFilterInternal(request, response, chain); } }
到此這篇關於SpringBoot創建存儲令牌的媒介類和過濾器的文章就介紹到這瞭,更多相關SpringBoot內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- SpringBoot配置自定義攔截器實現過程詳解
- springBoot前後端分離項目中shiro的302跳轉問題
- springboot簡單實現單點登錄的示例代碼
- shiro與spring security用自定義異常處理401錯誤
- 使用springcloud+oauth2攜帶token去請求其他服務