記一次token安全認證的實踐

閱讀此文前請先閱讀上一篇SpringBoot整合JWT實現用戶認證瞭解JWT前端

背景介紹:

因項目需求,有PC端 APP端和小程序端,但登錄接口是同一個,然而微服務也沒法使用傳統的session解決用戶登陸問題(注意這裏是傳統的session不是spring session),使用戶信息在其餘服務共享。java

如此一來就想到了token安全認證,而JWT生成token能夠包含用戶信息,也就果斷選擇了JWT做爲SpringCloud gateway網關的token校驗工具,這樣,咱們即可以直接解析token獲取用戶信息了。ajax

具體實現思路:

  1. 讓JWT在其餘全部服務能夠共同使用,父工程須要引入JWT jar。避免在其餘服務重複引入。redis

  2. 如何使用JWT生成token。算法

  3. 如何解析token。spring

  4. 如何讓網關攔截用戶請求校驗token。json

  5. 如何避免首次登陸被網關攔截。小程序

代碼實現:

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。

有圖有真相

記一次token安全認證的實踐

特別提示:個人auth工程端口是8766,登陸認證接口路由是/currency/login。而此時我請求的認證接口是/main/currency/login,端口是8765,咱們在文章開頭就已說明,gateway網關在yml文件中配置auth代理爲auth/,和這裏的main是同一個道理。

若是此時咱們再去請求項目中其餘端口攜帶過時的token試試看效果:

記一次token安全認證的實踐

咱們登錄認證返回的token是:

eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEsInN0YSI6MTU1NjcxODU2Nzc3NCwiY29tcGFueUlkIjowLCJkZXB0SWQiOjEwMCwidXNlclR5cGUiOm51bGwsImV4cCI6MTU1ODAxNDU2Nzc3NH0.6oXx4Wk-eWHSWTHyJHmoiGowKnAmBdCHIRCzsMq5XlA;

攜帶的其餘過時的token是:

eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEsInN0YSI6MTU1NjQ1NjUwNzIwMiwiY29tcGFueUlkIjowLCJkZXB0SWQiOjEwMCwidXNlclR5cGUiOm51bGwsImV4cCI6MTU1Nzc1MjUwNzIwMn0._yF2TeaR4MTmF-Re9QciMZOeRKBOQmfvi3o4hWeGSMU

再攜帶錯誤的token試試看:

記一次token安全認證的實踐

登錄認證返回的token是:

eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEsInN0YSI6MTU1NjcxODU2Nzc3NCwiY29tcGFueUlkIjowLCJkZXB0SWQiOjEwMCwidXNlclR5cGUiOm51bGwsImV4cCI6MTU1ODAxNDU2Nzc3NH0.6oXx4Wk-eWHSWTHyJHmoiGowKnAmBdCHIRCzsMq5XlA;

攜帶錯誤的token是:

eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEsInN0YSI6MTU1NjcxODU2Nzc3NCwiY29tcGFueUlkIjowLCJkZXB0SWQiOjEwMCwidXNlclR5cGUiOm51bGwsImV4cCI6MTU1ODAxNDU2Nzc3NH0.6oXx4Wk-eWHSWTHyJHmoiGowKnAmBdCHIRCzsMq5XlD

攜帶正確的token:

記一次token安全認證的實踐

到這裏我麼你的整個SpringCloud gateway網關+JWT安全認證就結束啦,很是抱歉,因爲項目保密性不能爲你們提供項目源碼。可是整個過程我已經寫的很是詳細,也不但願你們作伸手黨,若是有各類疑問歡迎留言,我能夠幫你們一一解決。

相關文章
相關標籤/搜索