一、梳理逻辑
之前的认证中心作为一个单体项目时,过滤器认证成功后,是直接放行然后访问方法。
但在微服务的情况下
- 在网关添加过滤器
- 在过滤器中获取到请求的JWT与url,通过openFeign将JWT与url发送给认证中心
- 在ShiroConfig中将该请求放行,该方法用于鉴权,不能被拦截
- 放行后进入AuthController中对应的方法,将url拼接成需要访问的uri,将JWT放入头信息,通过RestTemplate发出请求,该请求就会进入认证中心的过滤器中过滤(举例:JWTFilter,MyPermissionsAuthorizationFilter自定义过滤器)
- 修改过滤器中逻辑,判断当前过滤器是否是最后一个过滤器,如果不是,则权限认证通过时直接放行至下一个过滤器,如果是最后一个过滤器,则返回false,携带认证通过的信息返回给网关,网关执行chain.filter(exchange)方法后进入微服务执行方法
- 此时验证通过的话,如果url中的请求本来就是访问认证中心的其他服务(例如获取菜单),过滤器就会重新拦截一个请求,但请求中不包含localhost,而是另一个ip地址,所以我们在判断出最后一个过滤器中,还要加一层逻辑,第二次请求(即网关执行chain.filter(exchange)后进入认证中心的请求)就放行,让其访问认证中心中的方法
二、网关代码
2.1 配置文件application.yml
server:
port: 9005
spring:
application:
name: ticket-gateway
cloud:
gateway:
routes:
- id: users
uri: lb://ticket-user
predicates:
- Path=/user/**
- id: orders
uri: lb://ticket-order
predicates:
- Path=/order/**
- id: auth
uri: lb://ticket-auth
predicates:
- Path=/perm/**
logging:
level:
root: info
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
2.2 AuthService
import com.woniuxy.ticket.commons.entity.ResponseResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 访问认证服务
*/
@FeignClient("ticket-auth")
public interface AuthService {
@GetMapping("/auth")
ResponseResult parseJwt(@RequestParam("jwt") String jwt,@RequestParam("url") String url);
}
2.3 AuthFilter
import com.woniuxy.ticket.commons.entity.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
/**
* 获取到请求的JWT与url,通过openFeign将JWT与url发送给认证中心
*/
@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Resource
private AuthService authService;
/**
* 执行全局过滤器的业务逻辑
* @param exchange 请求/响应的上下文对象
* @param chain 过滤器链
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("Auth Filter");
String url = exchange.getRequest().getURI().getPath();
String jwt = exchange.getRequest().getHeaders().getFirst("jwt");
ResponseResult responseResult = authService.parseJwt(jwt,url);
if(responseResult.getStatusCode() == 403){
return authErro(exchange.getResponse(),responseResult.getMessage());
}else if(responseResult.getStatusCode() == 405){
return authErro(exchange.getResponse(),responseResult.getMessage());
}else {
return chain.filter(exchange);
}
}
private Mono<Void> authErro(ServerHttpResponse resp, String mess) {
resp.setStatusCode(HttpStatus.UNAUTHORIZED);
resp.getHeaders().add("Content-Type","application/json;charset=UTF-8");
DataBuffer buffer = resp.bufferFactory().wrap("error".getBytes(StandardCharsets.UTF_8));
return resp.writeWith(Flux.just(buffer));
}
/**
* 定义过滤器执行顺序
* 值越小,越先执行
* @return
*/
@Override
public int getOrder() {
return -100;
}
}
三、认证中心代码
3.1 放行鉴权方法
PermMapFactoryBean中添加:
map.put("/auth","anon");
3.2 AuthController
import com.woniuxy.ticket.commons.entity.ResponseResult;
import org.apache.http.client.utils.URIBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.net.URI;
import java.net.URISyntaxException;
/**
* 接收网关的请求
*/
@RestController
public class AuthController {
@Value("${server.port}")
private int port;
//shiro配置类中放行该方法,所以通过网关请求过来不会经过过滤器
@RequestMapping("auth")
public ResponseResult<Void> auth(String jwt, String url) throws URISyntaxException {
//发送HTTP请求到本项目,会经过Shiro过滤器
URI uri = new URIBuilder().setScheme("http").setHost("localhost").setPort(port).setPath(url).build();
// build http headers
HttpHeaders headers = new HttpHeaders();
headers.add("jwt", jwt);
RestTemplate restTemplate = new RestTemplate();
// send request and get response
ResponseResult result = restTemplate.postForObject(uri, new HttpEntity<String>(headers), ResponseResult.class);
return result;
}
}
3.3 修改过滤器逻辑
将过滤器逻辑封装为工具类,判断时调用isLastFilter来判断是否为最后一个过滤器
FilterUtil
import com.fasterxml.jackson.databind.ObjectMapper;
import com.woniuxy.ticket.commons.entity.ResponseResult;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.springframework.context.ApplicationContext;
import javax.servlet.Filter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Map;
public class FilterUtil {
/**
* 用于判断过滤器是否为过滤链中的最后一个
* @param request
* @param response
* @param applicationContext
* @param clazz
* @return
* @throws IOException
*/
public static boolean isLastFilter(ServletRequest request, ServletResponse response, ApplicationContext applicationContext, Class clazz) throws IOException {
//根据request获取请求uri
HttpServletRequest req = (HttpServletRequest)request;
String uri = req.getRequestURI();
//获取该uri需要经过的过滤器
ShiroFilterFactoryBean shiroFilterFactoryBean = applicationContext.getBean(ShiroFilterFactoryBean.class);
Map<String, String> filterMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();
//根据过滤器名找到具体的过滤器的类
String filters = filterMap.get(uri);
//获取需要经过的最后一个过滤器key
String[] strs = filters.replaceAll("(\\[(.*?)])", "").split(",");
String lastFilter = strs[strs.length -1];
//查询所有过滤器,通过key找到过滤器的value对应的类
Map<String, Filter> map = shiroFilterFactoryBean.getFilters();
Filter filter = map.get(lastFilter);
String filterClassName = filter.getClass().getName();
//判断该类是否为最后一个过滤器
if(clazz.getName().equals(filterClassName)){
//访问认证中心中的业务时,第一次进来时,url中请求有localhost,我们返回false
//访问认证中心中的业务时,第二次进来时,是网关鉴权完毕,我们就放行进入该微服务,返回true
if(req.getRequestURL().toString().contains("localhost")){
ResponseResult<Void> result = new ResponseResult<>(200, "Last Filter");
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter().write(objectMapper.writeValueAsString(result));
return false;
}else {
return true;
}
}else{
//如果不是最后一个,返回true放行至下一个过滤器
return true;
}
}
}
JwtFilter
import com.fasterxml.jackson.databind.ObjectMapper;
import com.woniuxy.ticket.commons.entity.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.context.ApplicationContext;
import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* 自定义过滤器,用于JWT认证的校验
*
*/
@Slf4j
public class JwtFilter extends AuthenticatingFilter {
@Resource
private ApplicationContext applicationContext;
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
String jwtToken = ((HttpServletRequest) request).getHeader("jwt");
return new JwtToken(jwtToken);
}
//判断验证是否通过
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (this.isLoginRequest(request, response)) {
return true;
}
boolean allowed = false;
try {
allowed = executeLogin(request, response);
} catch (IllegalStateException e) { // not found any token
log.error("Not found any token");
} catch (Exception e) {
log.error("Error occurs when login", e);
}
response.setContentType("application/json");
try {
if (allowed) {
return FilterUtil.isLastFilter(request,response,applicationContext,this.getClass());
} else {
ResponseResult<Void> result = new ResponseResult<>(403, "JWT Not Allowed");
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter().write(objectMapper.writeValueAsString(result));
}
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
//onPreHandle中,若请求的路径非该filter中配置的拦截路径,则直接返回true进行下一个filter
//我们需要重写,通过isAccessAllowed进行判断
@Override
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return isAccessAllowed(request, response, mappedValue);
}
//为JWT自动续期
//未实现
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
return super.onLoginSuccess(token, subject, request, response);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
ResponseResult<Void> result = new ResponseResult<>(403, "会话失效,重新登录");
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter().write(objectMapper.writeValueAsString(result));
return false;
}
}
MyPermissionsAuthorizationFilter
package com.woniuxy.ticket.auth.shiro;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.woniuxy.ticket.commons.entity.ResponseResult;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;
import org.springframework.context.ApplicationContext;
import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
/**
* 自定义过滤器
*/
public class MyPermissionsAuthorizationFilter extends PermissionsAuthorizationFilter {
@Resource
private ApplicationContext applicationContext;
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
Subject subject = getSubject(request, response);
String[] perms = (String[]) mappedValue;
boolean isPermitted = true;
if (perms != null && perms.length > 0) {
if (perms.length == 1) {
if (!subject.isPermitted(perms[0])) {
isPermitted = false;
}
} else {
if (!subject.isPermittedAll(perms)) {
isPermitted = false;
}
}
}
response.setContentType("application/json");
try {
if (isPermitted) {
return FilterUtil.isLastFilter(request,response,applicationContext,this.getClass());
} else {
ResponseResult<Void> result = new ResponseResult<>(405, "Perm Not Allowed");
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter().write(objectMapper.writeValueAsString(result));
}
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return isAccessAllowed(request, response, mappedValue);
}
}
3.4 PermController获取菜单方法
import com.woniuxy.ticket.auth.entity.dto.UserRoleDto;
import com.woniuxy.ticket.auth.jwt.Audience;
import com.woniuxy.ticket.auth.jwt.JwtUtil;
import com.woniuxy.ticket.auth.service.UserService;
import com.woniuxy.ticket.commons.entity.ResponseResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/perm")
public class PermController {
@Resource
private Audience audience;
@Resource
private UserService userService;
@GetMapping("/list")
public ResponseResult list(@RequestHeader("jwt") String jwt){
if(JwtUtil.parseJwt(jwt,audience.getBase64Secret())){
int id = JwtUtil.getUserId(jwt,audience.getBase64Secret());
UserRoleDto userRoleDto = userService.findUserMenus(id);
return new ResponseResult().success(userRoleDto);
}
return new ResponseResult().error();
}
}
四、其他问题
4.1 中文乱码
在网关pom.xml中导入插件依赖
原因:maven运行环境问题,主要原因是使用了spring boot的maven插件,以spring:run运行的项目,需要在插件中添加运行的编码配置:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<jvmArguments>-Dfile.encoding=UTF-8</jvmArguments>
</configuration>
</plugin>