閱讀此文前請先閱讀上一篇SpringBoot整合JWT實現用戶認證了解JWT。
背景介紹:
因項目需求,有PC端 APP端和小程序端,但登陸接口是同一個,然而微服務也無法使用傳統的session解決用戶登錄問題(注意這里是傳統的session不是spring session),使用戶信息在其他服務共享。
如此一來就想到了token安全認證,而JWT生成token可以包含用戶信息,也就果斷選擇了JWT作為SpringCloud gateway網關的token校驗工具,這樣,我們便可以直接解析token獲取用戶信息了。
具體實現思路:
-
讓JWT在其他所有服務可以共同使用,父工程需要引入JWT jar。避免在其他服務重復引入。
-
如何使用JWT生成token。
-
如何解析token。
-
如何讓網關攔截用戶請求校驗token。
-
如何避免首次登錄被網關攔截。
代碼實現:
1.創建SpringCloud項目
SpringCloud子項目包含 eureka,gateway,auth三個工程,父工程maven依賴如下。
<dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>6.0</version> </dependency>
2.Auth和gateway編寫TOKEN工具類
public class Token { private static final Logger log = LoggerFactory.getLogger(Token.class); /** * 1.創建一個32-byte的密匙JWT生成TOKEN */ private static final byte[] secret = "geiwodiangasfdjsikolkjikolkijswe".getBytes(); //生成一個token public static String creatToken(Map<String,Object> payloadMap) throws JOSEException { //3.先建立一個頭部Header /** * JWSHeader參數:1.加密算法法則,2.類型,3.。。。。。。。 * 一般只需要傳入加密算法法則就可以。 * 這里則采用HS256 * JWSAlgorithm類里面有所有的加密算法法則,直接調用。 */ JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.HS256); //建立一個載荷Payload Payload payload = new Payload(new JSONObject(payloadMap)); //將頭部和載荷結合在一起 JWSObject jwsObject = new JWSObject(jwsHeader, payload); //建立一個密匙 JWSSigner jwsSigner = new MACSigner(secret); //簽名 jwsObject.sign(jwsSigner); //生成token return jwsObject.serialize(); } /** * 解析一個token * @param token * @return * @throws ParseException * @throws JOSEException */ public static Map<String,Object> valid(String token) throws ParseException, JOSEException { //解析token JWSObject jwsObject = JWSObject.parse(token); //獲取到載荷 Payload payload=jwsObject.getPayload(); //建立一個解鎖密匙 JWSVerifier jwsVerifier = new MACVerifier(secret); Map<String, Object> resultMap = new HashMap<>(); //判斷token if (jwsObject.verify(jwsVerifier)) { resultMap.put("Result", 0); //載荷的數據解析成json對象。 JSONObject jsonObject = payload.toJSONObject(); resultMap.put("data", jsonObject); //判斷token是否過期 if (jsonObject.containsKey("exp")) { Long expTime = Long.valueOf(jsonObject.get("exp").toString()); Long nowTime = new Date().getTime(); //判斷是否過期 if (nowTime > expTime) { //已經過期 resultMap.clear(); resultMap.put("Result", 2); } } }else { resultMap.put("Result", 1); } return resultMap; } /** * 生成token的業務邏輯 登錄接口調用次業務 * @param uid * @return */ public static String TokenTest(Long uid,Long deptId,String userType,int companyId) { //獲取生成token Map<String, Object> map = new HashMap<>(); //建立載荷,這些數據根據業務,自己定義。 map.put("uid", uid); map.put("deptId", deptId); map.put("userType", userType); map.put("companyId", companyId); //生成時間 map.put("sta", new Date().getTime()); //過期時間 map.put("exp", new Date().getTime()+1000*3600*24*15); try { String token = Token.creatToken(map); System.out.println("token="+token); return token; } catch (JOSEException e) { System.out.println("生成token失敗"); e.printStackTrace(); } return null; } /** * 處理解析的業務邏輯 gateway JWT認證過濾器解析 * @param token */ public static Map<String,Object> ValidToken(String token) { Map<String, Object> userMsg = new HashMap<String, Object>(); //解析token try { if (token != null) { Map<String, Object> validMap = Token.valid(token); int i = (int) validMap.get("Result"); if (i == 0) { log.info("token解析成功"); JSONObject jsonObject = (JSONObject) validMap.get("data"); log.info("uid是:" + jsonObject.get("uid")); log.info("deptId是:" + jsonObject.get("deptId")); log.info("userType是:" + jsonObject.get("userType")); log.info("companyId是:" + jsonObject.get("companyId")); log.info("生成時間是:"+jsonObject.get("sta")); log.info("過期時間是:"+jsonObject.get("exp")); userMsg.put("token",token); userMsg.put("uid",jsonObject.get("uid")); userMsg.put("deptId",jsonObject.get("deptId")); userMsg.put("companyId",jsonObject.get("companyId")); userMsg.put("userType",jsonObject.get("userType")); return userMsg; } else if (i == 2) { log.info("token已經過期"); return userMsg; } } } catch (ParseException e) { e.printStackTrace(); } catch (JOSEException e) { e.printStackTrace(); } return userMsg; } public static void main(String[] ages) { //獲取token Long uid = 1L; Long deptId = 2L; String userType = "3"; int companyId = 4; String token = TokenTest(uid,deptId,userType,companyId); //解析token log.info(ValidToken(token).toString()); } }
特別提示:以上工具類可以在用戶登錄授權接口中調用,用以生成token,示例代碼如下(可以借鑒不可復制哦,請根據自己業務邏輯在合適的地方調用TOKEN工具)
@RestController @RequestMapping("/currency") public class CurrencyLoginController { //密鑰 (需要前端和后端保持一致) private static final String KEY = "abcdefgabcdefg12"; //redis初始KEY值 private static final String LOGIN_USER = "login_user"; @Autowired private RedisUtil ru; @PostMapping("/login") public Map<String, Object> ajaxLogin(String username, String password, Boolean rememberMe) throws Exception{ password = AESUtil.aesDecrypt(password,KEY);//雙向加密規則 UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe); Subject subject = SecurityUtils.getSubject(); try{ subject.login(token); User user = ShiroUtils.getUser(); String access_token = Token.generateToken(user.getUserId(), user.getDeptId(),user.getLoginUserType(), user.getCompanyId()); UserMsg resultUser = new UserMsg(); resultUser.setCompanyId(user.getCompanyId()); resultUser.setUserType(user.getLoginUserType()); resultUser.setDeptId(user.getDeptId()); resultUser.setUid(user.getUserId()); resultUser.setToken(access_token); ru.set(LOGIN_USER+user.getUserId(), resultUser, 3600*24*15); return ResultMap.ok("登錄成功", resultUser);//改造——》》獲取用戶信息保存到redis中實現用戶信息在微服務中共享,生成token }catch (AuthenticationException e){ String msg = "用戶或密碼錯誤"; if (StringUtils.isNotEmpty(e.getMessage())){ msg = e.getMessage(); } return ResultMap.error(msg); } } }
好了,此時呢,我們已經通過auth工程完成了用戶登錄授權,並且生成了token。那么如何在gateway網關中進行token認證呢?
3.gateway網關中編寫JwtCheckGatewayFilterFactory過濾器。
此類需要繼承gateway的AbstractGatewayFilterFactory。
代碼實現如下:
首先gateway網關yml文件中需要代理auth路由。
spring: cloud: gateway: routes: - id: neo_route uri: lb://YUNXI-AUTH predicates: - Path=/auth/** filters: - StripPrefix=1 - JwtCheck
自定義 JwtCheckGatewayFilterFactory 繼承 AbstractGatewayFilterFactory 抽象類,代碼如下:
public class JwtCheckGatewayFilterFactory extends AbstractGatewayFilterFactory<JwtCheckGatewayFilterFactory.Config> { private static final Logger log = LoggerFactory.getLogger(JwtCheckGatewayFilterFactory .class); //定義用戶認證登錄接口 private static final String CURRENCY_URL="/currency/login"; //redis初始KEY值 private static final String LOGIN_USER = "login_user"; @Autowired private RedisUtil ru; public JwtCheckGatewayFilterFactory() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { String jwtToken = exchange.getRequest().getHeaders().getFirst("Authorization"); log.info(exchange.getRequest().getURI().toString()); //校驗jwtToken的合法性,如果當前請求url和認證url相同跳過認證,表示用戶首次登錄認證 if(exchange.getRequest().getURI().toString().contains(CURRENCY_URL)){ return chain.filter(exchange); } if(jwtToken != null){ log.info(Token.ValidToken(jwtToken).toString()); //解析TOKEN Map<String, Object> userMsg = Token.ValidToken(jwtToken); Long uid = (Long) userMsg.get("uid"); if(ru.hasKey(LOGIN_USER+uid)){ Object obj = ru.get(LOGIN_USER+uid); UserMsg userModel = (UserMsg) obj; //解析客戶端傳過來的TOKEN是否和緩存中的TOKEN相同,並且判斷TOKEN過期時間是否大於當前時間 if(userModel.getToken().equals(jwtToken)){ return chain.filter(exchange); }else{ ServerHttpResponse response = exchange.getResponse(); String warningStr = "不合法的請求"; DataBuffer bodyDataBuffer = response.bufferFactory().wrap(warningStr.getBytes()); return response.writeWith(Mono.just(bodyDataBuffer)); } }else{ ServerHttpResponse response = exchange.getResponse(); String warningStr = "登錄超時"; DataBuffer bodyDataBuffer = response.bufferFactory().wrap(warningStr.getBytes()); return response.writeWith(Mono.just(bodyDataBuffer)); } } //不合法(響應未登錄的異常) ServerHttpResponse response = exchange.getResponse(); //設置headers HttpHeaders httpHeaders = response.getHeaders(); httpHeaders.add("Content-Type", "application/json; charset=UTF-8"); httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"); //設置body String warningStr = "未授權的請求,請登錄"; DataBuffer bodyDataBuffer = response.bufferFactory().wrap(warningStr.getBytes()); return response.writeWith(Mono.just(bodyDataBuffer)); }; } public static class Config { //Put the configuration properties for your filter here } }
編寫config文件將JWT認證過濾器添加到Spring bean中。
@Configuration public class AppConfig { @Bean public JwtCheckGatewayFilterFactory jwtCheckGatewayFilterFactory(){ return new JwtCheckGatewayFilterFactory(); } }
此時我們就完成了整個token認證過程,其實簡單的來說就是:
-
第一步:Auth工程配合用戶登錄生成token,並將token和用戶信息存儲在redis中。
-
第二步:在gayeway中編寫JWT認證過濾器,用以校驗用戶請求中攜帶的token。
有圖有真相
特別提示:我的auth工程端口是8766,登錄認證接口路由是/currency/login。而此時我請求的認證接口是/main/currency/login,端口是8765,我們在文章開頭就已說明,gateway網關在yml文件中配置auth代理為auth/,和這里的main是同一個道理。
如果此時我們再去請求項目中其他端口攜帶過期的token試試看效果:
我們登陸認證返回的token是:
eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEsInN0YSI6MTU1NjcxODU2Nzc3NCwiY29tcGFueUlkIjowLCJkZXB0SWQiOjEwMCwidXNlclR5cGUiOm51bGwsImV4cCI6MTU1ODAxNDU2Nzc3NH0.6oXx4Wk-eWHSWTHyJHmoiGowKnAmBdCHIRCzsMq5XlA;
攜帶的其他過期的token是:
eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEsInN0YSI6MTU1NjQ1NjUwNzIwMiwiY29tcGFueUlkIjowLCJkZXB0SWQiOjEwMCwidXNlclR5cGUiOm51bGwsImV4cCI6MTU1Nzc1MjUwNzIwMn0._yF2TeaR4MTmF-Re9QciMZOeRKBOQmfvi3o4hWeGSMU
再攜帶錯誤的token試試看:
登陸認證返回的token是:
eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEsInN0YSI6MTU1NjcxODU2Nzc3NCwiY29tcGFueUlkIjowLCJkZXB0SWQiOjEwMCwidXNlclR5cGUiOm51bGwsImV4cCI6MTU1ODAxNDU2Nzc3NH0.6oXx4Wk-eWHSWTHyJHmoiGowKnAmBdCHIRCzsMq5XlA;
攜帶錯誤的token是:
eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEsInN0YSI6MTU1NjcxODU2Nzc3NCwiY29tcGFueUlkIjowLCJkZXB0SWQiOjEwMCwidXNlclR5cGUiOm51bGwsImV4cCI6MTU1ODAxNDU2Nzc3NH0.6oXx4Wk-eWHSWTHyJHmoiGowKnAmBdCHIRCzsMq5XlD
攜帶正確的token:
到這里我么你的整個SpringCloud gateway網關+JWT安全認證就結束啦,非常抱歉,由於項目保密性不能為大家提供項目源碼。但是整個過程我已經寫的非常詳細,也不希望大家做伸手黨,如果有各種疑問歡迎留言,我可以幫大家一一解決。