This action will force synchronization from yuxue/micro-svc, which will overwrite any changes that you have made since you forked the repository, and can not be recovered!!!
Synchronous operation will process in the background and will refresh the page when finishing processing. Please be patient.
spring cloud + shiro框架; 博客地址: 手把手教你集成spring cloud + shiro微服务框架
假设我们有很多java实现的项目,认证授权用的是shiro框架,可能还有一个sso单点登录平台
突然有一天,你的项目经理说要做微服务
然后,你就给了你领导很多建议,什么dubbo、什么spring cloud等等;涉及的内容可能方方面面
但是! 该项目经理说:小明,你晚上加加班,花点时间来改造一下现有的项目就好了,我们现有的项目改造起来也不是很麻烦,另外,项目改造微服务不能影响原有的项目计划进度哦 此时,你的心里万马奔腾
总的来说一句话:用最少的工作量,改造基于shiro安全框架的微服务项目,实现spring cloud + shiro 框架集成 PS:当前博客描述的方案是小编根据公司实际情况设计实现、并且在生产环境正常运行的方案,可能一些设计不太合理,又或者不满足你的需求,但是,这个方案还是有借鉴意义的。觉得有用的,给个评论点个赞鼓励下。
版本:spring boot 2.1.5.RELEASE
spring cloud Greenwich.SR2
jdk1.8以上
postgresql-10
redis-2.8.17
简简单单的一个注册中心,没有啥特殊的配置。 启动类 Application.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
配置 application.yml
server:
port: 7001
spring:
application:
name: eureka
main:
allow-bean-definition-overriding: true
eureka:
instance:
prefer-ip-address: true
#hostname: svc-eureka #eureka服务端的实例名称
instance-id: ${spring.cloud.client.ip-address}:${server.port}
server:
enable-self-preservation: false ## 中小规模下,自我保护模式坑比好处多,所以关闭它
#renewal-threshold-update-interval-ms: 120000 ## 心跳阈值计算周期,如果开启自我保护模式,可以改一下这个配置
eviction-interval-timer-in-ms: 5000 ## 主动失效检测间隔,配置成5秒
use-read-only-response-cache: false ## 禁用readOnlyCacheMap
client:
register-with-eureka: false #false表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
defaultZone: http://${spring.cloud.client.ip-address}:${server.port}/eureka/
info:
app.name: eureka
company.name: test.com
build.artifactId: "@project.artifactId@"
build.version: "@project.version@"
详细代码,请下载附件查看
在网关实现了一个AuthFilter,用于过滤所有的请求,判断是否登录、是否有权限; 支持配置免登陆请求地址、免授权地址、兼容cookie跟token参数校验等。 兼容web端登陆、小程序、公众号登录等。 启动类 Application.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
@EnableFeignClients
@EnableEurekaClient
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
关键代码 AuthFilter.java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.alibaba.fastjson.JSONObject;
import com.fundway.auth.api.LoginCheckApi;
import com.google.common.collect.Maps;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
/**
* 自定义过滤器,向下游服务请求加header认证信息.
* 与敏感头(设置向内部服务不传递哪些header正好相反),
* 这种方式好像不能传递名称为 Authorization,Cookie,Set-Cookie 的请求头,这三个传递不到下游服务,这三个由敏感头管理,只能传递token这种自定义的头
*/
@Component
public class AuthFilter extends ZuulFilter{
@Autowired(required=true)
private LoginCheckApi loginCheckApi;
// 请求路径白名单,不校验登录,在application-url配置
private static Set<String> urlSet;
// 请求资源类型白名单,不校验登录,在application-url配置
private static Set<String> fileSet;
@Override
public String filterType() {
//pre型过滤器,路由到下级服务前执行
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
//优先级,数字越大,优先级越低
return 0;
}
@Override
public boolean shouldFilter() {
//是否执行该过滤器,true代表需要过滤
return true;
}
/**
* 过滤逻辑
* pre过滤器在route过滤前执行,RequestContext负责通信包含了请求等信息,debug发现,context.addZuulRequestHeader,
* 但在RibbonRoutingFilter 这个向下游服务发起请求的路由过滤器,自定义的header没有添加上。
* RibbonRoutingFilter是默认的过滤器,run方法可以看到,逻辑是从原来的RequestContext生产新的RibbonCommandContext发起请求
* @return
* @throws ZuulException
*/
@Override
public Object run() {
//Zull的Filter链间通过RequestContext传递通信,内部采用ThreadLocal 保存每个请求的信息,
//包括请求路由、错误信息、HttpServletRequest、response等
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = this.getHttpServletRequest();
// option请求,直接放行
if (request.getMethod().equals(RequestMethod.OPTIONS.name())) {
return null;
}
// 判断需要放行的url或者静态资源文件
String url = request.getRequestURI();
String end = "";
if(url.lastIndexOf("/") >= 0 ) { // 判断需要放行的请求
end = url.substring(url.lastIndexOf("/"));
if(urlSet.contains(end)) {
return null;
}
}
if(end.lastIndexOf(".") > 0) { //判断需要放行的静态文件
end = end.substring(end.lastIndexOf(".") + 1);
if(fileSet.contains(end)) {
return null;
}
}
// 获取到用户的Token
String cookie = request.getHeader("Cookie"); //获取到 JSESSIONID=值
if(StringUtils.isEmpty(cookie)) {
cookie = "";
}
String token = ctx.getRequest().getParameter("token"); //获取到 值
// 处理微信公众号登录业务,后端会重定向,生成的cookie是一个无效cookie,而后端重定向,又不能把有效cookie写到客户端
if(!StringUtils.isEmpty(token) && !"undefined".equals(token) && !cookie.contains(token)) {
cookie = "JSESSIONID=" + (ctx.getRequest().getParameter("token"));
}
if(StringUtils.isEmpty(token)) { // 参数未空或者null的话,feign调用的接口会报错!!坑比
token = "";
}
//过滤该请求,不往下级服务去转发请求,到此结束
if(StringUtils.isEmpty(cookie)) { // 会报跨域问题
this.setCORS(ctx);
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(200);
Map<String, Object> result = Maps.newHashMap();
result.put("code", 401);
result.put("msg", "未登录");
result.put("obj", "来自网关的消息:未获取到有效的Token");
result.put("success", false);
ctx.setResponseBody(JSONObject.toJSONString(result));
ctx.getResponse().setContentType("text/html;charset=UTF-8");
return null;
}
// 增加请求头
ctx.addZuulRequestHeader("Cookie", cookie);
// 调用统一认证接口,判断是否登录 && 判断是否有功能权限
// 优先校验cookie,,不通过则校验token //cookie从request里面拿
Object check = loginCheckApi.checkPermission(token, this.getUrl(request));
if(check instanceof HashMap) {
HashMap<String, Object> result = (HashMap) check;
if(Boolean.parseBoolean(result.get("success").toString())) {
// 添加序列化之后的用户信息
// 白名单url的请求,不能获取到该信息
setReqParams(ctx, request, "userEntity", result.get("obj").toString());
return null;
}
this.setCORS(ctx);
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(200);
// 权限校验接口异常
ctx.setResponseBody(JSONObject.toJSONString(check));
ctx.getResponse().setContentType("text/html;charset=UTF-8");
return null;
} else {
this.setCORS(ctx);
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(200);
Map<String, Object> result = Maps.newHashMap();
result.put("code", 401);
result.put("msg", "无权限");
result.put("obj", "来自网关的消息:该用户无当前请求权限");
result.put("success", false);
ctx.setResponseBody(JSONObject.toJSONString(result));
ctx.getResponse().setContentType("text/html;charset=UTF-8");
return null;
}
}
private String getUrl(HttpServletRequest request) {
// 获取到请求的相关数据 uri是斜杠开头
String uri = request.getRequestURI().toLowerCase().replaceAll("//", "/");
String method = request.getMethod().toLowerCase();
return method.concat(uri);
}
private HttpServletRequest getHttpServletRequest() {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
return request;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static void setReqParams(RequestContext ctx, HttpServletRequest request, String key, String value) {
// 一定要get一下,下面这行代码才能取到值... [注1]
request.getParameterMap();
Map<String, List<String>> requestQueryParams = ctx.getRequestQueryParams();
if (requestQueryParams==null) {
requestQueryParams=new HashMap<>();
}
//将要新增的参数添加进去,被调用的微服务可以直接 去取,就想普通的一样,框架会直接注入进去
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add(value);
requestQueryParams.put(key, arrayList);
ctx.setRequestQueryParams(requestQueryParams);
}
private void setCORS(RequestContext ctx) {
//处理跨域问题
HttpServletRequest request = ctx.getRequest();
HttpServletResponse response = ctx.getResponse();
// 这些是对请求头的匹配,网上有很多解释
response.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Credentials","true");
response.setHeader("Access-Control-Allow-Methods","GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH");
response.setHeader("Access-Control-Allow-Headers","authorization, content-type");
response.setHeader("Access-Control-Expose-Headers","X-forwared-port, X-forwarded-host");
response.setHeader("Vary","Origin,Access-Control-Request-Method,Access-Control-Request-Headers");
}
@Value("${whitelist.urlset}")
public void setUtlSet(Set<String> urlSet) {
this.urlSet = urlSet;
}
@Value("${whitelist.fileset}")
public void setFileSet(Set<String> fileSet) {
this.fileSet = fileSet;
}
}
详细代码,请下载附件查看
用户登录认证、访问授权、会话管理等;开放接口给gateway的AuthFilter使用; 关键代码
/**
* 权限判断接口:先查询到资源对应的id,然后根据用户权限判断
*/
@Override
public Result checkPermission(HttpServletRequest request, String cookie, String checkUrl) {
UserEntity user = this.getUserInfo(request, cookie);
if(null == user || user.getId() <=0) {
return Result.error("未登录", 401);
}
// 获取用户功能权限ID集合
Set<Integer> permissionSet = user.getPermissionId();
// 减少放到请求中的属性
user.setPermission(null);
user.setPermissionId(null);
// 获取微服务名称
String[] str = checkUrl.split("/");
String module = str[1];
// 判断是否是免校验资源
if(this.getIdByUrl(unCheckResMap.get(module), checkUrl) > 0) {
return Result.ok(JSONObject.toJSONString(user));
}
// 用户完全没有权限, 且请求资源不是开放资源
if(null == permissionSet || permissionSet.size() <= 0) {
log.info("当前用户未分配权限:" + user.getLoginName());
return Result.error("无权限", 401);
}
// 获取系统指定模块资源
Integer resId = this.getIdByUrl(resMap.get(module), checkUrl);
// 系统没有配置该权限,或者请求路径不存在
if(resId <= 0 && isPass) {
// log.info("系统没有配置该资源对应的权限, 但是配置放行:" + uri);
return Result.ok(JSONObject.toJSONString(user));
}
// 系统配置了权限
if(permissionSet.contains(resId)) {
return Result.ok(JSONObject.toJSONString(user));
}
return Result.error("无权限", 401);
}
public Integer getIdByUrl(HashMap<String, Integer> value, String url) {
Integer result = 0;
if(null != value && value.size() > 0) {
Set<Integer> resultSet = Sets.newHashSet();
if(value.containsKey(url)) {
result = value.get(url);
} else {
// 遍历,匹配,处理@PathVariable注解的请求
value.entrySet().forEach(entry -> {
String key1 = entry.getKey();
if(key1.contains("{")) {
AntPathMatcher matcher = new AntPathMatcher();
if(matcher.match(key1, url)) {
resultSet.add(entry.getValue());
}
}
});
}
if(resultSet.size() > 0) {
result = resultSet.stream().findFirst().get();
}
}
return result;
}
服务名称+请求方式+请求url
来判定唯一的权限,比如post/service-demo1/user/getSystemUserInfo
其中post是请求方式,service-demo1是服务名称,/user/getSystemUserInfo是接口请求路径;兼容如 {id}
由@PathVariable注解标注的请求。user:getSystemUserInfo
,但是使用这种方式,需要处理好多个服务权限集中管理,权限编码不能冲突的问题。
详细代码,请下载附件查看
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。