代码演示基于springboot
任何一个应用系统都离不开登录认证过程。实现登录认证主要目的是对系统的权限管理。
在单应用单节点下常用做法通常采用session认证机制。其主要流程如下:
public Map<String ,String > login(String username,String password){
Map<String,String> result = new HashMap<>();
if ("admin".equals(username) && "123456".equals(password)){
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
// 将用户信息存储至session中
HttpSession session = request.getSession();
session.setAttribute("username",username);
session.setAttribute("password",password);
session.setMaxInactiveInterval(0);
result.put("code","200");
result.put("message","success");
return result;
}
result.put("code","500");
result.put("message","error");
return result;
}
在验证用户名和密码之后,将用户信息放入session中进行存储。
单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
单点登录,主要分为两个场景。单应用集群部署场景和多应用互信访问。
单应用集群部署
主要是我们这有一个应用程序,比如只有淘宝一个网站,但是这个应用程序后台服务是集群部署,在集群后就我们后台每一个节点部署的应用之间需要实现一次登录后,它们之间都互信。
多应用互信
多应用,比如我们又淘宝和天猫两个系统。两个系统之间需要实现互信登录。即我们在浏览器端登录了淘宝,那么访问天猫系统时也应该自动登录成功。
从登录流程来看。都是登录时由后台交给前端一个sessionID放入到cookie中,前端携带cookie到后台进行获取已有session进行认证。因此在解决单点登录时主要分为以下两个方向:
原理: 当用户登录成功后,将session信息发送至服务的每个节点。(可通过http请求)
缺点:
同一个session在每一个服务器都需要存储一次。占用服务器内存资源
通过网络请求发送session进行复制。
原理: 采用ip_hash的策略保证一台设备发起的请求全都绑定到一台服务器上,这样它所有的认证及操作其实都在一个服务上完成了。
缺点:
只能解决单应用(集群)的场景。对于多应用程序的单点登录并不能实现。
采用ip_hash可能导致大量的ip同时分配到一台服务器上,从而使负载均衡失效。
对于网络存在路由的情况下,同一个路由下的所有设备访问过来,nginx获取到的ip均是同一个ip
原理: 自定义session存储。访问登录时,自定义session实现机制,将session存储到存储服务redis,memecache等缓存或database中) 同时为每一个session生成一个唯一id响应给前端。可放入cookie中, 浏览器请求时自动携带到后台,通过到存储服务获取共享的session。
缺点:
只能解决单应用(集群)的场景。对于域名完全不同的多应用程序的单点登录并不能实现。因为cookie是不允许跨域的。
关于自定义session实现完全跨域。也可以在前端实现cookie的跨域,或者后台将生成的id不用cookie存储而是直接响应给前端存储于localstorage中。然后前端实现localStorage的跨域以达到完全跨域单点登录的场景。
spring-session:即为上述自定义session的一种实现机制。它通过自己产生的cookie中存储一个名为SESSION 的id。并在后台将session自动放入了redis缓存中。
缺点: spring-session是不允许前端跨域的。即它只是解决单应用程序的集群环境下session的共享。
引入如下依赖:
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
application.properties中配置
spring.security.user.name=admin
spring.security.user.password=123456
# 该用户名和密码可以迁移为使用数据库中用户名和密码方式
启动项目后就会自动拦截登录请求进入springboot默认登录界面:
登录后存储于redis中的session信息:
查看cookie的域名等信息可以看到,其不允许前端跨域的。
其登录界面可以通过自定义登录界面。配置后实现访问项目自行定制登录界面。用户和密码等自定义数据获取可参考spring security的使用
以上方案都是通过session管理的方式来实现单点登录。但是通过session的方式,当大量用户登录后,后台服务会产生大量的session存储到存储服务中。占用大量的存储资源,但是在用户登录不多时这些存储资源基本处于空闲。造成了存储服务资源极大的浪费。为解决这种浪费从而现在采用的比较优秀的技术方案就是token机制。
token机制原理:
当用户登录成功之后,后台通过对一些认证和权限等信息进行加密为一串密文形式的token字符串。即:直接在token中存储认证相关信息。然后将token响应给客户端。由客户端自行保存。这样就把token的存储完全分发到了个客户机上。从而释放了大量的服务器资源。当用户发起其余请求时。后台服务按照指定的加解密方式对token进行解密和验证即可。
缺点:
token由于是交由前端保存的一个字符串。因此不适于在token里面存储过多的信息。尽可能简洁为主。
其次就是,与session对比之下。后台需要实现一套安全对token进行加解密的工具。
JWT: 即是一套当前比较主流的token机制解决方案的实现。
由于http请求时无状态的。而cookie产生的目的就是为了存储服务端的状态信息。该文件与特定的web文档相关联。保存了客户机访问文档时的相关信息。帮助我们记录用户信息等功能。
通过浏览器窗口查看到cookie信息:
从浏览器上可以看到其属性主要包含:name,value,Domain(所属域),Path,Expires,size,httpOnly,secure,sameSite,Priorty
属性 | 说明 |
---|---|
name | cookie 名(比如SSECITONID) |
value | 存储的值,name/value共同形成一个键值对 |
Domain | 所属的域。受浏览器同源策略的影响,同一个域下的cookie只允许访问当前域下的路径时才会被访问或携带。 |
Path | cookie允许访问的路径。比如: "/"表示允许当前域下的所有路径访问。 “/admin”:表示必须为domain/admin/**下的所有路径才能够携带当前cookie |
Expires | cookie的有效期,通常后端创建的cookie有效期默认为浏览器关闭后失效 |
size | 表明了cookie的大小 |
httpOnly | 设置为true,那么通过js脚本将无法读取当前cookie。可有效防止XSS攻击 |
secure | 是否使用安全协议传输,可以防止信息在传递过程中被监听捕获后导致信息泄露,如果设置为true,可以限制只有通过https访问时才会将浏览器保存的cookie传递后端。 |
sameSite | 可以用来防止CSRF攻击和用户追踪 |
Priorty | 在浏览器cookie数量超过限制时,可按照优先级等从低到高开始移除 |
Cookie cookie = new Cookie("auth",username);
cookie.setPath("/");
cookie.setDomain("user.com");
cookie.setMaxAge(-1);
cookie.setHttpOnly(true);
cookie.setSecure(true);
response.addCookie(cookie);
同源策略是一种浏览器安全功能,它限制同一个来源上的文档和脚本与另一个资源上进行交互的过程。
同源: 当协议,主机,端口都相同时,就认为是同源。比如:
# 同源
http://www.example.com/foo 与 http://www.example.com/bar。
# 不同源
http://www.example.com/foo 与 https://www.example.com/foo 因为协议不相同
详细参考文档:https://web.dev/same-origin-policy/
主要限制行为:
cookie、localStorage和IndexDB无法读取
DOM 无法获得
AJAX请求不能发送
在同源策略的限制下,单点登录在多域名下不同应用之间就需要解决这些跨域问题
由于cookie是可以跨一级域名,也就是同一个一级域名下的cookie是可以共享的。但是localStorage、IndexDB无法读取。
因此可以设定cookie的一级域名来实现单点登录。
cookie.setDomain("taobao.com"); // 设置后一级域名user.com下都可以访问
作如上设置后,www.user.taobao.com和www.order.taobao.com之间都可以共用同一个cookie了。但是该方式要求必须在同一个一级域名下。
实现www.taobao.com和www.tianmao.com之间的跨域方案:jsonp、nodejs superagent 等
localStorage是完全不允许跨域的,即使是同一个一级域名下。
跨域方案:
postMessage和iframe
比如a,b两个域。可以通过在a中使用iframe包含入b的页面。然后利用postMessage将localStorage数据发送至b的页面。在b中利用window.onmessage进行监听
CORS跨域资源共享等
基于以上跨域解决方案,在前端接收到cookie或者token时可以采用以上方案进行保存。后续请求进行携带即可
在前后端分离项目下。对于浏览器来说前端会公布一个页面访问地址,而后台api还会有一个地址。前端页面通过比如axios等发起api的调用就会造成后台的跨域请求问题。
通常控制台会有from origin ‘http://localhost:8080’ has been blocked by CORS policy:xxx的错误
通过定义Filter来设置跨域请求
public class SimpleCORSFilter implements Filter {
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
// 所有请求和返回都会被过滤器所拦截
HttpServletResponse response = (HttpServletResponse) res;
// 允许跨域访问的域名
response.addHeader("Access-Control-Allow-Origin", "*");
// 允许执行的方法
response.addHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS,DELETE");
// 请求缓存的时间
response.addHeader("Access-Control-Max-Age", "3600");
// 实际请求中允许携带的首部字段
response.addHeader("Access-Control-Allow-Headers", "x-requested-with");
// 放行,递交给下一个过滤器
chain.doFilter(req, res);
}
@Override
public void init(FilterConfig arg0) throws ServletException {
}
}
在web.xml中注册此过滤器
<filter>
<filter-name>simpleCORSFilterfilter-name>
<filter-class>com.ksec.filter.SimpleCORSFilterfilter-class>
<init-param>
<param-name>param-name>
<param-value>param-value>
init-param>
filter >
<filter-mapping>
<filter-name>simpleCORSFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 设置允许跨域请求
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("*")
.allowedHeaders("*")
.maxAge(3600);
}
}
通过自定义的webconfig来注册跨域请求,以及增加一些拦截器等。
方法说明:
方法 | 说明 | 配置示例 |
---|---|---|
addMapping | 对指定的路径模式启用跨域请求处理。配置示例: | /admin或者/admin/** |
allowedOrigins | 特定来源的允许来源列表 | http://domain.com或者 * 表示所有 |
allowCredentials | 是否允许用户发送、处理 cookie | true/false |
allowedMethods | 设置允许的http methods | GET,POST或者 *表示所有方法 |
allowedHeaders | 设置预检请求可以列出的标头列表 | * 表示允许所有 |
maxAge(3600) | 请求缓存时间 | 单位是 秒 |
以上讨论了如何实现多网站之间的认证解决方案、以及前端实现跨域的方案。但针对于一些企业级对权限要求较高的系统下。通常需要集中授权管理。
对于权限的管理在两个不同的网站之间还是存在较大差异的。通常就需要通过统一认证中心来实现。主要需要考虑的场景有:新设计开发的两套系统、已经存在各自登录授权模块的两套系统对于登录认证的重构。
对于两套新系统处于设计过程中实现方案相对容易。我们可以考虑公用同一套权限认证表。
基本权限 用户 <–>角色 <–> 资源(权限) 的模型:
即:用户表,用户角色表,角色表,角色资源表 ,资源表的表结构。
如果多系统共用同一套权限认证表结构。就需要在资源中增加所属系统的区别。可以增加一个字段或者增加一个系统表来完成。
这样在做认证和权限时 即可两个系统采用相同的认证授权体系来进行开发。比如:采用JWT时各系统均采用相同的加解密策略,以及权限控制策略即可。
对于已有的两套系统或者属于两个不同开发商的系统之间实现单点登录认证授权时提供以下方案:
整合统一授权认证中心
对各系统之间集中统一一个授权认证中心。专门用于做登录授权以及权限管理等。(可以直接在认证中心进行权限的分配)
采集用户及权限信息
首先需要向各子系统采集它们的用户以及权限管理信息。分析各系统权限认证机制设计一套可共通的表结构。
其次当在认证中心进行权限、角色、用户等增加时,向发生角色等改变的系统发送新的权限信息。
当在个系统中进行相关的改变时也将信息发送至认证中心时,认证中心也进行同步的更新
当在任意子系统登录
向认证中心发送登录请求,由登录中心进行统一认证之后,生成token或者往存储服务放入认证session。并向发起方返回一个证书标识 (sessionid/TOKEN) 发起方将该信息响应至客户端。
session/token中包含 当前用户在各系统上的角色号id等信息。比如:{用户名:张三,系统a角色:管理员,系统b角色:部门领导}
跨域请求任意服务时,均通过自己原始的授权机制,从session或者token中获取出当前系统的角色id进行自己系统的授权即可。
比如:当前位于系统a进行的解析操作时,获取张三用户并获取其系统a角色为管理员。那么查询管理员具有的权限进行授权即可。
当在个系统中进行相关的改变时也将信息发送至认证中心时,认证中心也进行同步的更新新的权限信息。
当在任意子系统登录
向认证中心发送登录请求,由登录中心进行统一认证之后,生成token或者往存储服务放入认证session。并向发起方返回一个证书标识 (sessionid/TOKEN) 发起方将该信息响应至客户端。
session/token中包含 当前用户在各系统上的角色号id等信息。比如:{用户名:张三,系统a角色:管理员,系统b角色:部门领导}
跨域请求任意服务时,均通过自己原始的授权机制,从session或者token中获取出当前系统的角色id进行自己系统的授权即可。
比如:当前位于系统a进行的解析操作时,获取张三用户并获取其系统a角色为管理员。那么查询管理员具有的权限进行授权即可。