基于微服务网关gateway的token认证机制(完整版)

去年写了一个基于前端vue的token认证。现在把后端的也补上。以前在网上看到过一篇,基于此篇改造了一下。

只需要6步即可。直接复制粘贴即可直接使用!

需引入如下包:

         
            org.projectlombok
            lombok
        
        
            io.jsonwebtoken
            jjwt
            0.9.0
            compile
        

 

1.过滤器。实现url拦截,并判断是否需要权限认证。

JwtTokenFilter.java

package com.gateway.auth;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gateway.tool.ReturnData;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;

/**
 * 描述: JwtToken 过滤器
 *
 * @Auther: qiemengyan
 */
@Component
//读取 yml 文件下的 org.my.jwt
@ConfigurationProperties("org.my.jwt")
@Data
@Slf4j
public class JwtTokenFilter implements GlobalFilter,Ordered {

    private String[] skipAuthUrls;

    private ObjectMapper objectMapper;

    public JwtTokenFilter(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    /**
     * 过滤器
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String url = exchange.getRequest().getURI().getPath();

        //跳过不需要验证的路径
        /*if(null != skipAuthUrls&&Arrays.asList(skipAuthUrls).contains(url)){
            return chain.filter(exchange);
        }*/
        if(Arrays.asList(skipAuthUrls).contains("/test")){
            return chain.filter(exchange);
        }

        //验证token
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        ServerHttpResponse resp = exchange.getResponse();
        if(StringUtils.isBlank(token)){
            //没有token
            return authErro(resp,"目前您没有无权限访问,请提交权限码");
        }else{
            //有token
            try {
                JwtUtil.checkToken(token,objectMapper);
                return chain.filter(exchange);
            }catch (ExpiredJwtException e){
                log.error(e.getMessage(),e);
                if(e.getMessage().contains("Allowed clock skew")){
                    return authErro(resp,"您的认证权限已过期,请重新进行认证");
                }else{
                    return authErro(resp,"认证失败");
                }
            }catch (Exception e) {
                log.error(e.getMessage(),e);
                return authErro(resp,"认证失败");
            }
        }
    }

    /**
     * 认证错误输出
     * @param resp 响应对象
     * @param mess 错误信息
     * @return
     */
    private Mono authErro(ServerHttpResponse resp,String mess) {
        resp.setStatusCode(HttpStatus.UNAUTHORIZED);
        resp.getHeaders().add("Content-Type","application/json;charset=UTF-8");
        ReturnData returnData = new ReturnData<>(org.apache.http.HttpStatus.SC_UNAUTHORIZED, mess,null);
        String returnStr = "";
        try {
            returnStr = objectMapper.writeValueAsString(returnData);
        } catch (JsonProcessingException e) {
            log.error(e.getMessage(),e);
        }
        DataBuffer buffer = resp.bufferFactory().wrap(returnStr.getBytes(StandardCharsets.UTF_8));
        return resp.writeWith(Flux.just(buffer));
    }

    @Override
    public int getOrder() {
        return -100;
    }
}

2.创建token。

JwtCreateToken.java

package com.gateway.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.gateway.model.Account;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 描述: jwt 工具类
 *
 * @Auther: qiemengyan
 */
@Slf4j
public class JwtCreateToken {

    public static final String KEY = "022bdc63c3c5a45879ee6581508b9d03adfec4a4658c0ab3d722e50c91a351c42c231cf43bb8f86998202bd301ec52239a74fc0c9a9aeccce604743367c9646b";

    /**
     * 由字符串生成加密key
     *
     * @return
     */
    public static SecretKey generalKey(){
        byte[] encodedKey = Base64.decodeBase64(KEY);
        SecretKeySpec key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");

        return key;
    }

    /**
     * 创建jwt
     * @param id
     * @param issuer
     * @param subject
     * @param ttlMillis
     * @return
     * @throws Exception
     */
    public static String createJWT(String id, String issuer, String subject, long ttlMillis) throws Exception {

        // 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT的时间
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        // 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
        // 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
        Map claims = new HashMap<>();
        claims.put("uid", "911210");
        claims.put("dev_name", "qmy");
        claims.put("com_name", "xxyfeb");

        // 生成签名的时候使用的秘钥secret,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。
        // 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
        SecretKey key = generalKey();

        // 下面就是在为payload添加各种标准声明和私有声明了
        JwtBuilder builder = Jwts.builder() // 这里其实就是new一个JwtBuilder,设置jwt的body
                .setClaims(claims)          // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setId(id)                  // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setIssuedAt(now)           // iat: jwt的签发时间
                .setIssuer(issuer)          // issuer:jwt签发人
                .setSubject(subject)        // sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
                .signWith(signatureAlgorithm, key); // 设置签名使用的签名算法和签名使用的秘钥

        // 设置过期时间
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }
        return builder.compact();
    }

    /**
     * 解密jwt
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey key = generalKey();  //签名秘钥,和生成的签名的秘钥一模一样
        Claims claims = Jwts.parser()  //得到DefaultJwtParser
                .setSigningKey(key)                 //设置签名的秘钥
                .parseClaimsJws(jwt).getBody();     //设置需要解析的jwt
        return claims;
    }

    /**
     * 检查token
     * @return
     */
    public static boolean checkToken(String jwtToken, ObjectMapper objectMapper) throws Exception {
        //TODO 根据自己的业务修改
        Claims claims = JwtUtil.parseJWT(jwtToken);
        String subject = claims.getSubject();
        Account jwtModel = objectMapper.readValue(subject, Account.class);
        /*
            TODO 对jwt里面的用户信息做判断
            根据自己的业务编写
         */

        /*
            获取token的过期时间,和当前时间作比较,如果小于当前时间,则token过期
         */
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
        Date expiration = claims.getExpiration();
        log.info("======== token的过期时间:"+df.format(expiration));
        return true;
    }

}

3.实体,收发参数

Account.java

package com.gateway.model;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.Date;

/**
 * 描述: 账户model
 *
 * @Auther: qiemengyan
 */
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Account {

    private int id;
    private String username;
    private String password;
    private String orgcode;
    private String name;
    private String phone;
    @JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss", timezone = "GMT+8")
    private Date createtime;

}

4.认证接口。用于前端请求获取token的接口

AuthController.java

package com.gateway.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.gateway.model.Account;
import com.gateway.auth.JwtUtil;
import com.gateway.tool.ReturnData;
import com.gateway.service.AuthService;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

/**
 * 描述: 认证接口
 *
 * @Auther: qiemengyan
 */
@RestController
@Slf4j
@RequestMapping("/auth")
public class AuthController {

    private ObjectMapper objectMapper;

    @Value("${org.my.jwt.effective-time}")
    private String effectiveTime;
    @Autowired
    private AuthService authService;


    public AuthController(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    /**
     * 登陆认证接口
     * @param account
     * @return
     */
    @PostMapping("/login")
    public ReturnData login(@RequestBody Account account) throws Exception {
        Account ac=this.authService.getJwtUser(account);//自定义验证前端传过来的用户名密码是否正确,使用者自行修改。
        if(ac!=null) {
            int effectivTimeInt = Integer.valueOf(effectiveTime.substring(0, effectiveTime.length() - 1));
            String effectivTimeUnit = effectiveTime.substring(effectiveTime.length() - 1, effectiveTime.length());
            String jwt = null;
            switch (effectivTimeUnit) {
                case "s": {
                    //秒
                    jwt = JwtUtil.createJWT("qmy", "0339", objectMapper.writeValueAsString(account), effectivTimeInt * 1000L);
                    break;
                }
                case "m": {
                    //分钟
                    jwt = JwtUtil.createJWT("qmy", "0339", objectMapper.writeValueAsString(account), effectivTimeInt * 60L * 1000L);
                    break;
                }
                case "h": {
                    //小时
                    jwt = JwtUtil.createJWT("qmy", "0339", objectMapper.writeValueAsString(account), effectivTimeInt * 60L * 60L * 1000L);
                    break;
                }
                case "d": {
                    //小时
                    jwt = JwtUtil.createJWT("qmy", "0339", objectMapper.writeValueAsString(account), effectivTimeInt * 24L * 60L * 60L * 1000L);
                    break;
                }
            }
            return new ReturnData(HttpStatus.SC_OK, "认证成功", jwt);
        }else{
            return new ReturnData(401, "认证失败,用户名或密码错误,请重新输入",null);
        }
    }

    /**
     * 为授权提示
     */
    @GetMapping("/unauthorized")
    public ReturnData unauthorized(){
        return new ReturnData (HttpStatus.SC_UNAUTHORIZED,"未认证,请重新登陆",null);
    }


}

5.返回结果工具

ReturnData.java

package com.gateway.tool;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

/**
 * 描述:
 *
 * @Auther: qiemengyan
 */
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class ReturnData {

    private int code;

    private String mass;

    private T data;

}

6.application.yml设置常量参数。

application.yml

org:
  my:
    jwt:
      #跳过认证的路由
      skip-auth-urls:
        - /ceshi
        - /test
      ############################################
      #   有效时长
      #     单位:d:天、h:小时、m:分钟、s:秒
      ###########################################
      effective-time: 7d

至此,基于微服务网关gateway的token认证搭建完整。

你可能感兴趣的:(SpringCloud)