SpringCloud--zuul网关使用

一、简单介绍

Spring Cloud Zuul的作用就是路由转发和过滤, 即将请求转发到微服务或拦截请求; Zuul默认集成了负载均衡功能。
二、简单使用
本项目基于Springboot2.0.3和Spring Cloud Finchley进行部署。

1.添加依赖pom.xml

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

2.配置文件application.yml

spring:
application:
name: zuul
server:
port: 8040

3.启动类ZuulApplication.java

启动类添加@EnableZuulProxy,即可支持网关路由

@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}

}

4.测试

用浏览器访问localhost:8040,即可测试是否启动项目。

三、服务化
Zuul注册到eureka server,通过url的映射转化为服务名ServiceId进行映射。

前提:

  • 先启动eureka,端口号为8761
  • 再启动微服务account,端口号为9090,注册到eureka,并存在请求localhost:9090/account/test

1.添加依赖pom.xml

添加组建服务eureka注册、发现

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

2.配置文件application.yml

注册到eureka,并且配置路由转发。

serviceId为微服务名称,path为路由地址

stripPrefix为false时,则保留account前缀,再转发url到微服务

eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
zuul:
routes:
account:
path: /account/**
serviceId: account
stripPrefix: false

3.测试

用浏览器访问localhost:8040/account/test,如果通过zuul路由,能访问account服务(端口是9090)的请求,则为成功。

网关zuul服务和account服务注册到eureka,如下图:

[图片上传失败...(image-ecc08a-1553260424830)]

四、跨域问题
转发过滤器前,在HttpServletResponse添加跨域头

1.创建CorsFilter .java

@Component
public class CorsFilter extends ZuulFilter {

@Override
public String filterType() {
return "pre"; // 可以在请求被路由之前调用
}

@Override
public int filterOrder() {
return 0;// filter执行顺序,通过数字指定 ,优先级为0,数字越大,优先级越低
}

@Override
public boolean shouldFilter() {
return true;// 是否执行该过滤器,此处为true,说明需要过滤
}

@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletResponse res= ctx.getResponse();
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE ,PUT");
res.setHeader("Access-Control-Max-Age", "3600");
res.setHeader("Access-Control-Allow-Headers", "Origin, No-Cache, X-Requested-With, If-Modified-Since,"
+ " Pragma, Last-Modified, Cache-Control, Expires, Content-Type, "
+ "X-E4M-With,userId,token,Authorization,deviceId,Access-Control-Allow-Origin,Access-Control-Allow-Headers,Access-Control-Allow-Methods");
res.setHeader("Access-Control-Allow-Credentials", "true");
return null;
}
}

2.启动类ZuulApplication.java,添加如下

@Bean
public CorsFilter corsFilter(){
return new CorsFilter();
}

3.测试

重新启动,即可解决跨域问题。[图片上传失败...(image-4533bd-1553260424830)]

4.注意

  • 如果zuul配置了cors跨域问题,则不能在微服务下面在配置cors跨域,不然会继续存在跨域问题。
  • Filter是Zuul的核心,用来实现对外服务的控制。Filter的生命周期有4个,分别是“pre”、“routing”、“post”、“error”。具体请看教程:springcloud(十一):服务网关Zuul高级篇
五、网关权限

核心框架为Spring Cloud security。所有微服务经过网关zuul,进行权限控制,其中包括登陆和注销。

account微服务的接口前提:

  • GET /account/user/username/{username} 为根据用户名获取当前用户信息
  • GET /account/menu/user/{id} 为根据用户id获取当前用户权限菜单
  • POST /account/password/check 为根据输入密码和加密密码进行对比
  • GET /account/token/{token} 为根据token,获取当前用户
  • POST /account/token 为保存token和用户信息
  • POST /account/token/check/{token} 为检测token是否存在
  • DELETE /account/token/{token} 删除token

1.添加依赖pom.xml,feign用于访问微服务account

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2.添加配置

创建全局权限类:WebSecurityConfig.java

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
}

/**
*不添加权限的url过滤请求,其中开启swagger
**/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(HttpMethod.POST, "/login", "/logout")
.antMatchers("/swagger-ui.html", "/webjars/**", "/v2/**", "/swagger-resources/**")
.antMatchers("/actuator/**");

}
/**
*security基础配置
**/
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置customAccessDecisionManager
http.authorizeRequests()
//.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
.anyRequest().authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
fsi.setAccessDecisionManager(customAccessDecisionManager());
return fsi;
}
});
//替换传统的session,用token代替
http.securityContext().securityContextRepository(tokenSecurityContextRepository());
//异常信息处理
http.exceptionHandling()//
.authenticationEntryPoint(goAuthenticationEntryPoint())
.accessDeniedHandler(goAccessDeniedHandler());
//关闭csrf
http.csrf().disable();
}

@Bean
public AccessDecisionManager customAccessDecisionManager() {
return new CustomAccessDecisionManager();
}

@Bean
public GoAuthenticationEntryPoint goAuthenticationEntryPoint() {
return new GoAuthenticationEntryPoint();
}

@Bean
public GoAccessDeniedHandler goAccessDeniedHandler() {
return new GoAccessDeniedHandler();
}

@Bean
public TokenSecurityContextRepository tokenSecurityContextRepository() {
return new TokenSecurityContextRepository();
}

}

添加自定义验证类CustomAccessDecisionManager.java,用于对url过滤

/**
* 自定义验证权限
*/
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {

//decide 方法是判定是否拥有权限的决策方法
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
String url, method;
AntPathRequestMatcher matcher;
for (GrantedAuthority ga : authentication.getAuthorities()) {
if (ga instanceof CustomGrantedAuthority) {
CustomGrantedAuthority urlGrantedAuthority = (CustomGrantedAuthority) ga;
url = urlGrantedAuthority.getUrl();
method = MethodEnum.getNameByCode(urlGrantedAuthority.getMethod());
matcher = new AntPathRequestMatcher(url);
if (matcher.matches(request)) {
//当权限表权限的method为ALL时表示拥有此路径的所有请求方式权利。
if (method.equals(request.getMethod()) || "ALL".equals(method)) {
return;
}
}
}
}
throw new AccessDeniedException("denied.error.message");
}


@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}

@Override
public boolean supports(Class<?> clazz) {
return true;
}
}

创建自定义权限实体类CustomGrantedAuthority.java,自定义权限url

/**
* 自定义权限实体(Spring security)
*/
@Data
public class CustomGrantedAuthority implements GrantedAuthority {

private String url;
private Integer method;
private String authority;


@Override
public String getAuthority() {
return this.url + ";" + MethodEnum.getNameByCode(this.method);
}
}

创建自定义用户实体(Spring Security)

/**
* 自定义用户实体(Spring Security)
*/
@Data
public class CustomUserDetails implements UserDetails {
private Integer id;
private String userName;
private String password;
private String email;
private String fullName;
private Integer usable;
private List<GrantedAuthority> authorities;
private String token;
//重写
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}

@Override
public String getPassword() {
return this.password;
}

@Override
public String getUsername() {
return this.userName;
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}

创建token管理类TokenSecurityContextRepository.java,重写SecurityContext(Spring Security),token代替传统session

@Component
public class TokenSecurityContextRepository implements SecurityContextRepository {

@Autowired
AccountFeignClient accountFeignClient;

/**
* 加载用户对象
*
* @param httpRequestResponseHolder
* @return
*/
@Override
public SecurityContext loadContext(HttpRequestResponseHolder httpRequestResponseHolder) {
String token = SystemUtil.getToken(httpRequestResponseHolder.getRequest());
if (StringUtils.isEmpty(token)) {
return this.generateNewContext();
}
SigmaResponse<CustomUser> response = accountFeignClient.getToken(token);
CustomUser customUser = response.getData();
if (customUser == null) {
return this.generateNewContext();
}
CustomUserDetails user = new CustomUserDetails();
user.setUserName(user.getUsername());
BeanUtils.copyProperties(customUser, user);
List<GrantedAuthority> authorities = new ArrayList<>();
for (MenuAuth menuAuth : customUser.getAuthorities()) {
CustomGrantedAuthority authority = new CustomGrantedAuthority();
authority.setMethod(menuAuth.getMethod());
authority.setUrl(menuAuth.getUrl());
authorities.add(authority);
}
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null,
authorities);
return new SecurityContextImpl(authentication);
}

/**
* 保存用户对象
*
* @param securityContext
* @param httpServletRequest
* @param httpServletResponse
*/
@Override
public void saveContext(SecurityContext securityContext, HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse) {
}

/**
* 验证用户对象
*
* @param httpServletRequest
* @return
*/
@Override
public boolean containsContext(HttpServletRequest httpServletRequest) {
String token = SystemUtil.getToken(httpServletRequest);
if (StringUtils.isEmpty(token)) {
return false;
}
SigmaResponse<Boolean> response = accountFeignClient.checkToken(token);
return response.getData();
}

/**
* 初始化用户对象
*
* @return
*/
protected SecurityContext generateNewContext() {
return SecurityContextHolder.createEmptyContext();
}
}

创建自定义创建返回异常权限类和未登录类GoAccessDeniedHandler.java和GoAuthenticationEntryPoint.java

/**
* 授权异常,返回信息
* 未登录,会返回
*/
@Component
public class GoAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Autowired
MessageService messageService;

@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException {
response.setHeader("Content-Type", "application/json;charset=utf-8");
SigmaResponse<String> sigmaResponse = new SigmaResponse<>();
sigmaResponse.setHeader(new SigmaResponseHeader("-1", messageService.getMessage("nologin.error.message",
request), null));
response.getWriter().print(new ObjectMapper().writeValueAsString(sigmaResponse));
response.getWriter().flush();
}
}
/**
* 权限异常,返回信息
* 无菜单权限,跨角色的权限操作会报进入此类
*/
@Component
public class GoAccessDeniedHandler implements AccessDeniedHandler {
@Autowired
MessageService messageService;

@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException exception) throws IOException {
response.setHeader("Content-Type", "application/json;charset=utf-8");
SigmaResponse<String> sigmaResponse = new SigmaResponse<>();
sigmaResponse.setHeader(new SigmaResponseHeader("-2",
messageService.getMessage("denied.error.message", request), null));
response.getWriter().print(new ObjectMapper().writeValueAsString(sigmaResponse));
response.getWriter().flush();
}
}

创建自定义登陆和注销类LoginController.java

@RestController
@RequestMapping
@Api(value = "login & logout", tags = "login")
public class LoginController {
@Autowired
MessageService messageService;
@Autowired
AccountFeignClient accountFeignClient;

@ApiOperation(value = "user login")
@PostMapping("/login")
public SigmaResponse login(@RequestBody ReqUserLoginParam userLoginParam) {
if (userLoginParam == null
|| StringUtil.isEmpty(userLoginParam.getUsername())
|| StringUtil.isEmpty(userLoginParam.getPassword())) {
throw new MessageException("login.error.message");
}
SigmaResponse<UserFindResultBO> response = accountFeignClient.getUserByUsername(userLoginParam.getUsername());
UserFindResultBO user = response.getData();
//登陆验证
if (user == null) {
throw new MessageException("login.error.message");
}
SigmaResponse<Boolean> checkResult = accountFeignClient.checkPassword(
new ReqPasswordCheckParam(userLoginParam.getPassword(), user.getPassword()));
if (checkResult.getData() == null || checkResult.getData() == false) {
throw new MessageException("login.error.message");
}
if (user.getUsable() != 1) {
throw new MessageException("login.enableError.message");
}
SigmaResponse<List<MenuListForLoginBO>> menuListForLogin = accountFeignClient.getMenuListForLogin(user.getId());
List<MenuListForLoginBO> menuList = menuListForLogin.getData();
//菜单权限配置(Security框架)
List<MenuAuth> menuAuths = new ArrayList<>();
for (MenuListForLoginBO menu : menuList) {
if (menu != null && menu.getName() != null && menu.getIsMenu() == 0) {
MenuAuth authority = new MenuAuth();
authority.setUrl(menu.getUrl());
authority.setMethod(menu.getMethod());
menuAuths.add(authority);
}
}
//token生成
String token = SystemUtil.createToken();
//自定义保存redis的用户对象
CustomUser customUser = CopyUtil.copyObject(user, CustomUser.class);
customUser.setAuthorities(menuAuths);
customUser.setToken(token);
//保存到redis中
accountFeignClient.saveToken(customUser);

//自定义返回user对象
RespUserLoginParam respUserLoginParam = CopyUtil.copyObject(customUser, RespUserLoginParam.class);
// List<MenuListResultBO> boList = CopyUtil.copyList(menuList, MenuListResultBO.class);
// List treeJson = MenuUtil.createTreeJson(boList, true);
// respUserLoginParam.setMenus(treeJson);
//返回response对象
SigmaResponse<RespUserLoginParam> respUserFindParamSigmaResponse = new SigmaResponse<>();
respUserFindParamSigmaResponse.setHeader(new SigmaResponseHeader("0",
messageService.getMessage("login.seccess.message"), null));
respUserFindParamSigmaResponse.setData(respUserLoginParam);
return respUserFindParamSigmaResponse;
}

@ApiOperation(value = "user logout")
@PostMapping("/logout")
public SigmaResponse<String> login(HttpServletRequest request) {
String token = SystemUtil.getToken(request);
accountFeignClient.deleteToken(token);
SigmaResponse<String> respUserFindParamSigmaResponse = new SigmaResponse<>();
respUserFindParamSigmaResponse.setHeader(new SigmaResponseHeader("0",
messageService.getMessage("logout.success.message"), null));
return respUserFindParamSigmaResponse;
}
}

创建Feign请求类:AccountFeignClient.java,用于访问account微服务

@FeignClient("account")
public interface AccountFeignClient {
//登陆验证
@RequestMapping(value = "/account/user/username/{username}", method = RequestMethod.GET)
SigmaResponse<UserFindResultBO> getUserByUsername(@PathVariable("username") String username);

@RequestMapping(value = "/account/menu/user/{id}", method = RequestMethod.GET)
SigmaResponse<List<MenuListForLoginBO>> getMenuListForLogin(@PathVariable("id") Integer id);

@RequestMapping(value = "/account/password/check", method = RequestMethod.POST)
SigmaResponse<Boolean> checkPassword(@RequestBody ReqPasswordCheckParam reqPasswordCheckParam);

//token验证
@RequestMapping(value = "/account/token/{token}", method = RequestMethod.GET)
SigmaResponse<CustomUser> getToken(@PathVariable("token") String token);

@RequestMapping(value = "/account/token", method = RequestMethod.POST)
SigmaResponse saveToken(@RequestBody CustomUser customUser);

@RequestMapping(value = "/account/token/check/{token}", method = RequestMethod.POST)
SigmaResponse<Boolean> checkToken(@PathVariable("token") String token);

@RequestMapping(value = "/account/token/{token}", method = RequestMethod.DELETE)
SigmaResponse deleteToken(@PathVariable("token") String token);
}

3.测试

首先登陆localhost:8040/login,获取当前token,再去访问account微服务。[图片上传失败...(image-74079-1553260424830)][图片上传失败...(image-9602af-1553260424830)]

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容