重新定义cloud 第13章 spring cloud 认证和鉴权.md

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----------------
      
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值