1、架构图:
1、UAA认证服务负责认证授权。
2、所有请求经过 网关到达微服务
3、网关负责鉴权客户端以及请求转发
4、网关将token解析后传给微服务,微服务进行授权。
2、注册中心
所有微服务的请求都经过网关,网关从注册中心读取微服务的地址,将请求转发至微服务。 本节完成注册中心的搭建,注册中心采用Eureka。
新建一个module。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>OAuth2.0</artifactId>
<groupId>com.oauth.security</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>discovery</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
</project>
spring:
application:
name: distributed-discovery
server:
port: 53000 #启动端口
eureka:
server:
enable-self-preservation: false #关闭服务器自我保护,客户端心跳检测15分钟内错误达到80%服务会保护,导致别人还认为是好用的服务
eviction-interval-timer-in-ms: 10000 #清理间隔(单位毫秒,默认是60*1000)5秒将客户端剔除的服务在服务注册列表中剔除#
shouldUseReadOnlyResponseCache: true #eureka是CAP理论种基于AP策略,为了保证强一致性关闭此切换CP 默认不关闭 false关闭
client:
register-with-eureka: false #false:不作为一个客户端注册到注册中心
fetch-registry: false #为true时,可以启动,但报异常:Cannot execute request on any known server
instance-info-replication-interval-seconds: 10
serviceUrl:
defaultZone: http://localhost:${server.port}/eureka/
instance:
hostname: ${spring.cloud.client.ip-address}
prefer-ip-address: true
instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
package com.oauth.security;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
/**
* @ClassName DiscoveryServer
* @Description 注册中心
* @Author
* @Date 2020/5/11 14:40
* @Version 1.0
**/
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServer {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServer.class, args);
}
}
然后修改order和uaa的pom和配置文件(解注释):
3、网关
网关整合 OAuth2.0 有两种思路,一种是认证服务器生成jwt令牌, 所有请求统一在网关层验证,判断权限等操作; 另一种是由各资源服务处理,网关只做请求转发。
我们选用第一种。我们把API网关作为OAuth2.0的资源服务器角色,实现接入客户端权限拦截、令牌解析并转发当 前登录用户信息(jsonToken)给微服务,这样下游微服务就不需要关心令牌格式解析以及OAuth2.0相关机制了。
API网关在认证授权体系里主要负责两件事:
(1)作为OAuth2.0的资源服务器角色,实现接入方权限拦截。
(2)令牌解析并转发当前登录用户信息(明文token)给微服务
微服务拿到明文token(明文token中包含登录用户的身份和权限信息)后也需要做两件事:
(1)用户授权拦截(看当前用户是否有权访问该资源)
(2)将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>OAuth2.0</artifactId>
<groupId>com.oauth.security</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>getway</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
spring.application.name=gateway-server
server.port=53010
spring.main.allow-bean-definition-overriding = true
logging.level.root = info
logging.level.org.springframework = info
zuul.retryable = true
zuul.ignoredServices = *
zuul.add-host-header = true
zuul.sensitiveHeaders = *
zuul.routes.uaa-service.stripPrefix = false
zuul.routes.uaa-service.path = /uaa/**
zuul.routes.order-service.stripPrefix = false
zuul.routes.order-service.path = /order/**
eureka.client.serviceUrl.defaultZone = http://localhost:53000/eureka/
eureka.instance.preferIpAddress = true
eureka.instance.instance-id = ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
management.endpoints.web.exposure.include = refresh,health,info,env
feign.hystrix.enabled = true
feign.compression.request.enabled = true
feign.compression.request.mime-types[0] = text/xml
feign.compression.request.mime-types[1] = application/xml
feign.compression.request.mime-types[2] = application/json
feign.compression.request.min-request-size = 2048
feign.compression.response.enabled = true
package com.oauth.security;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
/**
* @ClassName GatewayServer
* @Description 网关
* @Author
* @Date 2020/5/11 14:41
* @Version 1.0
**/
@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
public class GatewayServer {
public static void main(String[] args) {
SpringApplication.run(GatewayServer.class, args);
}
}
package com.oauth.security.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
@Configuration
public class ResouceServerConfig {
public static final String RESOURCE_ID = "res1";
//uaa资源服务配置
@Configuration
@EnableResourceServer
public class UAAServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources){
resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/uaa/**").permitAll();
}
}
//order资源
//uaa资源服务配置
@Configuration
@EnableResourceServer
public class OrderServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources){
resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/order/**").access("#oauth2.hasScope('ROLE_API')");
}
}
//配置其它的资源服务..
}
再将TokenConfig复制到config目录下。
上面定义了两个微服务的资源,其中:
UAAServerConfig指定了若请求匹配/uaa/**网关不进行拦截。
OrderServerConfig指定了若请求匹配/order/**,也就是访问统一用户服务,接入客户端需要有scope中包含 read,并且authorities(权限)中需要包含ROLE_USER。
由于res1这个接入客户端,read包括ROLE_ADMIN,ROLE_USER,ROLE_API三个权限。
package com.oauth.security.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @ClassName WebSecurityConfig
* @Description
* @Author
* @Date 2020/5/11 21:50
* @Version 1.0
**/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/**").permitAll()
.and().csrf().disable();
}
}
转发明文token给微服务:
通过Zuul过滤器的方式实现,目的是让下游微服务能够很方便的获取到当前的登录用户信息(明文token)。
(1)实现Zuul前置过滤器,完成当前登录用户信息提取,并放入转发微服务的request中
package com.oauth.security.config;
import com.oauth.security.filter.AuthFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* @ClassName ZuulConfig
* @Description
* @Author
* @Date 2020/5/11 22:21
* @Version 1.0
**/
@Configuration
public class ZuulConfig {
@Bean
public AuthFilter preFileter() {
return new AuthFilter();
}
@Bean
public FilterRegistrationBean corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setMaxAge(18000L);
source.registerCorsConfiguration("/**", config);
CorsFilter corsFilter = new CorsFilter(source);
FilterRegistrationBean bean = new FilterRegistrationBean(corsFilter);
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}
package com.oauth.security.filter;
import com.alibaba.fastjson.JSON;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.oauth.security.util.EncryptUtil;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @ClassName AuthFilter
* @Description
* @Author 戴书博
* @Date 2020/5/11 22:04
* @Version 1.0
**/
public class AuthFilter extends ZuulFilter {
@Override
public boolean shouldFilter() {
return true;
}
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
//从安全上下文中拿 到用户身份对象
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(!(authentication instanceof OAuth2Authentication)){
return null;
}
OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication;
Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();
//取出用户身份信息
String principal = userAuthentication.getName();
//取出用户权限
List<String> authorities = new ArrayList<>();
//从userAuthentication取出权限,放在authorities
userAuthentication.getAuthorities().stream().forEach(c->authorities.add(((GrantedAuthority) c).getAuthority()));
OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
Map<String,Object> jsonToken = new HashMap<>(requestParameters);
if(userAuthentication!=null){
jsonToken.put("principal",principal);
jsonToken.put("authorities",authorities);
}
//把身份信息和权限信息放在json中,加入http的header中,转发给微服务
ctx.addZuulRequestHeader("json-token", EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken)));
return null;
}
}
4、修改微服务order
当微服务收到明文token时,应该怎么鉴权拦截呢?自己实现一个filter?自己解析明文token,自己定义一套资源 访问策略?
能不能适配Spring Security呢,是不是突然想起了前面我们实现的Spring Security基于token认证例子。咱们还拿 统一用户服务作为网关下游微服务,对它进行改造,增加微服务用户鉴权拦截功能。
package com.oauth.security.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* @ClassName WebSecurityConfig
* @Description
* @Author
* @Date 2020/5/10 15:16
* @Version 1.0
**/
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//安全拦截机制,由于我们上面对controller做了一个注解,这里面暂时不用具体写拦截,但是这个要有。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通
.anyRequest().permitAll()//除了/r/**,其它的请求可以访问
;
}
}
package com.oauth.security.filter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.oauth.security.model.UserDTO;
import com.oauth.security.util.EncryptUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
/**
* @ClassName TokenAuthenticationFilter
* @Description
* @Author
* @Date 2020/5/12 8:28
* @Version 1.0
**/
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//获取json
String token = httpServletRequest.getHeader("json‐token");
if (token != null) {
String json = EncryptUtil.decodeUTF8StringBase64(token);
JSONObject userJson = JSON.parseObject(json);
//用户身份信息
UserDTO userDTO = new UserDTO();
userDTO.setUsername( userJson.getString("principal"));
//用户权限
JSONArray authoritiesArray = userJson.getJSONArray("authorities");
String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);
//将用户信息和权限填充到用户token对象中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDTO,null, AuthorityUtils.createAuthorityList(authorities));
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
//将这个对象填充到安全上下文。
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
UserDTO user = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
还是三个步骤: 1.解析token 2.新建并填充authentication 3.将authentication保存进安全上下文 剩下的事儿就交给Spring Security好了。
增加资源:
@RestController
public class OrderController {
@GetMapping(value = "/r1")
@PreAuthorize("hasAnyAuthority('p1')")
public String r1() {
UserDTO user = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return user.getUsername() + "访问资源1";
}
@GetMapping(value = "/r2")
@PreAuthorize("hasAnyAuthority('p2')")
public String r2() {
UserDTO user = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return user.getUsername() + "访问资源2";
}
}
5、集成测试
启动四个服务。
(1)密码模式
(2)授权码
http://localhost:53010/uaa/oauth/authorize?client_id=c1&response_type=code&scope=ROLE_ADMIN&redirect_uri=http://www.baidu.com
localhost:53020/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=N9PW7w&redirect_uri=http://www.baidu.com