Spring Cloud Gateway + JWT 实现统一的认证授权

ゝ一纸荒年。 2023-01-06 01:37 322阅读 0赞

项目结构说明

主要类说明:
JwtCheck.java —> JwtToken校验注解
JwtCheckAop.java —> JwtToken校验注解AOP
JwtTokenFilter.java —>自定义JWT 过滤器
AuthController.java —>认证测试接口
application.yml —>配置文件
JwtUtil.java —> jwt工具类

  1. .gitignore
  2. build.sh
  3. Dockerfile
  4. mvnw
  5. mvnw.cmd
  6. my-gateway-deployment.yaml
  7. my-gateway-service.yaml
  8. pom.xml
  9. README.md
  10. ├─.idea
  11. └─libraries
  12. ├─.mvn
  13. └─src
  14. ├─main
  15. ├─java
  16. └─com
  17. └─lzx
  18. └─gateway
  19. MyGatewayApplication.java
  20. ├─annotation
  21. ExecuteTime.java
  22. JwtCheck.java
  23. ├─aop
  24. JwtCheckAop.java
  25. ├─auth
  26. JwtTokenFilter.java
  27. ├─config
  28. ├─controller
  29. AuthController.java
  30. ├─dto
  31. ReturnData.java
  32. UserDTO.java
  33. └─jwt
  34. JwtModel.java
  35. JwtUtil.java
  36. └─resources
  37. application.yml
  38. bootstrap.yml
  39. └─test
  40. └─java
  41. └─com
  42. └─lzx
  43. └─gateway
  44. └─demo
  45. MyGatewayApplicationTests.java

一、项目添加 Spring Cloud Gateway 依赖

二、加入jjwt依赖

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

  1. <dependency>
  2. <groupId>io.jsonwebtoken</groupId>
  3. <artifactId>jjwt</artifactId>
  4. <version>0.9.0</version>
  5. </dependency>

核心方法

创建jwt token的方法

  1. /**
  2. * 创建jwt
  3. * @param id
  4. * @param issuer
  5. * @param subject
  6. * @param ttlMillis
  7. * @return
  8. * @throws Exception
  9. */
  10. public static String createJWT(String id, String issuer, String subject, long ttlMillis) throws Exception {
  11. // 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
  12. SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
  13. // 生成JWT的时间
  14. long nowMillis = System.currentTimeMillis();
  15. Date now = new Date(nowMillis);
  16. // 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
  17. // 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
  18. Map<String, Object> claims = new HashMap<>();
  19. claims.put("uid", "123456");
  20. claims.put("user_name", "admin");
  21. claims.put("nick_name", "X-rapido");
  22. // 生成签名的时候使用的秘钥secret,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。
  23. // 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
  24. SecretKey key = generalKey();
  25. // 下面就是在为payload添加各种标准声明和私有声明了
  26. JwtBuilder builder = Jwts.builder() // 这里其实就是new一个JwtBuilder,设置jwt的body
  27. .setClaims(claims) // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
  28. .setId(id) // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
  29. .setIssuedAt(now) // iat: jwt的签发时间
  30. .setIssuer(issuer) // issuer:jwt签发人
  31. .setSubject(subject) // sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
  32. .signWith(signatureAlgorithm, key); // 设置签名使用的签名算法和签名使用的秘钥
  33. // 设置过期时间
  34. if (ttlMillis >= 0) {
  35. long expMillis = nowMillis + ttlMillis;
  36. Date exp = new Date(expMillis);
  37. builder.setExpiration(exp);
  38. }
  39. return builder.compact();
  40. }

解码jwt token的方法

  1. /**
  2. * 解密jwt
  3. *
  4. * @param jwt
  5. * @return
  6. * @throws Exception
  7. */
  8. public static Claims parseJWT(String jwt) throws Exception {
  9. SecretKey key = generalKey(); //签名秘钥,和生成的签名的秘钥一模一样
  10. Claims claims = Jwts.parser() //得到DefaultJwtParser
  11. .setSigningKey(key) //设置签名的秘钥
  12. .parseClaimsJws(jwt).getBody(); //设置需要解析的jwt
  13. return claims;
  14. }

最后贴出来一个JWT 的工具类:包含了创建和解码的工具

  1. package com.lzx.gateway.jwt;
  2. import io.jsonwebtoken.Claims;
  3. import io.jsonwebtoken.JwtBuilder;
  4. import io.jsonwebtoken.Jwts;
  5. import io.jsonwebtoken.SignatureAlgorithm;
  6. import org.apache.commons.codec.binary.Base64;
  7. import javax.crypto.SecretKey;
  8. import javax.crypto.spec.SecretKeySpec;
  9. import java.util.Date;
  10. import java.util.HashMap;
  11. import java.util.Map;
  12. /**
  13. * 描述: jwt 工具类
  14. *
  15. * @Auther: lzx
  16. * @Date: 2019/7/9 17:50
  17. */
  18. public class JwtUtil {
  19. //密钥 -- 根据实际项目,这里可以做成配置
  20. public static final String KEY = "022bdc63c3c5a45879ee6581508b9d03adfec4a4658c0ab3d722e50c91a351c42c231cf43bb8f86998202bd301ec52239a74fc0c9a9aeccce604743367c9646b";
  21. /**
  22. * 由字符串生成加密key
  23. *
  24. * @return
  25. */
  26. public static SecretKey generalKey(){
  27. byte[] encodedKey = Base64.decodeBase64(KEY);
  28. SecretKeySpec key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
  29. return key;
  30. }
  31. /**
  32. * 创建jwt
  33. * @param id
  34. * @param issuer
  35. * @param subject
  36. * @param ttlMillis
  37. * @return
  38. * @throws Exception
  39. */
  40. public static String createJWT(String id, String issuer, String subject, long ttlMillis) throws Exception {
  41. // 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
  42. SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
  43. // 生成JWT的时间
  44. long nowMillis = System.currentTimeMillis();
  45. Date now = new Date(nowMillis);
  46. // 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
  47. // 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
  48. Map<String, Object> claims = new HashMap<>();
  49. claims.put("uid", "123456");
  50. claims.put("user_name", "admin");
  51. claims.put("nick_name", "X-rapido");
  52. // 生成签名的时候使用的秘钥secret,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。
  53. // 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
  54. SecretKey key = generalKey();
  55. // 下面就是在为payload添加各种标准声明和私有声明了
  56. JwtBuilder builder = Jwts.builder() // 这里其实就是new一个JwtBuilder,设置jwt的body
  57. .setClaims(claims) // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
  58. .setId(id) // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
  59. .setIssuedAt(now) // iat: jwt的签发时间
  60. .setIssuer(issuer) // issuer:jwt签发人
  61. .setSubject(subject) // sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
  62. .signWith(signatureAlgorithm, key); // 设置签名使用的签名算法和签名使用的秘钥
  63. // 设置过期时间
  64. if (ttlMillis >= 0) {
  65. long expMillis = nowMillis + ttlMillis;
  66. Date exp = new Date(expMillis);
  67. builder.setExpiration(exp);
  68. }
  69. return builder.compact();
  70. }
  71. /**
  72. * 解密jwt
  73. *
  74. * @param jwt
  75. * @return
  76. * @throws Exception
  77. */
  78. public static Claims parseJWT(String jwt) throws Exception {
  79. SecretKey key = generalKey(); //签名秘钥,和生成的签名的秘钥一模一样
  80. Claims claims = Jwts.parser() //得到DefaultJwtParser
  81. .setSigningKey(key) //设置签名的秘钥
  82. .parseClaimsJws(jwt).getBody(); //设置需要解析的jwt
  83. return claims;
  84. }
  85. }

三、添加token检查的过滤器

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

  1. package com.lzx.gateway.auth;
  2. import com.fasterxml.jackson.core.JsonProcessingException;
  3. import com.fasterxml.jackson.databind.ObjectMapper;
  4. import com.lzx.gateway.dto.ReturnData;
  5. import com.lzx.gateway.jwt.JwtUtil;
  6. import io.jsonwebtoken.ExpiredJwtException;
  7. import lombok.Getter;
  8. import lombok.Setter;
  9. import lombok.extern.slf4j.Slf4j;
  10. import org.apache.commons.lang.StringUtils;
  11. import org.springframework.boot.context.properties.ConfigurationProperties;
  12. import org.springframework.cloud.gateway.filter.GatewayFilterChain;
  13. import org.springframework.cloud.gateway.filter.GlobalFilter;
  14. import org.springframework.core.Ordered;
  15. import org.springframework.core.io.buffer.DataBuffer;
  16. import org.springframework.http.HttpStatus;
  17. import org.springframework.http.server.reactive.ServerHttpResponse;
  18. import org.springframework.stereotype.Component;
  19. import org.springframework.web.server.ServerWebExchange;
  20. import reactor.core.publisher.Flux;
  21. import reactor.core.publisher.Mono;
  22. import java.nio.charset.StandardCharsets;
  23. import java.util.Arrays;
  24. /**
  25. * 描述: JwtToken 过滤器
  26. *
  27. * @Auther: lzx
  28. * @Date: 2019/7/9 15:49
  29. */
  30. @Component
  31. //读取 yml 文件下的 org.my.jwt
  32. @ConfigurationProperties("org.my.jwt")
  33. @Setter
  34. @Getter
  35. @Slf4j
  36. public class JwtTokenFilter implements GlobalFilter,Ordered {
  37. private String[] skipAuthUrls;
  38. private ObjectMapper objectMapper;
  39. public JwtTokenFilter(ObjectMapper objectMapper) {
  40. this.objectMapper = objectMapper;
  41. }
  42. /**
  43. * 过滤器
  44. * @param exchange
  45. * @param chain
  46. * @return
  47. */
  48. @Override
  49. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  50. String url = exchange.getRequest().getURI().getPath();
  51. //跳过不需要验证的路径
  52. if(null != skipAuthUrls&&Arrays.asList(skipAuthUrls).contains(url)){
  53. return chain.filter(exchange);
  54. }
  55. //获取token
  56. String token = exchange.getRequest().getHeaders().getFirst("Authorization");
  57. ServerHttpResponse resp = exchange.getResponse();
  58. if(StringUtils.isBlank(token)){
  59. //没有token
  60. return authErro(resp,"请登陆");
  61. }else{
  62. //有token
  63. try {
  64. JwtUtil.checkToken(token,objectMapper);
  65. return chain.filter(exchange);
  66. }catch (ExpiredJwtException e){
  67. log.error(e.getMessage(),e);
  68. if(e.getMessage().contains("Allowed clock skew")){
  69. return authErro(resp,"认证过期");
  70. }else{
  71. return authErro(resp,"认证失败");
  72. }
  73. }catch (Exception e) {
  74. log.error(e.getMessage(),e);
  75. return authErro(resp,"认证失败");
  76. }
  77. }
  78. }
  79. /**
  80. * 认证错误输出
  81. * @param resp 响应对象
  82. * @param mess 错误信息
  83. * @return
  84. */
  85. private Mono<Void> authErro(ServerHttpResponse resp,String mess) {
  86. resp.setStatusCode(HttpStatus.UNAUTHORIZED);
  87. resp.getHeaders().add("Content-Type","application/json;charset=UTF-8");
  88. ReturnData<String> returnData = new ReturnData<>(org.apache.http.HttpStatus.SC_UNAUTHORIZED, mess, mess);
  89. String returnStr = "";
  90. try {
  91. returnStr = objectMapper.writeValueAsString(returnData);
  92. } catch (JsonProcessingException e) {
  93. log.error(e.getMessage(),e);
  94. }
  95. DataBuffer buffer = resp.bufferFactory().wrap(returnStr.getBytes(StandardCharsets.UTF_8));
  96. return resp.writeWith(Flux.just(buffer));
  97. }
  98. @Override
  99. public int getOrder() {
  100. return -100;
  101. }
  102. }

四、添加认证的api接口

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

  1. /**
  2. * 登陆认证接口
  3. * @param userDTO
  4. * @return
  5. */
  6. @PostMapping("/login")
  7. public ReturnData<String> login(@RequestBody UserDTO userDTO) throws Exception {
  8. ArrayList<String> roleIdList = new ArrayList<>(1);
  9. roleIdList.add("role_test_1");
  10. JwtModel jwtModel = new JwtModel("test", roleIdList);
  11. int effectivTimeInt = Integer.valueOf(effectiveTime.substring(0,effectiveTime.length()-1));
  12. String effectivTimeUnit = effectiveTime.substring(effectiveTime.length()-1,effectiveTime.length());
  13. String jwt = null;
  14. switch (effectivTimeUnit){
  15. case "s" :{
  16. //秒
  17. jwt = JwtUtil.createJWT("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 1000L);
  18. break;
  19. }
  20. case "m" :{
  21. //分钟
  22. jwt = JwtUtil.createJWT("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 60L * 1000L);
  23. break;
  24. }
  25. case "h" :{
  26. //小时
  27. jwt = JwtUtil.createJWT("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 60L * 60L * 1000L);
  28. break;
  29. }
  30. case "d" :{
  31. //小时
  32. jwt = JwtUtil.createJWT("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 24L * 60L * 60L * 1000L);
  33. break;
  34. }
  35. }
  36. return new ReturnData<String>(HttpStatus.SC_OK,"认证成功",jwt);
  37. }

五、yml配置文件

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

  1. ###################################
  2. #服务启动端口的配置
  3. ###################################
  4. server:
  5. port: ${server-port}
  6. ###############################################################
  7. # eureka 的相关配置
  8. # 如果不需要 结合eureka 使用,可以不要这一段配置
  9. ###############################################################
  10. eureka:
  11. client:
  12. fetch-registry: true
  13. register-with-eureka: ${register-with-eureka} # 是否注册到eureka
  14. service-url:
  15. defaultZone: ${service-url-defaultZone}
  16. instance:
  17. prefer-ip-address: false
  18. hostname: ${instance-hostname}
  19. spring:
  20. cloud:
  21. #################################
  22. # gateway相关配置
  23. #################################
  24. gateway:
  25. # 路由定义
  26. routes:
  27. - id: baidu
  28. uri: https://www.baidu.com
  29. predicates:
  30. - Path=/baidu/**
  31. filters:
  32. - StripPrefix=1
  33. - id: eureka-manage
  34. uri: lb://eureka-manage
  35. predicates:
  36. - Path=/eureka-manage/**
  37. filters:
  38. - StripPrefix=1
  39. - id: sina
  40. uri: https://www.sina.com.cn/
  41. predicates:
  42. - Path=/sina/**
  43. filters:
  44. - StripPrefix=1
  45. org:
  46. my:
  47. jwt:
  48. #跳过认证的路由
  49. skip-auth-urls:
  50. - /baidu
  51. ############################################
  52. # 有效时长
  53. # 单位:d:天、h:小时、m:分钟、s:秒
  54. ###########################################
  55. effective-time: 1m

测试

直接不带认证信息访问一个需要认证的路由:访问一个新浪得路由,提示需要认证
http://localhost:30006/sina
在这里插入图片描述
调用认证api获取token

在这里插入图片描述
把token加入请求头,再次访问新浪得路由,可以通过认证
在这里插入图片描述
尝试token过期后访问,在application.yml中我配置了token一分钟后过期,一分钟后我再次携带token访问新浪得路由,提示认证过期
在这里插入图片描述

进阶:制作JwtToken校验注解

定义注解

  1. package com.lzx.gateway.annotation;
  2. import java.lang.annotation.*;
  3. /**
  4. * 描述: jwt检查注解
  5. *
  6. * @Auther: lzx
  7. * @Date: 2019/6/17 16:24
  8. */
  9. @Target(ElementType.METHOD)
  10. @Retention(RetentionPolicy.RUNTIME)
  11. @Documented
  12. public @interface JwtCheck {
  13. String value() default "";
  14. }

定义注解得AOP

  1. package com.lzx.gateway.aop;
  2. import com.fasterxml.jackson.databind.ObjectMapper;
  3. import com.lzx.gateway.dto.ReturnData;
  4. import com.lzx.gateway.jwt.JwtUtil;
  5. import io.jsonwebtoken.ExpiredJwtException;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.apache.commons.lang.ArrayUtils;
  8. import org.apache.commons.lang.StringUtils;
  9. import org.aspectj.lang.ProceedingJoinPoint;
  10. import org.aspectj.lang.annotation.Around;
  11. import org.aspectj.lang.annotation.Aspect;
  12. import org.aspectj.lang.annotation.Pointcut;
  13. import org.aspectj.lang.reflect.MethodSignature;
  14. import org.springframework.beans.factory.annotation.Autowired;
  15. import org.springframework.stereotype.Component;
  16. import org.springframework.web.bind.annotation.RequestHeader;
  17. import java.lang.annotation.Annotation;
  18. import java.lang.reflect.Method;
  19. /**
  20. * 描述:添加了 JwtCheck 注解 的Aop
  21. *
  22. * @Auther: lzx
  23. * @Date: 2019/6/18 10:56
  24. */
  25. @Component
  26. @Aspect
  27. @Slf4j
  28. public class JwtCheckAop {
  29. @Autowired
  30. private ObjectMapper objectMapper;
  31. @Pointcut("@annotation(com.lzx.gateway.annotation.JwtCheck)")
  32. private void apiAop(){
  33. }
  34. /**
  35. * 方法执行前的aop
  36. * @param point
  37. * @return
  38. * @throws Throwable
  39. */
  40. @Around("apiAop()")
  41. public Object aroundApi(ProceedingJoinPoint point) throws Throwable {
  42. MethodSignature signature = (MethodSignature) point.getSignature();
  43. Method method = signature.getMethod();
  44. //获取参数上得所有注解
  45. Annotation[][] parameterAnnotationArray = method.getParameterAnnotations();
  46. Object[] args = point.getArgs();
  47. String token = null;
  48. /*
  49. a -> start
  50. 这个代码片得逻辑:找出有 @RequestHeader("Authorization") 的参数,赋值给 token变量
  51. */
  52. for(Annotation[] annotations : parameterAnnotationArray){
  53. for(Annotation a:annotations){
  54. if(a instanceof RequestHeader){
  55. RequestHeader requestHeader = (RequestHeader)a;
  56. if("Authorization".equals(requestHeader.value())){
  57. token = (String) args[ArrayUtils.indexOf(parameterAnnotationArray,annotations)];
  58. }
  59. }
  60. }
  61. }
  62. /*
  63. a -> end
  64. */
  65. if(StringUtils.isBlank(token)){
  66. //没有token
  67. return authErro("请登陆");
  68. }else{
  69. //有token
  70. try {
  71. JwtUtil.checkToken(token,objectMapper);
  72. Object proceed = point.proceed();
  73. return proceed;
  74. }catch (ExpiredJwtException e){
  75. log.error(e.getMessage(),e);
  76. if(e.getMessage().contains("Allowed clock skew")){
  77. return authErro("认证过期");
  78. }else{
  79. return authErro("认证失败");
  80. }
  81. }catch (Exception e) {
  82. log.error(e.getMessage(),e);
  83. return authErro("认证失败");
  84. }
  85. }
  86. }
  87. /**
  88. * 认证错误输出
  89. * @param mess 错误信息
  90. * @return
  91. */
  92. private Object authErro(String mess) {
  93. ReturnData<String> returnData = new ReturnData<>(org.apache.http.HttpStatus.SC_UNAUTHORIZED, mess, mess);
  94. return returnData;
  95. }
  96. }

注解的使用方法
直接在方法上使用@JwtCheck

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

发表评论

表情:
评论列表 (有 0 条评论,322人围观)

还没有评论,来说两句吧...

相关阅读

    相关 spring-cloud-gateway统一前缀

    由于前端框架需要做路由代理,需要在后端api接口加上前缀,区分前端路由跟后端接口。前后端分离项目,后端采用Spring cloud微服务架构。 一、第一次尝试(失败) 在