• JAVA++:JWT 实现统一的认证授权


    概述:

      以下代码仅供参考:在实际开发中用什么的都有(都存在优点与缺点)提供一种设计理念思想;

                                            根据实际场景 可以设计出自己的一套认证规则

    主要类说明:

      JwtCheck.java --> JwtToken校验注解

      JwtCheckAop.java --> JwtToken校验注解AOP

      JwtTokenFilter.java --> (基于GateWay)自定义JWT 过滤器

      AuthController.java -->认证测试接口

      application.yml -->配置文件

      JwtUtil.java --> jwt工具类

    加入 jjwt 依赖:

    jjwt 是一个Java对jwt的支持库,我们使用这个库来创建、解码token

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.0</version>
    </dependency>

    JwtUtil :工具类

    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.JwtBuilder;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import org.apache.commons.codec.binary.Base64;
    
    import javax.crypto.SecretKey;
    import javax.crypto.spec.SecretKeySpec;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 描述: jwt 工具类
     */
    public class JwtUtil {
    
        //密钥 -- 根据实际项目,这里可以做成配置
        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<String, Object> claims = new HashMap<>();
            claims.put("uid", "123456");
            claims.put("user_name", "admin");
            claims.put("nick_name", "X-rapido");
    
            // 生成签名的时候使用的秘钥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;
        }
    
    }

    基于 gateway 编写的过滤器 JwtTokenFilter :

    getOrder方法中的返回值的数据越小,过滤器的级别越高

    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.lzx.gateway.dto.ReturnData;
    import com.lzx.gateway.jwt.JwtUtil;
    import io.jsonwebtoken.ExpiredJwtException;
    import lombok.Getter;
    import lombok.Setter;
    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 过滤器
     */
    @Component
    //读取 yml 文件下的 org.my.jwt
    @ConfigurationProperties("org.my.jwt")
    @Setter
    @Getter
    @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<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            String url = exchange.getRequest().getURI().getPath();
    
            //跳过不需要验证的路径
            if(null != skipAuthUrls&&Arrays.asList(skipAuthUrls).contains(url)){
                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<Void> authErro(ServerHttpResponse resp,String mess) {
            resp.setStatusCode(HttpStatus.UNAUTHORIZED);
            resp.getHeaders().add("Content-Type","application/json;charset=UTF-8");
            ReturnData<String> returnData = new ReturnData<>(org.apache.http.HttpStatus.SC_UNAUTHORIZED, mess, mess);
            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;
        }
    }

    添加认证的api接口:

    这里为了方便测试,认证的接口写在了网关的项目中,实际生产可以把接口设计在专门的认证服务中

     /**
     * 登陆认证接口
     * @param userDTO
     * @return
     */
    @PostMapping("/login")
    public ReturnData<String> login(@RequestBody UserDTO userDTO) throws Exception {
        ArrayList<String> roleIdList = new ArrayList<>(1);
        roleIdList.add("role_test_1");
        JwtModel jwtModel = new JwtModel("test", roleIdList);
        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("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 1000L);
                break;
            }
            case "m" :{
                //分钟
                jwt = JwtUtil.createJWT("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 60L * 1000L);
                break;
            }
            case "h" :{
                //小时
                jwt = JwtUtil.createJWT("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 60L * 60L * 1000L);
                break;
            }
            case "d" :{
                //小时
                jwt = JwtUtil.createJWT("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 24L * 60L * 60L * 1000L);
                break;
            }
        }
        return new ReturnData<String>(HttpStatus.SC_OK,"认证成功",jwt);
    }

    yml配置文件:

     这里读取了配置中心的文件,大家可以根据自己的需求更改

    ###################################
    #服务启动端口的配置
    ###################################
    server:
      port: ${server-port}
    
    ###############################################################
    # eureka 的相关配置
    # 如果不需要 结合eureka 使用,可以不要这一段配置
    ###############################################################
    eureka:
      client:
        fetch-registry: true
        register-with-eureka: ${register-with-eureka}     # 是否注册到eureka
        service-url:
          defaultZone: ${service-url-defaultZone}
      instance:
        prefer-ip-address: false
        hostname: ${instance-hostname}
    
    
    spring:
      cloud:
    #################################
    #   gateway相关配置
    #################################
        gateway:
    #    路由定义
          routes:
    
          - id: baidu
            uri: https://www.baidu.com
            predicates:
            - Path=/baidu/**
            filters:
            - StripPrefix=1
    
          - id: eureka-manage
            uri: lb://eureka-manage
            predicates:
            - Path=/eureka-manage/**
            filters:
            - StripPrefix=1
    
          - id: sina
            uri: https://www.sina.com.cn/
            predicates:
            - Path=/sina/**
            filters:
            - StripPrefix=1
    
    org:
      my:
        jwt:
          #跳过认证的路由
          skip-auth-urls:
          - /baidu
          ############################################
          #   有效时长
          #     单位:d:天、h:小时、m:分钟、s:秒
          ###########################################
          effective-time: 1m

    测试:

    直接不带认证信息访问一个需要认证的路由:访问一个新浪得路由,提示需要认证

    http://localhost:30006/sina

     

     调用认证api获取token

     把token加入请求头,再次访问新浪得路由,可以通过认证

     

     尝试token过期后访问,在application.yml中我配置了token一分钟后过期,一分钟后我再次携带token访问新浪得路由,提示认证过期。

    进阶:制作JwtToken校验注解:

    import java.lang.annotation.*;
    
    /**
     * 描述: jwt检查注解
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface JwtCheck {
    
        String value() default "";
    }

    定义注解得AOP:

    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.lzx.gateway.dto.ReturnData;
    import com.lzx.gateway.jwt.JwtUtil;
    import io.jsonwebtoken.ExpiredJwtException;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang.ArrayUtils;
    import org.apache.commons.lang.StringUtils;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.web.bind.annotation.RequestHeader;
    
    import java.lang.annotation.Annotation;
    import java.lang.reflect.Method;
    
    /**
     * 描述:添加了 JwtCheck 注解 的Aop
     */
    @Component
    @Aspect
    @Slf4j
    public class JwtCheckAop {
    
        @Autowired
        private ObjectMapper objectMapper;
    
        @Pointcut("@annotation(com.lzx.gateway.annotation.JwtCheck)")
        private void apiAop(){
    
        }
    
        /**
         * 方法执行前的aop
         * @param point
         * @return
         * @throws Throwable
         */
        @Around("apiAop()")
        public Object aroundApi(ProceedingJoinPoint point) throws Throwable {
            MethodSignature signature = (MethodSignature) point.getSignature();
            Method method = signature.getMethod();
            //获取参数上得所有注解
            Annotation[][] parameterAnnotationArray = method.getParameterAnnotations();
            Object[] args = point.getArgs();
    
            String token = null;
    
            /*
                a -> start
                这个代码片得逻辑:找出有 @RequestHeader("Authorization") 的参数,赋值给 token变量
             */
            for(Annotation[]  annotations : parameterAnnotationArray){
                for(Annotation a:annotations){
                    if(a instanceof RequestHeader){
                        RequestHeader requestHeader =  (RequestHeader)a;
                        if("Authorization".equals(requestHeader.value())){
                            token = (String) args[ArrayUtils.indexOf(parameterAnnotationArray,annotations)];
                        }
                    }
                }
            }
            /*
                a -> end
             */
    
            if(StringUtils.isBlank(token)){
                //没有token
                return authErro("请登陆");
            }else{
                //有token
                try {
                    JwtUtil.checkToken(token,objectMapper);
                    Object proceed = point.proceed();
                    return proceed;
                }catch (ExpiredJwtException e){
                    log.error(e.getMessage(),e);
                    if(e.getMessage().contains("Allowed clock skew")){
                        return authErro("认证过期");
                    }else{
                        return authErro("认证失败");
                    }
                }catch (Exception e) {
                    log.error(e.getMessage(),e);
                    return authErro("认证失败");
                }
            }
        }
    
        /**
         * 认证错误输出
         * @param mess 错误信息
         * @return
         */
        private Object authErro(String mess) {
            ReturnData<String> returnData = new ReturnData<>(org.apache.http.HttpStatus.SC_UNAUTHORIZED, mess, mess);
            return returnData;
        }
    
    }

    注解的使用方法:

    直接在方法上使用 @JwtCheck

        /**
         * jwt 检查注解测试 测试
         * @return
         */
        @GetMapping("/testJwtCheck")
        @JwtCheck
        public ReturnData<String> testJwtCheck(@RequestHeader("Authorization")String token,@RequestParam("name")@Valid String name){
    
            return new ReturnData<String>(HttpStatus.SC_OK,"请求成功咯","请求成功咯"+name);
    
        }

     没有十全十美的技术,都会有 缺点和优点,根据实际情况而定。

  • 相关阅读:
    Rewrite the master page form action attribute in asp.net 2.0
    Using Output Cache
    An invalid character was found in text content!
    Microsoft Football Scoreboard
    A typical ASP.NET 2.0 Configuration Settings
    Visual Studio 2005 Web Application Projects Released!
    Test Driven Development with Visual Studio 2005 Team System
    How to pass a parameter to HyperLink in GridView/DataList
    MS的一个BUG折腾我几个小时!
    Create builtin tables in your own database to handle exceptions, Part 2
  • 原文地址:https://www.cnblogs.com/codingmode/p/15323721.html
Copyright © 2020-2023  润新知