物联网架构成长之路(56)-SpringCloudGateway+JWT实现网关鉴权
0. 前言
结合前面两篇博客,前面博客实现了Gateway网关的路由功能。此时,如果每个微服务都需要一套帐号认证体系就没有必要了。可以在网关处进行权限认证。然后转发请求到后端服务。这样后面的微服务就可以直接调用,而不需要每个都单独一套鉴权体系。参考了Oauth2和JWT,发现基于微服务,使用JWT会更方便一些,所以准备集成JWT作为微服务架构的认证方式。
【https://www.cnblogs.com/wunaozai/p/12512753.html】 物联网架构成长之路(54)-基于Nacos+Gateway实现动态路由
【https://www.cnblogs.com/wunaozai/p/12512850.html】 物联网架构成长之路(55)-Gateway+Sentinel实现限流、熔断
1. Gateway增加一个过滤器
在上一篇博客中实现的Gateway,增加一个AuthFilter过滤器。目的就是对所有的请求进行认证。
代码可以参考官方的几个标准过滤器
AuthFilter.java
1 package com.wunaozai.demo.gateway.config.filter; 2 3 import java.nio.charset.StandardCharsets; 4 import java.util.Arrays; 5 import java.util.List; 6 import java.util.Map; 7 8 import org.springframework.beans.factory.annotation.Autowired; 9 import org.springframework.cloud.gateway.filter.GatewayFilterChain; 10 import org.springframework.cloud.gateway.filter.GlobalFilter; 11 import org.springframework.context.annotation.Bean; 12 import org.springframework.context.annotation.Configuration; 13 import org.springframework.core.annotation.Order; 14 import org.springframework.core.io.buffer.DataBuffer; 15 import org.springframework.http.HttpCookie; 16 import org.springframework.http.HttpStatus; 17 import org.springframework.http.server.reactive.ServerHttpRequest; 18 import org.springframework.http.server.reactive.ServerHttpResponse; 19 import org.springframework.util.MultiValueMap; 20 import org.springframework.util.StringUtils; 21 import org.springframework.web.client.RestTemplate; 22 import org.springframework.web.server.ServerWebExchange; 23 24 import com.wunaozai.demo.gateway.config.JsonResponseUtils; 25 import reactor.core.publisher.Mono; 26 27 @Configuration 28 public class AuthFilter { 29 30 private static final String JWT_TOKEN = "jwt-token"; 31 32 @Autowired 33 private RestTemplate restTemplate; 34 35 @Bean 36 @Order 37 public GlobalFilter authJWT() { 38 GlobalFilter auth = new GlobalFilter() { 39 @Override 40 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { 41 System.out.println("filter auth...."); 42 ServerHttpRequest request = exchange.getRequest(); 43 ServerHttpResponse response = exchange.getResponse(); 44 //判断是否需要过滤 45 String path = request.getURI().getPath(); 46 List<String> pages = Arrays.asList("/auth/v1/login", 47 "/auth/v1/refresh", "/auth/v1/check"); 48 for(int i=0; i<pages.size(); i++) { 49 if(pages.get(i).equals(path)) { 50 //直接通过,传输到下一级 51 return chain.filter(exchange); 52 } 53 } 54 55 //判断是否存在JWT 56 String jwt = ""; 57 List<String> headers = request.getHeaders().get(JWT_TOKEN); 58 if(headers != null && headers.size() > 0) { 59 jwt = headers.get(0); 60 } 61 if(StringUtils.isEmpty(jwt)) { 62 MultiValueMap<String, HttpCookie> cookies = request.getCookies(); 63 if(cookies != null && cookies.size() > 0) { 64 List<HttpCookie> cookie = cookies.get(JWT_TOKEN); 65 if(cookie != null && cookie.size() > 0) { 66 HttpCookie ck = cookie.get(0); 67 jwt = ck.getValue(); 68 } 69 } 70 } 71 if(StringUtils.isEmpty(jwt)) { 72 //返回未授权错误 73 return error(response, JsonResponseUtils.AUTH_UNLOGIN_ERROR); 74 } 75 76 //通过远程调用判断JWT是否合法 77 String json = ""; 78 try { 79 Map<?, ?> ret = restTemplate.getForObject("http://jieli-story-auth/auth/v1/info?jwt=" + jwt, Map.class); 80 String code = ret.get("code").toString(); 81 if(!"0".equals(code)) { 82 //返回认证错误 83 return error(response, JsonResponseUtils.AUTH_EXP_ERROR); 84 } 85 json = ret.get("data").toString(); 86 } catch (Exception e) { 87 e.printStackTrace(); 88 return error(response, JsonResponseUtils.AUTH_EXP_ERROR); 89 } 90 //将登录信息保存到下一级 91 ServerHttpRequest newRequest = request.mutate().header("auth", json).build(); 92 ServerWebExchange newExchange = 93 exchange.mutate().request(newRequest).build(); 94 return chain.filter(newExchange); 95 } 96 }; 97 return auth; 98 } 99 100 private Mono<Void> error(ServerHttpResponse response, String json) { 101 //返回错误 102 response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); 103 response.setStatusCode(HttpStatus.UNAUTHORIZED); 104 DataBuffer buffer = response.bufferFactory().wrap(json.getBytes(StandardCharsets.UTF_8)); 105 return response.writeWith(Mono.just(buffer)); 106 } 107 }
BeanConfig.java
1 package com.wunaozai.demo.gateway.config.filter; 2 3 import org.springframework.cloud.client.loadbalancer.LoadBalanced; 4 import org.springframework.context.annotation.Bean; 5 import org.springframework.stereotype.Component; 6 import org.springframework.web.client.RestTemplate; 7 8 @Component 9 public class BeanConfig { 10 11 /** 12 * 消费者 13 * @return 14 */ 15 @Bean 16 @LoadBalanced 17 public RestTemplate restTemplate() { 18 return new RestTemplate(); 19 } 20 }
JsonResponseUtils.java
1 package com.wunaozai.demo.gateway.config; 2 3 /** 4 * 常量返回 5 * @author wunaozai 6 * @Date 2020-03-18 7 */ 8 public class JsonResponseUtils { 9 10 public static final String BLOCK_FLOW_ERROR = "{\"code\": -1, \"data\": null, \"msg\": \"系统限流\"}"; 11 public static final String AUTH_UNLOGIN_ERROR = "{\"code\": -1, \"data\": null, \"msg\": \"未授权\"}"; 12 public static final String AUTH_EXP_ERROR = "{\"code\": -1, \"data\": null, \"msg\": \"授权过期\"}"; 13 public static final String AUTH_PARAM_ERROR = "{\"code\": -1, \"data\": null, \"msg\": \"参数异常\"}"; 14 15 }
2. Auth授权服务
这里使用JWT作为微服务间的鉴权协议
pom.xml
1 <!-- JWT --> 2 <dependency> 3 <groupId>io.jsonwebtoken</groupId> 4 <artifactId>jjwt</artifactId> 5 <version>0.9.1</version> 6 </dependency>
AuthController.java(这里面包含了部分数据库操作代码,如果测试,删除即可)
1 package com.wunaozai.demo.auth.controller; 2 3 import java.util.HashMap; 4 import java.util.Map; 5 6 import org.springframework.beans.factory.annotation.Autowired; 7 import org.springframework.web.bind.annotation.RequestMapping; 8 import org.springframework.web.bind.annotation.RestController; 9 10 import com.alibaba.fastjson.JSONObject; 11 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; 12 import com.baomidou.mybatisplus.extension.api.R; 13 14 import io.jsonwebtoken.Claims; 15 import com.wunaozai.demo.auth.common.utils.SecretUtils; 16 import com.wunaozai.demo.auth.common.utils.jwt.JWTToken; 17 import com.wunaozai.demo.auth.common.utils.jwt.JWTUtils; 18 import com.wunaozai.demo.auth.model.entity.AuthUserModel; 19 import com.wunaozai.demo.auth.service.IAuthUserService; 20 21 @RestController 22 @RequestMapping(value="/auth/v1") 23 public class AuthController { 24 25 @Autowired 26 private IAuthUserService authuserService; 27 28 @RequestMapping(value="/login") 29 public R<Object> login(String username, String password, String type){ 30 AuthUserModel user = getUser(username); 31 if(user == null) { 32 return R.failed("帐号密码错误"); 33 } 34 if(user.getStatus() == false) { 35 return R.failed("当前账号被禁用"); 36 } 37 if (checkPwd(user, password) == false) { 38 return R.failed("帐号密码错误"); 39 } 40 Map<String, String> map = new HashMap<>(); 41 map.put("userId", user.getUserId().toString()); 42 map.put("username", user.getUsername()); 43 String body = JSONObject.toJSONString(map); 44 JWTToken token = JWTUtils.getJWT(body, "admin"); 45 return R.ok(token); 46 } 47 @RequestMapping(value="/check") 48 public R<Object> check(String jwt){ 49 boolean flag = JWTUtils.checkJWT(jwt); 50 return R.ok(flag); 51 } 52 @RequestMapping(value="/refresh") 53 public R<Object> refresh(String jwt){ 54 boolean flag = JWTUtils.checkJWT(jwt); 55 if(flag == false) { 56 return R.ok("Token已过期"); 57 } 58 JWTToken token = JWTUtils.refreshJWT(jwt); 59 return R.ok(token); 60 } 61 @RequestMapping(value="/info") 62 public R<Object> info(String jwt){ 63 boolean flag = JWTUtils.checkJWT(jwt); 64 if(flag == false) { 65 return R.ok("Token已过期"); 66 } 67 Claims claims = JWTUtils.infoJWT(jwt); 68 return R.ok(claims); 69 } 70 71 /** 72 * 匹配密码 73 * @param user 74 * @param password 75 * @return 76 */ 77 private boolean checkPwd(AuthUserModel user, String password) { 78 if(user == null) { 79 return false; 80 } 81 return SecretUtils.matchBcryptPassword(password, user.getPassword()); 82 } 83 /** 84 * 获取用户模型 85 * @param username 86 * @return 87 */ 88 private AuthUserModel getUser(String username) { 89 QueryWrapper<AuthUserModel> query = new QueryWrapper<>(); 90 query.eq("username", username); 91 return authuserService.getOne(query); 92 } 93 }
JWTToken.java
1 package com.wunaozai.demo.auth.common.utils.jwt; 2 3 import lombok.Builder; 4 import lombok.Getter; 5 import lombok.Setter; 6 7 @Getter 8 @Setter 9 @Builder 10 public class JWTToken { 11 private String access_token; 12 private String token_type; 13 private Long expires_in; 14 }
JWTUtils.java
1 package com.wunaozai.demo.auth.common.utils.jwt; 2 3 import java.util.Base64; 4 import java.util.Date; 5 import java.util.UUID; 6 7 import javax.crypto.SecretKey; 8 import javax.crypto.spec.SecretKeySpec; 9 10 import io.jsonwebtoken.Claims; 11 import io.jsonwebtoken.JwtBuilder; 12 import io.jsonwebtoken.Jwts; 13 import io.jsonwebtoken.SignatureAlgorithm; 14 15 /** 16 * JWT 工具类 17 * @author wunaozai 18 * @Date 2020-03-18 19 */ 20 public class JWTUtils { 21 22 private static final String JWT_KEY = "test"; 23 /** 24 * 生成JWT 25 * @param body 26 * @param role 27 * @return 28 */ 29 public static JWTToken getJWT(String body, String role) { 30 Long expires_in = 1000 * 60 * 60 * 24L; //一天 31 long time = System.currentTimeMillis(); 32 time = time + expires_in; 33 JwtBuilder builder = Jwts.builder() 34 .setId(UUID.randomUUID().toString()) //设置唯一ID 35 .setSubject(body) //设置内容,这里用JSON包含帐号信息 36 .setIssuedAt(new Date()) //签发时间 37 .setExpiration(new Date(time)) //过期时间 38 .claim("roles", role) //设置角色 39 .signWith(SignatureAlgorithm.HS256, generalKey()) //设置签名 使用HS256算法,并设置密钥 40 ; 41 String code = builder.compact(); 42 JWTToken token = JWTToken.builder() 43 .access_token(code) 44 .expires_in(expires_in / 1000) 45 .token_type("JWT") 46 .build(); 47 return token; 48 } 49 /** 50 * 解析JWT 51 * @param jwt 52 * @return 53 */ 54 public static Claims parseJWT(String jwt) { 55 Claims body = Jwts.parser().setSigningKey(generalKey()).parseClaimsJws(jwt).getBody(); 56 return body; 57 } 58 /** 59 * 刷新JWT 60 * @param jwt 61 * @return 62 */ 63 public static JWTToken refreshJWT(String jwt) { 64 Claims claims = parseJWT(jwt); 65 String body = claims.getSubject(); 66 String role = claims.get("roles").toString(); 67 return getJWT(body, role); 68 } 69 /** 70 * 获取JWT信息 71 * @param jwt 72 * @return 73 */ 74 public static Claims infoJWT(String jwt) { 75 Claims claims = parseJWT(jwt); 76 return claims; 77 } 78 /** 79 * 验证JWT 80 * @param jwt 81 * @return 82 */ 83 public static boolean checkJWT(String jwt) { 84 try { 85 Claims body = Jwts.parser().setSigningKey(generalKey()).parseClaimsJws(jwt).getBody(); 86 if(body != null) { 87 return true; 88 } 89 } catch (Exception e) { 90 return false; 91 } 92 return false; 93 } 94 95 /** 96 * 生成加密后的秘钥 secretKey 97 * @return 98 */ 99 public static SecretKey generalKey() { 100 byte[] encodedKey = Base64.getDecoder().decode(JWT_KEY); 101 SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); 102 return key; 103 } 104 }
3. Res资源服务
测试是否转发到后端服务
IndexController.java
1 package com.wunaozai.demo.res.controller.web; 2 3 import java.util.HashMap; 4 import java.util.Map; 5 6 import javax.servlet.http.HttpServletRequest; 7 8 import org.springframework.beans.factory.annotation.Autowired; 9 import org.springframework.web.bind.annotation.RequestBody; 10 import org.springframework.web.bind.annotation.RequestMapping; 11 import org.springframework.web.bind.annotation.RestController; 12 13 import com.baomidou.mybatisplus.extension.api.R; 14 15 @RestController 16 @RequestMapping(value="/res/v1/") 17 public class IndexController { 18 19 @Autowired 20 private HttpServletRequest request; 21 22 @RequestMapping(value="/login") 23 public R<Map<String, Object>> login(){ 24 Map<String, Object> data = new HashMap<String, Object>(); 25 data.put("", ""); 26 return R.ok(data); 27 } 28 29 @RequestMapping(value="/test") 30 public R<Object> test(String msg, @RequestBody String body){ 31 System.out.println(msg); 32 System.out.println(body); 33 System.out.println(request.getHeader("auth")); 34 return R.ok("ok"); 35 } 36 }
4. 系统架构图
整体的架构流程图,就是一个请求经过Nginx,进行前后端分离。后端请求转发到Gateway,Gateway通过Nacos上配置的route(路由转发规则,限流Sentinel规则)。判断是否携带JWT-Token信息,请求访问Auth授权服务,查询是否正确的JWT-Token合法用户。如果是合法用户,将对应的请求转发到后端各个微服务中,以本例子,将/res/v1 开头转发到StoryRes服务,将/aiml/v1 开头的请求转发到StoryAIML服务。
架构流程图
各个微服务
各个微服务注册到Nacos上
本项目所有Nacos上的配置信息
5. 测试过程
通过PostMan进行模拟测试
5.1 请求/auth/v1/login
注意保存返回的access_token,以后每次请求都需要设置到Header上
5.2 请求/auth/v1/info
注意将jwt-token设置到Header上,这里就是返回用户信息。一般是给后端服务查询用的。不会暴露给用户。可以看到AuthFilter.java 这个类就是调用这个微服务,实现验证当前用户是否合法。同时将这个返回保存到Header上,并将登录信息保存到下一级。这样后面的微服务可以通过判断Header里面的这个登录信息userId。作为外键。
5.3 请求/res/v1/test
注意将jwt-token设置到Header上,这里模拟测试,通过QueryParam方式传参数和Body传参数。后端都是可以正常接收并打印
后续就会出基于vue-element-admin的前端开发框架,结合到本项目。实现前后端分离。【期待】
参考资料:
https://blog.csdn.net/tianyaleixiaowu/article/details/83375246
https://www.cnblogs.com/fdzang/p/11812348.html
本文地址:https://www.cnblogs.com/wunaozai/p/12522485.html
本系列目录: https://www.cnblogs.com/wunaozai/p/8067577.html
个人主页:https://www.wunaozai.com/
作者:无脑仔的小明 出处:http://www.cnblogs.com/wunaozai/ 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。 如果文中有什么错误,欢迎指出。以免更多的人被误导。有需要沟通的,可以站内私信,文章留言,或者关注“无脑仔的小明”公众号私信我。一定尽力回答。 |