An implementation of micro service authentication

This article mainly introduces the implementation of authentication based on the combination of client token and gateway API.

In this way, the request header or parameter of each request must carry a token, and all requests must pass through the gateway, which effectively hides the micro service.

At present, there are common ways to generate tokens, one is to generate tokens through MD5, and the other is JWT: base64 encoded information + signature.

If you select MD5 to generate a token, use redis as the cache for judgment, take the token as the key, the login information as the value, and set the key expiration time as the login aging.

Select JWT, set the expiration time and login information for authentication.

Let's start with the first:

Generate token in MD5 mode

public class TokenGeneratorUtils {

    public static String generateValue() {
        return generateValue(UUID.randomUUID().toString());
    }

    private static final char[] HEX_CODE = "0123456789abcdef".toCharArray();

    public static String toHexString(byte[] data) {
        if (data == null) {
            return null;
        }
        StringBuilder r = new StringBuilder(data.length * 2);
        for (byte b : data) {
            r.append(HEX_CODE[(b >> 4) & 0xF]);
            r.append(HEX_CODE[(b & 0xF)]);
        }
        return r.toString();
    }

    public static String generateValue(String param) {
        try {
            MessageDigest algorithm = MessageDigest.getInstance("MD5");
            algorithm.reset();
            algorithm.update(param.getBytes());
            byte[] messageDigest = algorithm.digest();
            return toHexString(messageDigest);
        } catch (Exception e) {
            throw new RenException("token invalid", e);
        }
    }
}

Save in redis when logging in

private String refreshToken(SmsSysUserDTO userDTO) {
        //User token
        String token;

        //current time 
        Date now = new Date();
        //Expiration time
        Date expireTime = new Date(now.getTime() + EXPIRE * 1000);

        //Judge whether the token has expired
        if (userDTO.getExpireTime() == null || userDTO.getExpireTime().getTime() < System.currentTimeMillis()) {
            //When the token expires, regenerate the token
            token = TokenGeneratorUtils.generateValue();
        } else {
            token = userDTO.getToken();
        }
        userDTO.setToken(token);
        userDTO.setExpireTime(expireTime);
        userDTO.setLastLoginTime(new Date());
        smsSysUserService.saveOrUpdateDTO(userDTO);
        UserShareDTO userShareDTO = new UserShareDTO(userDTO.getId(), userDTO.getUsername(), token, SmsConstant.SmsSystem.SERVER.getValue());
        // Cache token
        redisUtils.set(token, userShareDTO, EXPIRE);
        return token;
    }

JWT tool class

My tool class is mainly used to verify authorization scenarios.

Specific business scenario: the third-party system jumps to my system and displays the information on the home page of my system,

Specific implementation scheme:

1. A key is agreed with the third party, and AES symmetric encryption is used to request my interface through post for the information and a uuid

2. I decrypt it here. If the decryption is successful, the information is stored in the database, the uuid is encapsulated, and the JWT returns a signId

3. The third party substitutes the signId and uuid into the url. The front end parses the url and requests my authentication interface. If authentication passes, the information in the database is pulled for display

The specific JWT tools are as follows:

public class JwtTokenUtils implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * User name
     */
    private static final String USERNAME = Claims.SUBJECT;

    public static final String USERNAME_VALUE = "ABCabc-username";

    /**
     * secret key
     */
    private static final String SECRET = "TESTs-jwt";

    /**
     * Creation time
     */
    private static final String CREATED = "created";
    /**
     * Permission list
     */
    private static final String AUTHORITIES = "authorities";

    /**
     * Valid for 12 hours
     */
    public static final long EXPIRE_TIME = 12 * 60 * 60 * 1000;


    /**
     * Valid for 5 minutes
     */
    public static final long EXPIRE = 5 * 60 * 1000;


    /**
     * Generate token
     *
     * @param uuid
     * @param expire
     * @return
     */
    public static String generateToken(String uuid, Long expire) {
        Map<String, Object> claims = new HashMap<>(3);
        claims.put(USERNAME, USERNAME_VALUE);
        claims.put(CREATED, new Date());
        // Here you can put some authentication information
        claims.put(AUTHORITIES, uuid);
        return generateToken(claims, expire);
    }


    /**
     * Get data declaration from token
     *
     * @param token token
     * @return Data declaration
     */
    private static Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }


    /**
     * Determine whether the token has expired
     *
     * @param token token
     * @return Expired
     */
    public static Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            throw new RenException("Your token is invalid or expired");
        }
    }

    /**
     * Verify the token according to uuid
     *
     * @param token
     * @param uuid
     */
    public static void verifyToken(String token, String uuid) {
        Claims claims = getClaimsFromToken(token);
        if (claims == null) {
            throw new RenException("Your token is invalid or expired");
        }
        if (isTokenExpired(token)) {
            throw new RenException("Your token is invalid or expired");
        }
        String username = claims.getSubject();
        if (username == null) {
            throw new RenException("Your token is invalid or expired");
        }
        Object authors = claims.get(AUTHORITIES);
        if (authors instanceof String) {
            String str = authors.toString();
            if (!str.equals(uuid)) {
                throw new RenException("Your token is invalid or expired");
            }
        } else {
            throw new RenException("Your token is invalid or expired");
        }
    }


    /**
     * Generate token from data claim
     *
     * @param claims Data declaration
     * @param expire Expiration time
     * @return
     */
    private static String generateToken(Map<String, Object> claims, long expire) {
        Date expirationDate = new Date(System.currentTimeMillis() + expire);
        return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact();
    }

}

Gateway authentication

Take the token generated by MD5 and saved in redis as an example:

Specific steps:

1. Filter white list

2. Take the token as the key from the request header and request redis

3. Packaging parameters

4. AES encryption sending request

5. Encapsulate the current login user object, and obtain the current login user object by parsing the parameters in the request header

@Component
@RefreshScope
public class GatewayFilter implements GlobalFilter, Ordered {

    @Autowired
    private RedisUtils redisUtils;

    /**
     * White list
     */
    @Value(value = "${white.urls}")
    private Set<String> whiteUrls;

    /**
     * Interceptor
     *
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();
        // Filter white list
        if (null != whiteUrls.stream().filter(path::contains).findAny().orElse(null)) {
             return chain.filter(exchange);
        }
        // Get token from header
        String token = exchange.getRequest().getHeaders().getFirst("token");
        // Access is blocked without a token or unauthorized
        if (StringUtils.isBlank(token)) {
            return TokenException(exchange);
        }
        // Cache token
        Map tokenMap = (Map) redisUtils.get(token);
        if (MapUtils.isEmpty(tokenMap)) {
            return TokenException(exchange);
        }
        if (MapUtils.isNotEmpty(tokenMap)) {
            // Encapsulate user
            UserShareDTO user = new UserShareDTO();
            user.setId(MapUtils.getLong(tokenMap, "id"));
            user.setToken(MapUtils.getString(tokenMap, "token"));
            user.setSystem(MapUtils.getString(tokenMap, "system"));
            user.setUsername(MapUtils.getString(tokenMap, "username"));
            if (StringUtils.isBlank(user.getSystem()) || StringUtils.isBlank(user.getToken())
                    || StringUtils.isBlank(user.getUsername())) {
                return TokenException(exchange);
            }
            return buildRequest(exchange, chain, JSON.toJSONString(user));
        }
         return chain.filter(exchange);
    }

    /**
     * The authentication information is encrypted with AES to send the request
     *
     * @param exchange
     * @param chain
     * @param auth
     * @return
     */
    public Mono<Void> buildRequest(ServerWebExchange exchange, GatewayFilterChain chain, String auth) {
        String userAESStr = AESUtils.AESencode("abcdssdfsdfsafq", auth);
        ServerHttpRequest request = exchange.getRequest().mutate()
                .header("userInfo", userAESStr)
                .build();
        return chain.filter(exchange.mutate().request(request).build());
    }

    /**
     * token Invalid or expired
     *
     * @param exchange
     * @return
     */
    private Mono<Void> TokenException(ServerWebExchange exchange) {
        //1. Get the response object
        ServerHttpResponse response = exchange.getResponse();
        //2. Set response header type
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        response.setStatusCode(HttpStatus.OK);
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("msg", "Your token is invalid or expired");
        jsonObject.put("code", HttpStatus.UNAUTHORIZED.value());
        jsonObject.put("data", false);
        DataBuffer wrap = response.bufferFactory().wrap(jsonObject.toJSONString().getBytes());
        return response.writeWith(Flux.just(wrap));
    }

    /**
     * Interceptor sequence
     *
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

Currently logged in user object tool

@Slf4j
public class ShareUtils {

    public static UserShareDTO user;

    public static UserShareDTO getUser() {
        updateInfo();
        return user;
    }

    public static synchronized void updateInfo() {
        try {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String userInfoStr = request.getHeader("userInfo");
            String userInfo = AESUtils.AESdecode("abcdssdfsdfsafq", userInfoStr);
            if (StringUtils.isNotBlank(userInfo)) {
                user = JSON.parseObject(userInfo, UserShareDTO.class);
            } else {
                user = new UserShareDTO();
            }
            log.info("ShareUtils UserShareDTO ", user);
        } catch (Exception e) {
            log.error("ShareUtils HttpServletRequest request is null: " + e.getMessage());
            user = new UserShareDTO();
        }
    }
}

If this authentication method is used, the feign interceptor needs to be rewritten when feign is called, and the request header will be lost

For specific solutions, please refer to my This article

Personally, I think this authentication method is more flexible. After the gateway redis authentication, the client, server, internal request, external request, etc. can be distinguished in the form of parameters. At the same time, it can be combined with mybaitis plus for data isolation.

Would you please comment on the disadvantages of this method or the areas that need to be improved!

Keywords: Java Spring Cloud Microservices

Added by daglasen on Tue, 04 Jan 2022 17:02:44 +0200