oauth2.0 + awt
-
对于 授权 - 认证 比较成熟的面向资源 的授权协议
-
gitHub,QQ, 登录
-
用于定义 spring cloud中国社区(自己的软件)与 用户之间的 那个 “授权层” 的。
-
认证流程
-
客户端 ——>资源拥有者 (用户)
-
A 请求认证
-
B 确认授权 (返回)
-
客户端——>授权服务器
-
C 申请令牌
-
D 发放令牌
-
客户端——>资源服务器
-
E 使用令牌
-
F 返回资源
-
JWT
-
json web token
-
使用 json 格式 来 规约 token 或 session 的 协议
-
传统方式 生成一个凭证,保存于 服务端 或 持久化工具中 (存取麻烦)
-
jwt 打破了 这一瓶颈,实现了 :客户端 session
jwt 构成
- header头部,jwt 签名算法
- payload 载荷,自定义 与 非自定义的认证信息
- signature 签名,头部 与 载荷 使用 . 连接之后,使用 头部 的 签名算法 生成 签名信息,并拼接到 末尾
oauth2.0 + awt 意义
-
使用 oauth2.0 协议的思想 拉取 认证生成 token
-
使用 jwt 保存这个token
-
在 客户端 和 资源端 进行 对称 或 非对称 加密
-
具有 定时,定量,授权 认证 功能。(免去 token 存储)
-
发送请求到 负载均衡 软件
-
在到 网关,网关 进行 用户的认证,解析出用户的基本信息
-
携带 用户本身的标识 到 后台 微服务
-
微服务 获得标识,进行 鉴权
认证 与 鉴权
- 认证: 验证这个用户是谁
- 鉴权: 了解 这个用户 能做什么事
单体应用
- 固定的认证 和 鉴权的包
- 把用户存入 session,生成一个 session ID,
- 客户端 存在 cookie 里
微服务下 sso 单点登录方案
- 拆分成 很多 小的服务,每个服务 都会做用户的 认证 和 鉴权
- 网络消耗,性能损耗。
分布式 session 与 网关结合方案
-
gateway SSO——用户认证,认证通过,存储在 redis里
-
gateway 发送请求 http://xxxx/base/xxx
-
session Id -Redis
-
通过 服务名 获得 服务真实IP,端口 Ribbon
-
MicroService
-
Redis 根据 session Id 获取 构建 用户
-
检查用户是否有权限 interceptor (success/failed)
-
finish
-
redis需要做 高可用
客户端 token 与 网关结合方案
-
zuul (sso)
-
http://xx/base/xxx 发送请求 zuul
-
Jwt session-Id (上图 用redis)
-
通过 服务名 获得 服务 真实 IP,端口 Ribbon
-
MicroService
-
JWT 根据 session ID 获取构建用户 (上图 用redis)
-
Interceptor 校验用户 是否有权限 (Success / Failed )
-
finish
-
客户端持有一个token
-
jwt 或 其他加密算法,实现一种 token (保存了用户信息)
-
token传递到网关,进行 认证 和 校验
-
校验通过,token 传递到 微服务中,进行具体的接口 或 URL 验证
-
或者把 信息 存在 cookie里,通过网关来解析token (老系统用,因为cookie通用性)
网关与 token 和 服务间 鉴权结合
-
服务 与 服务之间的调用 进行鉴权
- 网关做 认证后,传递用户信息 到 header中
- 后台收到 header后进行解析,查看是否有 调用此 服务 或 某个 url的权限
-
服务内部的请求,出去时,进行拦截,用户信息保存到 header,传递出去。
spring cloud 认证 鉴权 实战案例
- gateway 实现用户的认证
- 解析 JWT 后 传递用户信息到 后端服务,
- 后端服务,进行鉴权
gateway
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency> <!--网关-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency> <!--监控-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency> <!--注册中心 客户端-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency> <!--webflux 响应式编程-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency> <!--认证 鉴权 jsonwebtoken-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency> <!--json工具-->
</dependencies>
工具类
-
jwt 生成 和 验证解析 的方法
-
根据用户 生成 jwt,验证的时候 同样传入jwt 进行验证
public class JwtUtil { //n. 秘密,机密;秘诀; public static final String SECRET = "qazwsx123444$#%#()*&& asdaswwi1235 ?;!@#kmmmpom in***xx**&"; //token前缀 public static final String TOKEN_PREFIX = "Bearer"; //header_授权的(authorized) public static final String HEADER_AUTH = "Authorization"; //根据user,生成string public static String generateToken(String user) { //map HashMap<String, Object> map = new HashMap<>(); //放入id 为 随机数 map.put("id", new Random().nextInt()); //用户 为 user map.put("user", user); //jwt 构建,参数为 主题,map,签名包含, String jwt = Jwts.builder() .setSubject("user info").setClaims(map) .signWith(SignatureAlgorithm.HS512, SECRET) .compact(); //拼接最终jwt String finalJwt = TOKEN_PREFIX + " " +jwt; //返回 return finalJwt; } //验证 public static Map<String,String> validateToken(String token) { if (token != null) { HashMap<String, String> map = new HashMap<String, String>(); //Jwts解析,键签名是,解析 jws Map<String,Object> body = Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token.replace(TOKEN_PREFIX, "")) .getBody(); //id String id = String.valueOf(body.get("id")); //user String user = (String) (body.get("user")); map.put("id", id); map.put("user", user); //如果 获取不到 user,扔异常 if(StringUtils.isEmpty(user)) { throw new PermissionException("user is error, please check"); } return map; } else {//token为null,就仍异常 throw new PermissionException("token is error, please check"); } } }
添加过滤器
- 所有的请求会 经过此 filter
- 对 jwt token进行 解析校验,并转换 成 系统内部的 token
- 路由的服务名 也加入 header
@Component //交给spring
public class AuthFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取到 这个 网关的 url
Route gatewayUrl = exchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);//gatewayRoute
//获取到 uri
URI uri = gatewayUrl.getUri();
//获取到 request
ServerHttpRequest request = (ServerHttpRequest)exchange.getRequest();
//获取所有的 header
HttpHeaders header = request.getHeaders();
//获取 签名的token
String token = header.getFirst(JwtUtil.HEADER_AUTH);
//验证 token,获取到 map
Map<String,String> userMap = JwtUtil.validateToken(token);
//构建 mutate 变化,产生突变
ServerHttpRequest.Builder mutate = request.mutate();
//如果用户为 admin 或 spring 或 cloud
if(userMap.get("user").equals("admin") ||
userMap.get("user").equals("spring") ||
userMap.get("user").equals("cloud")) {
//放入 header
mutate.header("x-user-id", userMap.get("id"));
mutate.header("x-user-name", userMap.get("user"));
mutate.header("x-user-serviceName", uri.getHost());
}else {
//否则,扔异常,没有这个哟欧股
throw new PermissionException("user not exist, please check");
}
//构建 request
ServerHttpRequest buildReuqest = mutate.build();
//交给下个 过滤器
return chain.filter(exchange.mutate().request(buildReuqest).build());
}
}
配置文件
- application.yml
spring:
application:
name: gateway-server
cloud:
gateway:
discovery:
locator:
enabled: true #开启gateway
server:
port: 9002
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
security:
basic:
enabled: false
logging:
level:
org.springframework.cloud.gateway: debug
- 访问:http://localhost:9002/CLIENT-SERVICE/test (项目名必须大写)
- 即访问到 :client-service 项目的:test 接口。
核心工程 core-service
- 进入控制器 之前 进行校验
- 微服务 之间 调用时 进行 鉴权
- RestTemplate 拦截器,调用时 传递 上下文信息。
用户上下文拦截器
//继承:HandlerInterceptorAdapter
public class UserContextInterceptor extends HandlerInterceptorAdapter {
//logger打印
private static final Logger log = LoggerFactory.getLogger(UserContextInterceptor.class);
//执行前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse respone, Object arg2) throws Exception {
//获取到 user
User user = getUser(request);
//授权
UserPermissionUtil.permission(user);
//验证用户,如果 验证结果不为 true
if(!UserPermissionUtil.verify(user,request)) {
//执行错误的逻辑
respone.setHeader("Content-Type", "application/json");
//转成json
String jsonstr = JSON.toJSONString("no permisson access service, please check");
//response 写入
respone.getWriter().write(jsonstr);
respone.getWriter().flush();
respone.getWriter().close();
//并且扔异常
throw new PermissionException("no permisson access service, please check");
}
//放入:ThreadLocal<User>
UserContextHolder.set(user);
return true;
}
//执行后
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse respone, Object arg2, ModelAndView arg3)
throws Exception {
// DOING NOTHING
}
//执行完成
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse respone, Object arg2, Exception arg3)
throws Exception {
UserContextHolder.shutdown();
}
//获取到 user
private User getUser(HttpServletRequest request){
String userid = request.getHeader("x-user-id");
String username = request.getHeader("x-user-name");
User user = new User();
user.setUserId(userid);
user.setUserName(username);
return user;
}
static class PermissionException extends RuntimeException {
private static final long serialVersionUID = 1L;
public PermissionException(String msg) {
super(msg);
}
}
}
thread 和 User类
public class UserContextHolder {
public static ThreadLocal<User> context = new ThreadLocal<User>();
public static User currentUser() {
return context.get();
}
public static void set(User user) {
context.set(user);
}
public static void shutdown() {
context.remove();
}
}
public class User implements Serializable {
private static final long serialVersionUID = -4083327605430665846L;
public final static String CONTEXT_KEY_USERID = "x-user-id";
private String userId;
private String userName;
private List<String> allowPermissionService;
}
用户权限类
public class UserPermissionUtil {
/**
* 模拟权限校验, 可以根据自己项目需要定制不同的策略,如查询数据库获取具体的菜单url或者角色等等.
* @param user
*/
public static boolean verify(User user,HttpServletRequest request){
String url = request.getHeader("x-user-serviceName");
if(StringUtils.isEmpty(user)) {
return false;
}else {
List<String> str = user.getAllowPermissionService();
//user中的权限,和 header中的一样
for (String permissionService : str) {
if(url.equalsIgnoreCase(permissionService)) {
return true;
}
}
return false;
}
}
/**
* 模拟权限赋值, 可以根据自己项目需要定制不同的策略,如查询数据库获取具体的菜单url或者角色等等.
* @param user
*/
public static void permission(User user){
if(user.getUserName().equals("admin")) {
List allowPermissionService = new ArrayList();
allowPermissionService.add("client-service");
allowPermissionService.add("provider-service");
//user里 加两个list
user.setAllowPermissionService(allowPermissionService);
}else if(user.getUserName().equals("spring")) {
List allowPermissionService = new ArrayList();
allowPermissionService.add("client-service");
user.setAllowPermissionService(allowPermissionService);
} else {
List allowPermissionService = new ArrayList();
user.setAllowPermissionService(allowPermissionService);
}
}
}
- 给了不同用户 可以访问服务的列表
- 在根据 请求路由的 服务名 来校验 该用户 是否 有访问 这个 服务的权限
- 上线:查询数据库 获取 具体的 菜单 url 或 角色校验
restTemplate拦截器
-
拦截 请求后 传递 上下文信息 和 服务名 到 header中
public class RestTemplateUserContextInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { User user = UserContextHolder.currentUser(); request.getHeaders().add("x-user-id",user.getUserId()); request.getHeaders().add("x-user-name",user.getUserName()); request.getHeaders().add("x-user-serviceName",request.getURI().getHost()); return execution.execute(request, body); } }
配置 MVC
@Configuration //配置类
@EnableWebMvc //配置 mvc
public class CommonConfiguration extends WebMvcConfigurerAdapter{
/**
* 请求拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserContextInterceptor());
}
/***
* RestTemplate 拦截器,在发送请求前设置鉴权的用户上下文信息
* @return
*/
@LoadBalanced //开启 ribbon的 负载均衡
@Bean //这是个类
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new RestTemplateUserContextInterceptor());
return restTemplate;
}
}
服务提供者
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>cn.springcloud.book</groupId>
<artifactId>ch15-1-core-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
测试 action
@RestController
public class ProviderController {
@GetMapping("/provider/test")
public String test(HttpServletRequest request) {
System.out.println("auth success, the user is : " + UserContextHolder.currentUser().getUserName());
System.out.println("----------------success access provider service----------------");
return "success access provider service!";
}
}
bootstrap.yml
server:
port: 7777
spring:
application:
name: provider-service
eureka:
client:
serviceUrl:
defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8761}/eureka/
instance:
prefer-ip-address: true
客户端工程
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>cn.springcloud.book</groupId>
<artifactId>ch15-1-core-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
@RestController
public class TestController {
@Autowired
private RestTemplate rest;
@RequestMapping("/test")
public String test(HttpServletRequest request) {
System.out.println("----------------success access test method!----------------");
Enumeration headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = (String) headerNames.nextElement();
System.out.println(key + ": " + request.getHeader(key));
}
return "success access test method!!";
}
@RequestMapping("/accessProvider")
public String accessProvider(HttpServletRequest request) {
String result = rest.getForObject("http://provider-service/provider/test", String.class);
return result;
}
}
测试
- 在 gateway增加接口,传入用户名,获取到token
@RestController
public class TokenController {
@GetMapping("/getToken/{name}")
public String get(@PathVariable("name") String name) {
return JwtUtil.generateToken(name);
}
}
-
http://localhost:9002/getToken/abcd
-
http://localhost:9002/CLIENT-SERVICE/test
增加header头为: Authorization 值为:Bearer eyJhbGciOiJIUzUxMiJ9.eyJpZCI6ODc2NTgwODQ4LCJ1c2VyIjoiYWJjZCJ9.91Rcr-b8HCzYaV5HWwV1UbYLSHz5Y9e2tYGZeNwQYe05M28Ms2kWaD2ouGIeda847H7FxvuLsIi1c42kR8edjQ 返回:user not exist, please check (这个是自定义的,因为 系统中定义的是 admin,spring,cloud) 用cloud返回:"no permisson access service, please check" 用admin返回:success access test method!!
----------------success access test method!---------------- 10:20:44.541 INFO [AsyncResolver-bootstrap-executor-0] c.n.d.shared.resolver.aws.ConfigClusterResolver - Resolving eureka endpoints via configuration authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJpZCI6MTkxODE0MDY5OSwidXNlciI6ImFkbWluIn0.nZzswFFsApFdcuTCbr4WNad_m9uxQ88BEa445qD8S1z-9i9tM70lEHXGBAIAXiqXYtwrRaj7FVvc47owBQEmTA user-agent: PostmanRuntime/7.26.5 accept: */* postman-token: 13a97770-5b7c-466d-aa73-e1b647c6052e accept-encoding: gzip, deflate, br x-user-id: 1918140699 x-user-name: admin x-user-servicename: CLIENT-SERVICE forwarded: proto=http;host="localhost:9002";for="0:0:0:0:0:0:0:1:62325" x-forwarded-for: 0:0:0:0:0:0:0:1 x-forwarded-proto: http x-forwarded-port: 9002 x-forwarded-host: localhost:9002 host: 192.168.20.92:5566
-
http://localhost:9002/CLIENT-SERVICE/accessProvider 进行鉴权
-
最终调用到:http://provider-service/provider/test
-
auth success, the user is : admin ----------------success access provider service----------------
-