原文链接:
https://www.freesion.com/article/7246187280/
背景
假设我们有很多java实现的项目,认证授权用的是shiro框架,可能还有一个sso单点登录平台
突然有一天,你的项目经理说要做微服务,可能这个项目经理并不懂什么是微服务 😂
然后,你就给了你领导很多建议,什么dubbo、什么spring cloud等等;涉及的内容可能方方面面
但是! 😅
该项目经理说:小明,你晚上加加班,花点时间来改造一下现有的项目就好了,我们现有的项目改造起来也不是很麻烦,另外,项目改造微服务不能影响原有的项目计划进度哦 😄
此时,你的心里万马奔腾 🐎🐎🐎🐎🐎🐎
目标
总的来说一句话:用最少的工作量,改造基于shiro安全框架的微服务项目,实现spring cloud + shiro 框架集成
PS:当前博客描述的方案是小编根据公司实际情况设计实现、并且在生产环境正常运行的方案,可能一些设计不太合理,又或者不满足你的需求,但是,这个方案还是有借鉴意义的。觉得有用的,给个评论点个赞鼓励下。
方案设计
整体方案设计:
- zuul网关服务,主要用于同一系统的访问出入口;
- zuul实现一个filter,用于过滤所有的请求,校验登录状态及权限;认证:校验用户是否登录;授权:校验用户是否有权限
- service-auth服务,主要实现认证、授权功能,因为所有的请求都需要经过该服务,所以集成的功能不能太多;必须要保证该模块高可用;该服务,使用feign的方式开发接口,提供给zuul调用
service-auth服务,集成shiro-redis安全框架,其他服务模块可以不用集成shiro安全框架,如果非要集成也是可以的,但是要解决shiro的会话共享问题;
认证授权流程
在网关处配置支持https协议请求,则所有的服务均可以同时支持http、https协议请求
- 优先从cookie获取会话ID,同时需要支持token参数方式校验,因为在做公众号登录的时候,要求在后端登录接口进行重定向,所以需要提供token参数确定客户端身份
- 授权功能,通过url鉴权;服务名称、请求路径、请求方式三者确定唯一;当然也可以使用requirePermissions,根据自己的需要吧,我这里是根据公司项目实际需求根据url来识别权限的。
方案实现
版本:spring boot 2.1.5.RELEASE
spring cloud Greenwich.SR2
jdk1.8以上
postgresql-10
redis-2.8.17
EUREKA注册中心
简简单单的一个注册中心,没有啥特殊的配置。
启动类 Application.java
配置 application.ymlimport 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);
}
}
• 1
• 2
• 3
• 4
• 5
• 6
• 7
• 8
• 9
• 10
• 11
详细代码,请下载附件查看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@"
• 1
• 2
• 3
• 4
• 5
• 6
• 7
• 8
• 9
• 10
• 11
• 12
• 13
• 14
• 15
• 16
• 17
• 18
• 19
• 20
• 21
• 22
• 23
• 24
• 25
• 26
• 27
ZUUL网关
在网关实现了一个AuthFilter,用于过滤所有的请求,判断是否登录、是否有权限;
支持配置免登陆请求地址、免授权地址、兼容cookie跟token参数校验等。
兼容web端登陆、小程序、公众号登录等。
启动类 Application.java
关键代码 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; /**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);
}
}
• 1
• 2
• 3
• 4
• 5
• 6
• 7
• 8
• 9
• 10
• 11
• 12
• 13
• 14
• 15
• 16
• 17
- 自定义过滤器,向下游服务请求加header认证信息.
- 与敏感头(设置向内部服务不传递哪些header正好相反),
这种方式好像不能传递名称为 Authorization,Cookie,Set-Cookie 的请求头,这三个传递不到下游服务,这三个由敏感头管理,只能传递token这种自定义的头 */ @Component public class AuthFilter extends ZuulFilter{ @Autowired(required=true) private LoginCheckApi loginCheckApi; // 请求路径白名单,不校验登录,在application-url配置 private static Set
urlSet; // 请求资源类型白名单,不校验登录,在application-url配置 private static Set 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
> requestQueryParams = ctx.getRequestQueryParams(); if (requestQueryParams==null) { requestQueryParams=new HashMap<>();
} //将要新增的参数添加进去,被调用的微服务可以直接 去取,就想普通的一样,框架会直接注入进去 ArrayList
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 urlSet) { this.urlSet = urlSet; } @Value(“${whitelist.fileset}”) public void setFileSet(Set fileSet) { this.fileSet = fileSet; } } • 1 • 2 • 3 • 4 • 5 • 6 • 7 • 8 • 9 • 10 • 11 • 12 • 13 • 14 • 15 • 16 • 17 • 18 • 19 • 20 • 21 • 22 • 23 • 24 • 25 • 26 • 27 • 28 • 29 • 30 • 31 • 32 • 33 • 34 • 35 • 36 • 37 • 38 • 39 • 40 • 41 • 42 • 43 • 44 • 45 • 46 • 47 • 48 • 49 • 50 • 51 • 52 • 53 • 54 • 55 • 56 • 57 • 58 • 59 • 60 • 61 • 62 • 63 • 64 • 65 • 66 • 67 • 68 • 69 • 70 • 71 • 72 • 73 • 74 • 75 • 76 • 77 • 78 • 79 • 80 • 81 • 82 • 83 • 84 • 85 • 86 • 87 • 88 • 89 • 90 • 91 • 92 • 93 • 94 • 95 • 96 • 97 • 98 • 99 • 100 • 101 • 102 • 103 • 104 • 105 • 106 • 107 • 108 • 109 • 110 • 111 • 112 • 113 • 114 • 115 • 116 • 117 • 118 • 119 • 120 • 121 • 122 • 123 • 124 • 125 • 126 • 127 • 128 • 129 • 130 • 131 • 132 • 133 • 134 • 135 • 136 • 137 • 138 • 139 • 140 • 141 • 142 • 143 • 144 • 145 • 146 • 147 • 148 • 149 • 150 • 151 • 152 • 153 • 154 • 155 • 156 • 157 • 158 • 159 • 160 • 161 • 162 • 163 • 164 • 165 • 166 • 167 • 168 • 169 • 170 • 171 • 172 • 173 • 174 • 175 • 176 • 177 • 178 • 179 • 180 • 181 • 182 • 183 • 184 • 185 • 186 • 187 • 188 • 189 • 190 • 191 • 192 • 193 • 194 • 195 • 196 • 197 • 198 • 199 • 200 • 201 • 202 • 203 • 204 • 205 • 206 • 207 • 208 • 209 • 210 • 211 • 212 • 213 • 214 • 215 • 216 • 217 • 218 • 219 • 220 • 221 • 222 详细代码,请下载附件查看
<a name="zN0TW"></a>
## AUTH认证授权
用户登录认证、访问授权、会话管理等;开放接口给gateway的AuthFilter使用;<br />关键代码
/**
权限判断接口:先查询到资源对应的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
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
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; } • 1 • 2 • 3 • 4 • 5 • 6 • 7 • 8 • 9 • 10 • 11 • 12 • 13 • 14 • 15 • 16 • 17 • 18 • 19 • 20 • 21 • 22 • 23 • 24 • 25 • 26 • 27 • 28 • 29 • 30 • 31 • 32 • 33 • 34 • 35 • 36 • 37 • 38 • 39 • 40 • 41 • 42 • 43 • 44 • 45 • 46 • 47 • 48 • 49 • 50 • 51 • 52 • 53 • 54 • 55 • 56 • 57 • 58 • 59 • 60 • 61 • 62 • 63 • 64 • 65 • 66 • 67 • 68 • 69 • 70 • 71 • 72 • 73 ```
- 认证服务,主要通过
服务名称+请求方式+请求url
来判定唯一的权限,比如post/service-demo1/user/getSystemUserInfo
其中post是请求方式,service-demo1是服务名称,/user/getSystemUserInfo是接口请求路径;兼容如{id}
由@PathVariable注解标注的请求。 当然也可以使用shiro的权限编码方式,如
user:getSystemUserInfo
,但是使用这种方式,需要处理好多个服务权限集中管理,权限编码不能冲突的问题。
详细代码,请下载附件查看相关截图
project结构:auth:认证服务;auth-api:使用feign开放的接口声明;demo:微服务项目demo,不集成shiro;demo1:微服务项目demo,集成shiro;shiro:独立出来的shiro模块,供其他模块使用。
- 项目运行注册中心截图:
- 接口调用演示:
- auth服务接口文档,需要先登录才能打开:
其他说明
- 子系统后端开发过程中,不需要将服务注册eureka;提交前端对接、或者测试部署服务器的时候注册即可
因为如果每位后端开发,都将服务组册到eureka,如果服务名称相同,在服务端会产生负载均衡,访问的接口,不一定是本地的接口,也可能是别人的接口
开发过程不控制权限,发布测试环境后,统一管理权限;仅需关注如何获取登录用户信息即可 - 集成shiro
自行实现登录接口,产生本地会话,从而实现获取用户信息;有现成案例参考,复制粘贴即可,几乎不用考虑工作量问题;
部署的时候,使用redis缓存共享会话即可;
具体实现,请查看示例项目代码demo1 - 不集成shiro
网关校验登录成功之后,转发请求的过程,会把用户登录信息携带转发;具体的服务项目,直接通过参数名称获取即可;
具体实现,请查看示例项目代码demo代码下载地址
spring cloud + shiro集成方案.zip
版权声明:本文为weixin_42686388原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_42686388/article/details/103084289