gitea的数据备份文件损坏后的处理

之前在我的Nas上用nas自带的gitea建立了一个git服务器,本来一直运行得好好的,突然有一天,有块硬盘坏了,跑去数据恢复中心一问,是JBOD模式的,恢复特别贵。额,没办法,恢复了数据。

恢复后,重建gitea服务,这次直接扔到云上了,重建后发现,有些仓库访问时,直接报了500错误。

一直找不到头绪,今天突然心血来潮,访问了下Server Configuration页面,发现gitea服务访问的文件数据根本不在之前一直访问的那个目录。晕倒!

再次比对正常的仓库和500的仓库,发现500的仓库根目录下缺少refs目录,于是在500的仓库根目录下执行命令:

mkdir -p refs/tags
mkdir -p refs/heads

仓库首页正常打开了!!!

pm2开机启动项目脚本_wj193165zl的博客-CSDN博客_pm2开机启动脚本文件

pm2开机启动项目脚本

该方式支持的系统有:

  • systemd: Ubuntu >= 16, CentOS >= 7, Arch, Debian >= 7
  • upstart: Ubuntu <= 14
  • launchd: Darwin, MacOSx
  • openrc: Gentoo Linux, Arch Linux
  • rcd: FreeBSD
  • systemv: Centos 6, Amazon Linux

pm2命令自带生成开机启动脚本的参数:

在命令行终端执行:

pm2 startup

如果是非root用户,这时候会提示如下信息:


  1. $ pm2 startup

  2. [PM2] You have to run this command as root. Execute the following command:

  3. sudo su -c "env PATH=$PATH:/home/unitech/.nvm/versions/node/v4.3/bin pm2 startup <distribution> -u <user> --hp <home-path>

这时候需要将给出的命令粘贴执行就可以了,不过不用sudo 只粘贴下面的就行:

su -c "env PATH=$PATH:/home/unitech/.nvm/versions/node/v4.3/bin pm2 startup <distribution> -u <user> --hp <home-path>

执行完之后,系统会自动在开机启动脚本目录生成启动脚本(centos6的该脚本位于:/etc/init.d/pm2-xxxx,centos7的该脚本位于/usr/lib/systemd/system/下)


  1. 注意:当node.js版本更新后,需要如下执行:

  2. pm2 unstartup 清楚掉开机启动脚本

  3. pm2 startup 重新生成


  4. 你也可以使用参数--service-name <name>开机启动某个指定服务,

使用下面的命令保存当前pm2服务列表:

pm2 save

这会在$PM2_HOME/.pm2/dump.pm2文件中保存当前的pm2服务列表

通过命令:   pm2 resurrect    可以从上面保存的服务文件中恢复pm2的服务。

禁用掉开机启动脚本:

pm2 unstartup

更新启动脚本

先执行:

pm2 unstartup

再执行:

pm2 startup

这样就会更新pm2的开机启动脚本。

如果你想以其他普通用户来执行这个脚本,需要使用-u参数和–hp参数,

-u 参数指明你想要执行的用户

–hp参数指明你安装pm2的家目录

如:pm2 startup ubuntu -u www –hp /home/ubuntu

上一条命令指明了以www用户来执行,pm2的运行文件放在了/home/ubuntu下面

来自为知笔记(Wiz)

使用 docker 部署 tomcat 并接入 skywalking

一、概述

上一篇文章介绍了使用 docker 部署 spring boot 并接入 skywalking,其中描述了使用 docker-compose 搭建 skywalking,这一篇文章就不介绍 skywalking 的搭建了,这里主要记录一下使用 docker 部署一个 tomcat 并接入 skywalking 进行服务链路追踪。

二、使用 docker 部署 tomcat 并接入 skywalking

关于如何将 tomcat 应用接入 skywalking 官网有如下描述:

详细信息可以查看官网:skywalking agent 官网

  • Linux Tomcat 7 / Tomcat 8
    在tomcat/bin/catalina.sh第一行添加以下内容:

CATALINA_OPTS="$CATALINA_OPTS -javaagent:<skywalking-agent-path>"; export CATALINA_OPTS 
  • Windows Tomcat 7 / Tomcat 8
    在tomcat/bin/catalina.bat第一行添加以下内容:

set "CATALINA_OPTS=-javaagent:<skywalking-agent-path>" 
  • JAR File 或 Spring Boot
    在应用程序的启动命令行中添加-javaagent参数:

java -javaagent:<skywalking-agent-path> -jar yourApp.jar 

注意:-javaagent参数一定要在-jar参数之前。

由于我这里是使用 docker 部署 tomcat ,所以不打算修改tomcat/bin/catalina.sh文件,而且查看tomcat/bin/catalina.sh文件,有如下描述:

image.png

在下图中定义了读取setenv.sh的脚本:

image.png

意思是:不要在catalina.sh这个脚本中设置变量,为了让你的自定义变量分离,应该把自定义的环境变量放在CATALINA_BASE/bin/setenv.sh文件中,除此之外,经过我测试发现,可以直接使用环境变量的方式进行设置,下面我将以两者方式的配置进行说明。

1、将自定义的变量写在 setenv.sh 文件中

首先我们需要下载 skywalking agent,关于下载 agent 可以查看本文开头的文章,之后将agent 放在一个可以访问的目录,我这里是测试,所以将 agent 目录复制到了我的 Dockerfile 的同级目录,其中Dockerfile的内容如下:

FROM tomcat

LABEL maintaner="xiniao"

COPY agent /usr/skywalking/agent

WORKDIR /usr/local/tomcat/bin

RUN echo 'CATALINA_OPTS="$CATALINA_OPTS -javaagent:/usr/skywalking/agent/skywalking-agent.jar";' > setenv.sh

ENV SW_AGENT_NAME="my-spring-demo" \
    SW_AGENT_COLLECTOR_BACKEND_SERVICES="127.0.0.1:11800"

WORKDIR /usr/local/tomcat/webapps/ROOT

COPY target/my-spring-demo.war my-spring-demo.war

RUN jar -xf my-spring-demo.war 

关于 skywalking agent 的其他配置,则可以通过环境变量的方式进行定义,比如这里指定了 skywalking 中显示的服务名为ENV SW_AGENT_NAME=”my-spring-demo”以及 skywalking 的后端服务地址SW_AGENT_COLLECTOR_BACKEND_SERVICES=”127.0.0.1:11800″,我们访问 skywalking ui 可以看到如下图所示内容:

image.png

2、使用环境变量指定 skywalking agent 的路径

详细的Dockerfile内容如下:

FROM tomcat

LABEL maintaner="xiniao"

COPY agent /usr/skywalking/agent

ENV CATALINA_OPTS="$CATALINA_OPTS -javaagent:/usr/skywalking/agent/skywalking-agent.jar" \
    SW_AGENT_NAME="my-spring-demo-env" \
    SW_AGENT_COLLECTOR_BACKEND_SERVICES="127.0.0.1:11800"

WORKDIR /usr/local/tomcat/webapps/ROOT

COPY target/my-spring-demo.war my-spring-demo.war

RUN jar -xf my-spring-demo.war 

使用如下命令构建镜像:

docker build -t my-spring-demo . 

使用如下命令运行容器:

docker run --rm -p 8080:8080 my-spring-demo 

访问 skywalking ui 可以看到监控的信息如下:

image.png

总结

这里主要介绍了使用 docker 部署 tomact 并接入 skywalking 的使用,因为在网上并没有查到太多相关的信息,所以这里记录下来,需要对有需求的小伙伴提供一些帮助。这里还是存在一些问题,比如这里 skywalking agent 直接打到镜像文件里面并不友好,个人觉得可以自定义一个 tomcat 的基础镜像,并将 skywalking agent 打到基础镜像中,这样所有的 tomcat 引用这个基础镜像来构建镜像,就可以实现接入 skywalking。如果是使用 k8s 部署的话,可以在 pod 中定义一个初始容器,在 pod 启动时,将初始容器中的 skywalking agent 复制到我们的应用中,关于如何在 k8s 使用 skywalking,并且无侵入的将 pod 的应用接入 skywalking ,我将在下一篇文章中介绍。

来自为知笔记(Wiz)

win10使用WSL 2运行Docker Desktop,运行文件从C盘迁移到其他目录

­


前言


前几天重装系统,把系统升到了Windows 10 2004,然后在安装Docker Desktop(2.3.0.3版本)时发现跟以前不太一样了。现在Docker Desktop默认使用WLS 2来运行,而不是以前的Hyper-V。

WLS

WLS:适用于 Linux 的 Windows 子系统。

  • 什么是适用于 Linux 的 Windows 子系统?

适用于 Linux 的 Windows 子系统可让开发人员按原样运行 GNU/Linux 环境 – 包括大多数命令行工具、实用工具和应用程序 – 且不会产生虚拟机开销。

  • 什么是 WSL 2?

WSL 2 是适用于 Linux 的 Windows 子系统体系结构的一个新版本,它支持适用于 Linux 的 Windows 子系统在 Windows 上运行 ELF64 Linux 二进制文件。 它的主要目标是提高文件系统性能,以及添加完全的系统调用兼容性。

安装完后试了一下,最明显的感觉就是开启docker的速度大大提升!!!

但是以前设置镜像位置的功能不见了:

看官网说明,原来,启用WSL后,docker运行数据都在WSL发行版中,文件位置都只能由WSL管理!

安装docker后,docker会自动创建2个发行版:

  • docker-desktop
  • docker-desktop-data

WSL发行版默认都是安装在C盘,在%LOCALAPPDATA%/Docker/wsl目录
docker的运行数据、镜像文件都存在%LOCALAPPDATA%/Docker/wsl/data/ext4.vhdx中,这对C盘空间紧张的人非常不友好。。。

WSL发行版迁移

网上查了一下wsl发行版迁移,几乎都是说使用LxRunOffline.exe

经过我试验,LxRunOffline.exe确实可以迁移自己安装的发行版,却迁移不了docker自动创建的2个发行版!

最后只能去github提了个issues://github.com/docker/for-win/issues/7348

下面是操作方法:

  1. 首先关闭docker

  2. 关闭所有发行版:
    wsl --shutdown

  3. 将docker-desktop-data导出到D:\SoftwareData\wsl\docker-desktop-data\docker-desktop-data.tar(注意,原有的docker images不会一起导出)
    wsl --export docker-desktop-data D:\SoftwareData\wsl\docker-desktop-data\docker-desktop-data.tar

  4. 注销docker-desktop-data:
    wsl --unregister docker-desktop-data

  5. 重新导入docker-desktop-data到要存放的文件夹:D:\SoftwareData\wsl\docker-desktop-data\:
    wsl --import docker-desktop-data D:\SoftwareData\wsl\docker-desktop-data\ D:\SoftwareData\wsl\docker-desktop-data\docker-desktop-data.tar --version 2


只需要迁移docker-desktop-data一个发行版就行,另外一个不用管,它占用空间很小。

完成以上操作后,原来的%LOCALAPPDATA%/Docker/wsl/data/ext4.vhdx就迁移到新目录了:

重启docker,这下不用担心C盘爆满了!

:)

来自为知笔记(Wiz)

SpringCloud+Spring Security+OAuth2 + JWT + Gateway讲解

项目简介

  • 本登录系统是一个适应前后端分离并支持传统PC登录的全方位微服务登录架构
  • 基于Spring Boot 2.2.8.、 Spring Cloud Hoxton.SR5 和 Spring Cloud Alibaba 2.2.1
  • 深度定制Spring Security,基于RBAC(暂未实现)、jwt和oauth2的无状态统一权限认证的
  • 单点登录、单点登出(暂未实现)、续签等功能(暂未实现
  • 提供C端多租户功能(暂未实现
  • 提供第三方被授权登录方式(openId方式)
  • 提供供内部服务调用的OAuth2客户端模式(功能已实现,但未使用
  • 提供基于OAuth2的第三方授权码模式(暂未实现
  • 提供自定义添加OAuth2的四种模式的扩展(暂未实现
  • 统一角色权限校验(暂未实现

实现思路

1.基于Spring Security源码
Spring Security过滤器链

所有的请求首先会到 AbstractAuthenticationProcessingFilter 中,并调用doFilter方法,该过滤器会判断用户是否需要登录,如果不登录直接返回。如果需要登录,则调用attemptAuthentication判断自定义拦截器,如果存在自定义拦截器,则会调用子类的该方法,用于用户名、密码登录。否则会进UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter 主要负责登录请求(包含表单等),请求首先回到 attemptAuthentication 方法中,判断用户名和密码是否是空,如果不是,就去构建 UsernamePasswordAuthenticationToken 对象。

UsernamePasswordAuthenticationToken 是 Authentication 接口的其中一个实现,如果第一次进入(这时还没有执行权限认证),该构造方法首先会把用户名、密码设置到本地,然后会赋予权限一个空权限,然后会 setAuthenticated,因为这时还没有执行权限,所以会设置为false。

然后回到 UsernamePasswordAuthenticationFilter ,执行了 setDetails 方法,把request信息和UsernamePasswordAuthenticationToken 对象塞入进去。

UsernamePasswordAuthenticationFilter 最后一步会进去 AuthenticationManager(也就是如上图第二步所示),它本身没什么作用,主要用来管理多个AuthenticationProvider,因为本身登录方式有多种,比如用户名密码登录,第三方登录等区别,并判断当前请求是否支持当前AuthenticationProvider,如果支持,则执行真正的校验逻辑,会调用authenticate 方法,该方法默认从AbstractUserDetailsAuthenticationProvider 实现,该类的 authenticate 方法会调用retrieveUser 方式,该方式是一个抽象方法,由 DaoAuthenticationProvider 实现。

划重点:真正的校验逻辑就在这个方法内,而最终会拿到 getUserDetailsService的loadUserByUsername方法,这个方法就是我们自己程序要实现的用户校验逻辑

在上步拿到UserDetails后,AbstractUserDetailsAuthenticationProvider 把拿到的UserDetails校验,调用preAuthenticationChecks.check(UserDetails)方法,会进行一些预检查,会判断用户是否锁定,是否过期等操作。在做完预检查后,会调用additionalAuthenticationChecks 进行一些附加检查,该方法是一个抽象类,由DaoAuthenticationProvider 实现,主要是对密码的校验(PasswordEncoder)。上面检查都做完后,return this.createSuccessAuthentication(),至此,整个AbstractUserDetailsAuthenticationProvider 认证流程走完。

上面说到 createSuccessAuthentication(),该方法会重新(重点:记住是重新构造UsernamePasswordAuthenticationToken,上面调过一次)构造UsernamePasswordAuthenticationToken方法,把username、password和权限重新塞回去。

至此,整个调用链结束,然后Authentication会沿着刚才的调用链返回回去,然后又回到UsernamePasswordAuthenticationFilter,拿到用户名、密码那。最终回到最开始的AbstractAuthenticationProcessingFilter 过滤器 ,AbstractAuthenticationProcessingFilter.doFilter 成功结束return

PS:this.successfulAuthentication(request, response, chain, authResult),该方法会调用我们自定义的successfulHandler处理器(重点:在OAuth2中,会接手successfulHandler,然后返回token,因此在OAuth2不需要写该handler),successfulAuthentication 方法会把登录成功用户信息存到ThreadLocal中,全局共享。

PS:上面所有流程中任何一处出现错误或者异常则会掉unsuccessfulAuthentication方法,该方法会调用自定义的failureHandler把存入的用户信息从ThreadLocal中清除。

2.Security多请求共享
session共享

如上图所示,其实上面最后一步在 successfulAuthentication  unsuccessfulAuthentication 就能看到SecurityContext,它的作用是把 Authentication 包装起来,而 SecurityContextHolder 则是一个ThreadLocal封装,封装 SecurityContext

security过滤器链

SecurityContextPersistenceFilter 是过滤链上第一个过滤器,所有请求都先过它,响应最后过它,它的作用是:
请求过来后,检查session,判断session是否有
SecurityContext,如果有就把SecurityContext拿出来放到线程里;当整个响应回来最后一个过它的时候,它检查线程,如果线程里有SecurityContext,就拿出来放到Session里,因为整个响应过程是在一个线程里的,在线程其他位置随时可以拿到用户信息。

Oauth2.0

客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)

OAuth 2.0定义了四种授权方式:
  • 授权码模式(authorization code)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)
  • 客户端模式(client credentials)

而它默认的授权服务接口是:

/oauth/authorize:验证接口, AuthorizationEndpoint
/oauth/token:获取token
/oauth/confirm_access:用户授权
/oauth/error:认证失败
/oauth/check_token:资源服务器用来校验token
/oauth/token_key:jwt模式下获取公钥;位于:TokenKeyEndpoint ,通过 JwtAccessTokenConverter 访问key

JWT

简述

客户端身份经过服务器验证通过后,会生成带有签名的 JSON 对象并将它返回给客户端。客户端在收到这个 JSON 对象后存储起来。

在以后的请求中客户端将 JSON 对象连同请求内容一起发送给服务器,服务器收到请求后通过 JSON 对象标识用户,如果验证不通过则不返回请求的数据。

验证不通过的情况有很多,比如签名不正确、无权限等。在 JWT 中服务器不保存任何会话数据,使得服务器更加容易扩展。

Base64URL 算法

在讲解 JWT 的组成结构前我们先来讲解一下 Base64URL 算法。这个算法和 Base64 算法类似,但是有一点区别。

我们通过名字可以得知这个算法使用于 URL 的,因此它将 Base64 中的 + 、 / 、 = 三个字符替换成了 – 、 _ ,删除掉了 = 。因为这个三个字符在 URL 中有特殊含义。

JWT 组成结构

JWT 是由三段字符串和两个 . 组成,每个字符串和字符串之间没有换行(类似于这样:xxxxxx.yyyyyy.zzzzzz),每个字符串代表了不同的功能,我们将这三个字符串的功能按顺序列出来并讲解:

1. JWT 头

JWT 头描述了 JWT 元数据,是一个 JSON 对象,它的格式如下:

json{“alg”:”HS256″,”typ”:”JWT”}

这里的 alg 属性表示签名所使用的算法,JWT 签名默认的算法为 HMAC SHA256 , alg 属性值 HS256 就是 HMAC SHA256 算法。typ 属性表示令牌类型,这里就是 JWT。

2. 有效载荷

有效载荷是 JWT 的主体,同样也是个 JSON 对象。有效载荷包含三个部分:

标准注册声明
标准注册声明不是强制使用是的,但是我建议使用。它一般包括以下内容:

iss:jwt的签发者/发行人;

sub:主题;

aud:接收方;

exp:jwt过期时间;

nbf:jwt生效时间;

iat:签发时间

jti:jwt唯一身份标识,可以避免重放攻击

公共声明:
可以在公共声明添加任何信息,我们一般会在里面添加用户信息和业务信息,但是不建议添加敏感信息,因为公共声明部分可以在客户端解密。

私有声明:
私有声明是服务器和客户端共同定义的声明,同样这里不建议添加敏感信息。

下面这个代码段就是定义了一个有效载荷:

json{“exp”:”201909181230″,”role”:”admin”,”isShow”:false}

3. 哈希签名

哈希签名的算法主要是确保数据不会被篡改。它主要是对前面所讲的两个部分进行签名,通过 JWT 头定义的算法生成哈希。哈希签名的过程如下:

  1. 指定密码,密码保存在服务器中,不能向客户端公开;

  2. 使用 JWT 头指定的算法进行签名,进行签名前需要对 JWT 头和有效载荷进行 Base64URL 编码,JWT 头和邮箱载荷编码后的结果之间需要用 . 来连接。

简单示例如下:

HMACSHA256(base64UrlEncode(JWT 头) + “.” + base64UrlEncode(有效载荷),密码)

最终结果如下:

base64UrlEncode(JWT 头)+”.”+base64UrlEncode(有效载荷)+”.”+HMACSHA256(base64UrlEncode(JWT 头) + “.” + base64UrlEncode(有效载荷),密码)

JWT 注意事项

在使用 JWT 时需要注意以下事项:

  1. JWT 默认不加密,如果要写入敏感信息必须加密,可以用生成的原始令牌再次对内容进行加密;

  2. JWT 无法使服务器保存会话状态,当令牌生成后在有效期内无法取消也不能更改;

  3. JWT 包含认证信息,如果泄露了,任何人都可以获得令牌所有的权限;因此 JWT 有效期不能太长,对于重要操作每次请求都必须进行身份验证。

认证中心(uaa-server)

本项目深度定制了OAuth2.0,扩展了部分OAuth2部分接口,原因是现有业务不支持,原生接口只有传用户名、密码,而本公司业务除了用户名、密码还有买家多租户方式,因此不太适合,比如重写了如密码模式获取token、刷新token(暂未完成)、客户端模式获取token、openId获取token等方式,这些都是原生没有的。

其次如果用JWT这种获取token方式是要加密的,默认可以使用对称加密,也可以是用非对称加密,本项目使用非对称加密方式,因为非对称加密几乎不可能被破解,私钥存在认证中心,公钥存在各资源服务器上。

JWT的RSA非对称密钥生成

1.生成密钥文件
使用jdk自带的keytool工具,执行后会在当前目录生成fzp-jwt.jks(存在uaa资源目录下)密钥文件

keytool -genkey -alias dhgate -keyalg RSA -storetype PKCS12 -keysize 1024 -keystore fzp-jwt.jks

参数解析

-genkey:创建证书
-alias:证书的别名。在一个证书库文件中,别名是唯一用来区分多个证书的标识符
-keyalg:密钥的算法,非对称加密的话就是RSA
-keystore:证书库文件保存的位置和文件名。如果路径写错的话,会出现报错信息。如果在路径下,证书库文件不存在,那么就会创建一个
-keysize:密钥长度,一般都是1024
-validity:证书的有效期,单位是天。比如36500的话,就是100年

2.提取公钥

keytool -list -rfc -keystore fzp-jwt.jks -storepass libaojun@dhgate.com | openssl x509 -inform pem -pubkey

公私钥

参数解析

-keystore:密钥文件
-storepass:密钥密码

用户名密码获取token(/oauth/user/token)

该接口后期需要优化,原因是前期业务路线变化,前期实现给自己挖坑,也是最开始讨论所有认证中心+网关拆分,导致后面多了C端用户,并在C端用户上扩展多租户让原本的用户名、密码登录方式变成了各种填坑,因此也是后面要做的拆分。预期是不做认证中心拆分,而达到多租户效果。

代码逻辑: 

     * Oauth2 密码模式
     * @param passwordLoginParamDto
     * @param request
     * @param response
     * @throws IOException
     */
    (value = "用户名密码获取token")
    (value = "/oauth/user/token", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
    public void userTokenInfo( PasswordLoginParamDto passwordLoginParamDto,
                              HttpServletRequest request, HttpServletResponse response) throws IOException {
        String userName = "";
        if(passwordLoginParamDto.getUserType() == 1){
            userName = String.format("%s,%s", passwordLoginParamDto.getUsername(), passwordLoginParamDto.getUserType());
        }else{
            userName = String.format("%s;%s,%s", passwordLoginParamDto.getUsername(), passwordLoginParamDto.getShopId(), passwordLoginParamDto.getUserType());
        }

        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userName, passwordLoginParamDto.getPassword());
        TokenTransferDto dto = getTokenTransferDto("username or password error!");
        dto.request = request;
        dto.response = response;
        dto.token = token;
        writerDefaultToken(dto);
    }
image.png

1.将用户名、密码和用户类型(如果是C端用户还得传shopId)传入UsernamePasswordAuthenticationToken(这个类作用和实现原理在前面Security有提到),然后会校验用户名、密码等信息,校验逻辑可以是数据库,也可以是内存。
2.构造TokenTransferDto默认dto,把错误信息、request、response、UsernamePasswordAuthenticationToken等构造进去。
3.指定模式为密码模式,获取clientId和clientSecret(默认内存获取,已实现从数据库获取,并加入缓存),根据clientId拿到ClientDetails(ClientDetails在AuthorizationServerConfig.configure去刷到缓存中),拿到ClientDetails的目的是,OAuth2默认需要Basic验证,不加会报如下错误:

ClientDetails

4.根据附加参数(可选,刷新必须传)、ClientId、Scope和当前使用的OAuth2模式来构造TokenRequest,而TokenRequest的作用是:在隐式流中,令牌通过AuthorizationEndpoint直接请求,在这种情况下,AuthorizationRequest被转换为TokenRequest,以便通过令牌授予链进行处理。
5.TokenRequest根据clientSecret、grant_type、password和GrantedAuthority去构造OAuth2Request
6.根据OAuth2RequestAuthentication去构造OAuth2Authentication,然后通过OAuth2Authentication最终去创造AccessToken。至此,密码登录方式完成。效果如下:

密码模式token
clientId获取token(/oauth/client/token)

此登录方式适合服务间内部调用登录用,安全性比较低(但比简单模式高),因为不用传入用户名、密码等信息,就可以拿到登录信息

    /**
     * Oauth2 客户端模式
     * @param request
     * @param response
     * @throws IOException
     */
    @ApiOperation(value = "clientId获取token")
    @PostMapping("/oauth/client/token")
    public void clientTokenInfo(HttpServletRequest request, HttpServletResponse response)throws IOException {
        TokenTransferDto dto = getTokenTransferDto("clientId or secret error.");
        dto.request = request;
        dto.response = response;
        writerClientToken(dto);
    }

实现方式跟密码模式基本一致,请查看密码模式方式。该结果只能拿到userId,并不能拿到其他信息,返回结果如下:

客户端模式
openId获取token(/oauth/openId/token)

openId这种登录方式比较特别,因为它不在OAuth2的四种模式之内,但查看源码能看到可以依靠OAuth2提供的扩展实现该方式,详情源码可以参照UsernamePasswordAuthenticationToken,这在前面的Security已经有介绍。

@ApiOperation(value = "openId获取token")
    @PostMapping("/oauth/openId/token")
    public void getTokenByOpenId(
            @RequestBody OpenIdLoginParamDto openIdLoginParamDto,
            HttpServletRequest request, HttpServletResponse response) throws IOException {
        OpenIdAuthenticationToken token = new OpenIdAuthenticationToken(openIdLoginParamDto.getOpenId());
        TokenTransferDto dto = getTokenTransferDto("openId error!");
        dto.request = request;
        dto.response = response;
        dto.token = token;
        writerDefaultToken(dto);
    }

1.首先要实现一个类似于UsernamePasswordAuthenticationToken的token类(请看源码),去生成要传入的参数。
然后再实现AuthenticationProvider接口,该接口作用主要是生成UserDetails,然后把参数传入UsernamePasswordAuthenticationToken并构造:

/**
 * @author lbj
 */
public class OpenIdAuthenticationProvider implements AuthenticationProvider {

    private MySocialUserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) {
        OpenIdAuthenticationToken authenticationToken = (OpenIdAuthenticationToken) authentication;
        String openId = (String) authenticationToken.getPrincipal();
        UserDetails user = userDetailsService.loadUserByOpenId(openId);
        if (user == null) {
            throw new InternalAuthenticationServiceException("openId result user is null.");
        }
        OpenIdAuthenticationToken authenticationResult = new OpenIdAuthenticationToken(user, user.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return OpenIdAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public MySocialUserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(MySocialUserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}

然后再创建OpenIdAuthenticationSecurityConfig来加入Security的默认支持方式

/**
 * openId的相关处理配置
 *
 * @author lbj
 */
@Component
public class OpenIdAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    private MySocialUserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity http) {
        //openId provider
        OpenIdAuthenticationProvider provider = new OpenIdAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        http.authenticationProvider(provider);
    }
}

最后在Security的拦截类加入自定义的登录方式

@Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable().httpBasic().disable().authorizeRequests().anyRequest().permitAll()
        .and().apply(openIdAuthenticationSecurityConfig); //就是这里对OpenId生效

        // 基于密码 等模式可以无session,不支持授权码模式
        if (authenticationEntryPoint != null) {
            http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
                    .accessDeniedHandler(customAccessDeniedHandler);
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        } else {
            // 授权码模式单独处理,需要session的支持,此模式可以支持所有oauth2的认证
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
        }

        // 解决不允许显示在iframe的问题
        http.headers().frameOptions().disable();
        http.headers().cacheControl();

    }

资源中心

各资源服务器需要实现ResourceServerConfigurerAdapter,代表开启一个资源服务器,再配置下各资源服务引入的token方式,配置为resJwt的都会被标记为资源服务器,配置如下:

dhgate:
  oauth2:
    token:
      store:
        type: resJwt

前端传入token后,各资源服务器会去解析当前token(网关会校验当前登录有效性),解析方式在各资源服务器的资源文件中(public.cert),如果公钥验证成功,会进入DefaultUserAuthenticationConverter

如果当前用户已登录,则会去DefaultUserAuthenticationConverter中去获取用户的登录信息,然后塞入ThreadLocal中

public class JWTfaultUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
            private Collection<? extends GrantedAuthority> defaultAuthorities;

            public Authentication extractAuthentication(Map<String, ?> map) {
                if (map.containsKey("user_info")) {
                    Object principal = map.get("user_info");
                //  Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
                    LoginAppUser loginUser = new LoginAppUser();
                    if (principal instanceof Map) {
                        loginUser = BeanUtil.mapToBean((Map) principal, LoginAppUser.class, true);
                    }
                    return new UsernamePasswordAuthenticationToken(loginUser, "N/A", loginUser.getAuthorities());
                }
                return null;
            }
        }

因为SecurityContext在当前线程全局有效,所以登录信息可以在资源服务器任何一个地方拿到

/**
 * @author 作者 lbj
 * @version 创建时间:2020年07月01日 上午20:57:51 获取用户信息
 */
public class SysUserUtil {

    /**
     * 获取登陆的 LoginAppUser
     *
     * @return
     */
    public static LoginAppUser getLoginAppUser() {
        
        // 当OAuth2AuthenticationProcessingFilter设置当前登录时,直接返回
        // 强认证时处理
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication instanceof OAuth2Authentication) {
            OAuth2Authentication oAuth2Auth = (OAuth2Authentication) authentication;
            authentication = oAuth2Auth.getUserAuthentication();

            if (authentication instanceof UsernamePasswordAuthenticationToken) {
                UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) authentication;

                if (authenticationToken.getPrincipal() instanceof LoginAppUser) {
                    return (LoginAppUser) authenticationToken.getPrincipal();
                } else if (authenticationToken.getPrincipal() instanceof Map) {

                    LoginAppUser loginAppUser = BeanUtil.mapToBean((Map) authenticationToken.getPrincipal(), LoginAppUser.class, true);
                    return loginAppUser;
                }
            } else if (authentication instanceof PreAuthenticatedAuthenticationToken) {
                // 刷新token方式
                PreAuthenticatedAuthenticationToken authenticationToken = (PreAuthenticatedAuthenticationToken) authentication;
                return (LoginAppUser) authenticationToken.getPrincipal();
            }
        }
       
        return null;
    }
}

登录信息已经存到了本地线程变量中,因此上面代码能在资源服务器任何地方拿到用户登录信息。

Gateway

网关在本项目中的角色是对来自认证中心的服务鉴权、过滤地址、服务转发等功能(后续功能添加中,比如全局过滤,局部过滤)

网关鉴权

要实现网关鉴权,则网关必须得标记为资源服务器,因网关使用webflux异步非阻塞原理实现,底层服务器是基于netty,不向下兼容,不兼容普通web相关,所以网关得单独实现一套资源服务认证。资源认证流程如下:

/**
 * webflux资源服务器配置
 *
 * @author lbj
 * @date 2020/07/02
 */
@EnableConfigurationProperties(SecurityProperties.class)
public class ResourceServerConfiguration {
    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private PermitAuthenticationWebFilter permitAuthenticationWebFilter;

    @Bean
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http.addFilterBefore(permitAuthenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
        //认证处理器
        ReactiveAuthenticationManager customAuthenticationManager = new CustomAuthenticationManager(tokenStore);
        JsonAuthenticationEntryPoint entryPoint = new JsonAuthenticationEntryPoint();
        //token转换器
        ServerBearerTokenAuthenticationConverter tokenAuthenticationConverter = new ServerBearerTokenAuthenticationConverter();
        tokenAuthenticationConverter.setAllowUriQueryParameter(true);
        //oauth2认证过滤器
        AuthenticationWebFilter oauth2Filter = new AuthenticationWebFilter(customAuthenticationManager);
        oauth2Filter.setServerAuthenticationConverter(tokenAuthenticationConverter);
        oauth2Filter.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
        oauth2Filter.setAuthenticationSuccessHandler(new Oauth2AuthSuccessHandler());
        http.addFilterAt(oauth2Filter, SecurityWebFiltersOrder.AUTHENTICATION);

        ServerHttpSecurity.AuthorizeExchangeSpec authorizeExchange = http.authorizeExchange();
//        if (securityProperties.getAuth().getHttpUrls().length > 0) {
//            authorizeExchange.pathMatchers(securityProperties.getAuth().getHttpUrls()).authenticated();
//        }
        if (securityProperties.getIgnore().getUrls().length > 0) {
            authorizeExchange.pathMatchers(securityProperties.getIgnore().getUrls()).permitAll();
        }

        ServerHttpSecurity.AuthorizeExchangeSpec.Access access = authorizeExchange
                .pathMatchers(HttpMethod.OPTIONS).permitAll()
                .anyExchange();
        setAuthenticate(access);

                //.anyExchange().authenticated()  //这个跟下面那行是一样的,只是下面能更细的控制权限
                   // .access(permissionAuthManager)   // 应用api权限控制 后期权限控制会用,暂时先不做
        http
            .exceptionHandling()
                .accessDeniedHandler(new JsonAccessDeniedHandler())
                .authenticationEntryPoint(entryPoint)
        .and()
            .headers()
                .frameOptions()
                .disable()
        .and()
            .httpBasic().disable()
            .csrf().disable();

        return http.build();
    }

    /**
     * url权限控制,默认是认证就通过,可以重写实现个性化 permisson设置
     * @param authorizedAccess
     */
    public ServerHttpSecurity setAuthenticate(ServerHttpSecurity.AuthorizeExchangeSpec.Access authorizedAccess) {
        return authorizedAccess.authenticated().and();
    }
}
网关转发、动态路由

网关转发和动态路由需要如下配置,id为要匹配的id、predicates为要匹配的路径,加前缀、filters为截去的路径,动态路由实现是基于Nacos的配置监听,Gateway给了一个端点来实现此功能,当然Gateway提供多种方式实现路由:

[
    {
        "id": "ebay-service",
        "predicates": [{
            "name": "Path",
            "args": {
                "pattern": "/ebay/**"
            }
        }],
        "uri": "lb://ebay-service",
        "filters": [{
            "name": "StripPrefix",
            "args": {
                "parts": "1"
            }
        }]
    }
]
网关统一请求拦截

统一资源过滤是需要把普通资源服务器的鉴权工作移到网关,只需网关也是资源服务器就能做到此功能,前面已经实现了网关作为资源服务器,并且实现了对普通服务转发,因此只需如下配置就可以对各资源服务器统一请求拦截:

  security:
    ignore:
      httpUrls: >
        /uaa/**,
        /dsuser/users-anon/**,
        /dsuser/api/**,
        /buser/users-anon/**,
        /buser/api/**,
        /shopify/dhgate/shopify/unauth/**,
        /myyshop/everybody/**,
        /umc/email/**,
        /myyshop-order/everybody/**,
        /myyshop/everybody/**,
        /umc/email/**,
        /myyshop-order/order/payCallBack,
        /search/myyshop/search,
        /myyshop-order/shipping/api/**
网关拦截器 PermitAuthenticationWebFilter

该拦截器作用于全局,并且顺序是在鉴权之前,顺序配置只需要在网关资源中心加个拦截器并指定生效于哪个拦截器之前即可:

http.addFilterBefore(permitAuthenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);

作用是判断当前地址是否需要放权,放权判断你是通过spring提供的通配符判断,如果放权则删除当前请求header token:

/**
 * webflux资源服务器配置
 *
 * @author lbj
 * @date 2020/07/02
 */
@EnableConfigurationProperties(SecurityProperties.class)
public class ResourceServerConfiguration {
    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private PermitAuthenticationWebFilter permitAuthenticationWebFilter;

    @Bean
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http.addFilterBefore(permitAuthenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
        //认证处理器
        ReactiveAuthenticationManager customAuthenticationManager = new CustomAuthenticationManager(tokenStore);
        JsonAuthenticationEntryPoint entryPoint = new JsonAuthenticationEntryPoint();
        //token转换器
        ServerBearerTokenAuthenticationConverter tokenAuthenticationConverter = new ServerBearerTokenAuthenticationConverter();
        tokenAuthenticationConverter.setAllowUriQueryParameter(true);
        //oauth2认证过滤器
        AuthenticationWebFilter oauth2Filter = new AuthenticationWebFilter(customAuthenticationManager);
        oauth2Filter.setServerAuthenticationConverter(tokenAuthenticationConverter);
        oauth2Filter.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
        oauth2Filter.setAuthenticationSuccessHandler(new Oauth2AuthSuccessHandler());
        http.addFilterAt(oauth2Filter, SecurityWebFiltersOrder.AUTHENTICATION);

        ServerHttpSecurity.AuthorizeExchangeSpec authorizeExchange = http.authorizeExchange();
//        if (securityProperties.getAuth().getHttpUrls().length > 0) {
//            authorizeExchange.pathMatchers(securityProperties.getAuth().getHttpUrls()).authenticated();
//        }
        if (securityProperties.getIgnore().getUrls().length > 0) {
            authorizeExchange.pathMatchers(securityProperties.getIgnore().getUrls()).permitAll();
        }

        ServerHttpSecurity.AuthorizeExchangeSpec.Access access = authorizeExchange
                .pathMatchers(HttpMethod.OPTIONS).permitAll()
                .anyExchange();
        setAuthenticate(access);

                //.anyExchange().authenticated()  //这个跟下面那行是一样的,只是下面能更细的控制权限
                   // .access(permissionAuthManager)   // 应用api权限控制 后期权限控制会用,暂时先不做
        http
            .exceptionHandling()
                .accessDeniedHandler(new JsonAccessDeniedHandler())
                .authenticationEntryPoint(entryPoint)
        .and()
            .headers()
                .frameOptions()
                .disable()
        .and()
            .httpBasic().disable()
            .csrf().disable();

        return http.build();
    }

    /**
     * url权限控制,默认是认证就通过,可以重写实现个性化 permisson设置
     * @param authorizedAccess
     */
    public ServerHttpSecurity setAuthenticate(ServerHttpSecurity.AuthorizeExchangeSpec.Access authorizedAccess) {
        return authorizedAccess.authenticated().and();
    }
}

至此,整个SpringCloud+Spring Security+OAuth2 + JWT + Gateway集成通过源码+现有代码结束。

此外。

Security拦截器

Security拦截器顺序在FilterComparator中,可以动态加入比他前或者后的顺序

private static final int INITIAL_ORDER = 100;
    private static final int ORDER_STEP = 100;
    private final Map<String, Integer> filterToOrder = new HashMap();

    FilterComparator() {
        FilterComparator.Step order = new FilterComparator.Step(100, 100);
        this.put(ChannelProcessingFilter.class, order.next());
        this.put(ConcurrentSessionFilter.class, order.next());
        this.put(WebAsyncManagerIntegrationFilter.class, order.next());
        this.put(SecurityContextPersistenceFilter.class, order.next());
        this.put(HeaderWriterFilter.class, order.next());
        this.put(CorsFilter.class, order.next());
        this.put(CsrfFilter.class, order.next());
        this.put(LogoutFilter.class, order.next());
        this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter", order.next());
        this.filterToOrder.put("org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter", order.next());
        this.put(X509AuthenticationFilter.class, order.next());
        this.put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
        this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next());
        this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter", order.next());
        this.filterToOrder.put("org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter", order.next());
        this.put(UsernamePasswordAuthenticationFilter.class, order.next());
        this.put(ConcurrentSessionFilter.class, order.next());
        this.filterToOrder.put("org.springframework.security.openid.OpenIDAuthenticationFilter", order.next());
        this.put(DefaultLoginPageGeneratingFilter.class, order.next());
        this.put(DefaultLogoutPageGeneratingFilter.class, order.next());
        this.put(ConcurrentSessionFilter.class, order.next());
        this.put(DigestAuthenticationFilter.class, order.next());
        this.filterToOrder.put("org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter", order.next());
        this.put(BasicAuthenticationFilter.class, order.next());
        this.put(RequestCacheAwareFilter.class, order.next());
        this.put(SecurityContextHolderAwareRequestFilter.class, order.next());
        this.put(JaasApiIntegrationFilter.class, order.next());
        this.put(RememberMeAuthenticationFilter.class, order.next());
        this.put(AnonymousAuthenticationFilter.class, order.next());
        this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter", order.next());
        this.put(SessionManagementFilter.class, order.next());
        this.put(ExceptionTranslationFilter.class, order.next());
        this.put(FilterSecurityInterceptor.class, order.next());
        this.put(SwitchUserFilter.class, order.next());
    }

FilterComparator 比较器中初始化了Spring Security 自带的Filter 的顺序,即在创建时已经确定了默认Filter的顺序。并将所有过滤器保存在一个 filterToOrder Map中。key值是Filter的类名,value是过滤器的顺序号。

完结。

来自为知笔记(Wiz)

ConcurrentHashMap实现原理及源码分析 – dreamcatcher-cx – 博客园

ConcurrentHashMap实现原理及源码分析

  ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现(若对HashMap的实现原理还不甚了解,可参考我的另一篇文章HashMap实现原理及源码分析),ConcurrentHashMap在并发编程的场景中使用频率非常之高,本文就来分析下ConcurrentHashMap的实现原理,并对其实现原理进行分析(JDK1.7).

ConcurrentHashMap实现原理

  众所周知,哈希表是中非常高效,复杂度为O(1)的数据结构,在Java开发中,我们最常见到最频繁使用的就是HashMap和HashTable,但是在线程竞争激烈的并发场景中使用都不够合理。

  HashMap :先说HashMap,HashMap是线程不安全的,在并发环境下,可能会形成环状链表(扩容时可能造成,具体原因自行百度google或查看源码分析),导致get操作时,cpu空转,所以,在并发环境中使用HashMap是非常危险的。

  HashTable : HashTable和HashMap的实现原理几乎一样,差别无非是1.HashTable不允许key和value为null;2.HashTable是线程安全的。但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。

  HashTable性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的”分段锁“思想。

  

ConcurrentHashMap源码分析   

ConcurrentHashMap采用了非常精妙的”分段锁”策略,ConcurrentHashMap的主干是个Segment数组。

 final Segment<K,V>[] segments;

  Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的。(就按默认的ConcurrentLeve为16来讲,理论上就允许16个线程并发执行,有木有很酷)

  所以,对于同一个Segment的操作才需考虑线程同步,不同的Segment则无需考虑。

Segment类似于HashMap,一个Segment维护着一个HashEntry数组

 transient volatile HashEntry<K,V>[] table;

HashEntry是目前我们提到的最小的逻辑处理单元了。一个ConcurrentHashMap维护一个Segment数组,一个Segment维护一个HashEntry数组。

复制代码
 static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;
        //其他省略
}    
复制代码

我们说Segment类似哈希表,那么一些属性就跟我们之前提到的HashMap差不离,比如负载因子loadFactor,比如阈值threshold等等,看下Segment的构造方法

Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
            this.loadFactor = lf;//负载因子
            this.threshold = threshold;//阈值
            this.table = tab;//主干数组即HashEntry数组
        }

我们来看下ConcurrentHashMap的构造方法

复制代码
 1  public ConcurrentHashMap(int initialCapacity,
 2                                float loadFactor, int concurrencyLevel) {
 3           if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
 4               throw new IllegalArgumentException();
 5           //MAX_SEGMENTS 为1<<16=65536,也就是最大并发数为65536
 6           if (concurrencyLevel > MAX_SEGMENTS)
 7               concurrencyLevel = MAX_SEGMENTS;
 8           //2的sshif次方等于ssize,例:ssize=16,sshift=4;ssize=32,sshif=5
 9          int sshift = 0;
10          //ssize 为segments数组长度,根据concurrentLevel计算得出
11          int ssize = 1;
12          while (ssize < concurrencyLevel) {
13              ++sshift;
14              ssize <<= 1;
15          }
16          //segmentShift和segmentMask这两个变量在定位segment时会用到,后面会详细讲
17          this.segmentShift = 32 - sshift;
18          this.segmentMask = ssize - 1;
19          if (initialCapacity > MAXIMUM_CAPACITY)
20              initialCapacity = MAXIMUM_CAPACITY;
21          //计算cap的大小,即Segment中HashEntry的数组长度,cap也一定为2的n次方.
22          int c = initialCapacity / ssize;
23          if (c * ssize < initialCapacity)
24              ++c;
25          int cap = MIN_SEGMENT_TABLE_CAPACITY;
26          while (cap < c)
27              cap <<= 1;
28          //创建segments数组并初始化第一个Segment,其余的Segment延迟初始化
29          Segment<K,V> s0 =
30              new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
31                               (HashEntry<K,V>[])new HashEntry[cap]);
32          Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
33          UNSAFE.putOrderedObject(ss, SBASE, s0); 
34          this.segments = ss;
35      }
复制代码

  初始化方法有三个参数,如果用户不指定则会使用默认值,initialCapacity为16,loadFactor为0.75(负载因子,扩容时需要参考),concurrentLevel为16。

  从上面的代码可以看出来,Segment数组的大小ssize是由concurrentLevel来决定的,但是却不一定等于concurrentLevel,ssize一定是大于或等于concurrentLevel的最小的2的次幂。比如:默认情况下concurrentLevel是16,则ssize为16;若concurrentLevel为14,ssize为16;若concurrentLevel为17,则ssize为32。为什么Segment的数组大小一定是2的次幂?其实主要是便于通过按位与的散列算法来定位Segment的index。至于更详细的原因,有兴趣的话可以参考我的另一篇文章HashMap实现原理及源码分析,其中对于数组长度为什么一定要是2的次幂有较为详细的分析。

  接下来,我们来看看put方法

复制代码
 public V put(K key, V value) {
        Segment<K,V> s;
        //concurrentHashMap不允许key/value为空
        if (value == null)
            throw new NullPointerException();
        //hash函数对key的hashCode重新散列,避免差劲的不合理的hashcode,保证散列均匀
        int hash = hash(key);
        //返回的hash值无符号右移segmentShift位与段掩码进行位运算,定位segment
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);
    }
复制代码

 从源码看出,put的主要逻辑也就两步:1.定位segment并确保定位的Segment已初始化 2.调用Segment的put方法

 关于segmentShift和segmentMask

  segmentShift和segmentMask这两个全局变量的主要作用是用来定位Segment,int j =(hash >>> segmentShift) & segmentMask。

  segmentMask:段掩码,假如segments数组长度为16,则段掩码为16-1=15;segments长度为32,段掩码为32-1=31。这样得到的所有bit位都为1,可以更好地保证散列的均匀性

  segmentShift:2的sshift次方等于ssize,segmentShift=32-sshift。若segments长度为16,segmentShift=32-4=28;若segments长度为32,segmentShift=32-5=27。而计算得出的hash值最大为32位,无符号右移segmentShift,则意味着只保留高几位(其余位是没用的),然后与段掩码segmentMask位运算来定位Segment。

  get/put方法

  get方法

复制代码
 public V get(Object key) {
        Segment<K,V> s; 
        HashEntry<K,V>[] tab;
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
//先定位Segment,再定位HashEntry
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) { for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); e != null; e = e.next) { K k; if ((k = e.key) == key || (e.hash == h && key.equals(k))) return e.value; } } return null; }
复制代码

  get方法无需加锁,由于其中涉及到的共享变量都使用volatile修饰,volatile可以保证内存可见性,所以不会读取到过期数据。

  来看下concurrentHashMap代理到Segment上的put方法,Segment中的put方法是要加锁的。只不过是锁粒度细了而已。

复制代码
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);//tryLock不成功时会遍历定位到的HashEnry位置的链表(遍历主要是为了使CPU缓存链表),若找不到,则创建HashEntry。tryLock一定次数后(MAX_SCAN_RETRIES变量决定),则lock。若遍历过程中,由于其他线程的操作导致链表头结点变化,则需要重新遍历。
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;//定位HashEntry,可以看到,这个hash值在定位Segment时和在Segment中定位HashEntry都会用到,只不过定位Segment时只用到高几位。
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        K k;
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    else {
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
              //若c超出阈值threshold,需要扩容并rehash。扩容后的容量是当前容量的2倍。这样可以最大程度避免之前散列好的entry重新散列,具体在另一篇文章中有详细分析,不赘述。扩容并rehash的这个过程是比较消耗资源的。
if (c > threshold && tab.length < MAXIMUM_CAPACITY) rehash(node); else setEntryAt(tab, index, node); ++modCount; count = c; oldValue = null; break; } } } finally { unlock(); } return oldValue; }
复制代码

 总结

  ConcurrentHashMap作为一种线程安全且高效的哈希表的解决方案,尤其其中的”分段锁”的方案,相比HashTable的全表锁在性能上的提升非常之大。本文对ConcurrentHashMap的实现原理进行了详细分析,并解读了部分源码,希望能帮助到有需要的童鞋。

 

作者:
dreamcatcher-cx

出处:
<http://www.cnblogs.com/chengxiao/>

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在页面明显位置给出原文链接。

来自为知笔记(Wiz)

zookeeper.KeeperException$UnimplementedExceptio… – 简书

zookeeper.KeeperException$UnimplementedException: KeeperErrorCode = Unimplemented for {root.path}

云舒Lee

2017.10.04 00:51*

1. 问题描述

Dubbo 应用使用 ZooKeeper 作为注册中心,启动时发生该异常。

ZooKeeper JAR及其 Curator 客户端版本:

zookeeper-3.5.1-alpha
curator-framework-3.3.0
curator-client-3.3.0

ZooKeeper 服务器版本:3.4.9

$ echo stat | nc localhost 2181
Zookeeper version: 3.4.9-1757313, built on 08/23/2016 06:50 GMT

2. 异常日志

2017-10-03 20:06:48.485  INFO 62124 --- [           main] utoConfigurationReportLoggingInitializer :

Error starting ApplicationContext. To display the auto-configuration report re-run your application with 'debug' enabled.
2017-10-03 20:06:48.498 ERROR 62124 --- [           main] o.s.boot.SpringApplication               : Application startup failed

java.lang.IllegalStateException: Failed to register ......
    ......
Caused by: com.alibaba.dubbo.rpc.RpcException: Failed to register
dubbo://192.168.1.2:20880/spring.guides.dubbo.service.DemoService?accepts=0&anyhost=true&application=spring-boot-rpc-soa-dubbo-provider&dubbo=2.5.5&generic=false&interface=spring.guides.dubbo.service.DemoService&methods=sayHello&pid=62124&retries=1&side=provider&threadpool=cached&threads=100&timeout=1000&timestamp=1507032406454
to zookeeper zookeeper://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=spring-boot-rpc-soa-dubbo-provider&check=true&client=curator&dubbo=2.5.5&file=/Users/dannong/.dubbo/registry.cache&group=dubbo_develop&interface=com.alibaba.dubbo.registry.RegistryService&pid=62124&timestamp=1507032406438, cause: KeeperErrorCode = Unimplemented for /dubbo_develop
    at com.alibaba.dubbo.registry.zookeeper.ZookeeperRegistry.doRegister(ZookeeperRegistry.java:112) ~[dubbo-2.5.5.jar:2.5.5]
    at com.alibaba.dubbo.registry.support.FailbackRegistry.register(FailbackRegistry.java:136) ~[dubbo-2.5.5.jar:2.5.5]
    ... 27 common frames omitted
Caused by: java.lang.IllegalStateException: KeeperErrorCode = Unimplemented for /dubbo_develop
    at com.alibaba.dubbo.remoting.zookeeper.curator.CuratorZookeeperClient.createPersistent(CuratorZookeeperClient.java:59) ~[dubbo-2.5.5.jar:2.5.5]
    at com.alibaba.dubbo.remoting.zookeeper.support.AbstractZookeeperClient.create(AbstractZookeeperClient.java:44) ~[dubbo-2.5.5.jar:2.5.5]
    at com.alibaba.dubbo.remoting.zookeeper.support.AbstractZookeeperClient.create(AbstractZookeeperClient.java:39) ~[dubbo-2.5.5.jar:2.5.5]
    at com.alibaba.dubbo.remoting.zookeeper.support.AbstractZookeeperClient.create(AbstractZookeeperClient.java:39) ~[dubbo-2.5.5.jar:2.5.5]
    at com.alibaba.dubbo.remoting.zookeeper.support.AbstractZookeeperClient.create(AbstractZookeeperClient.java:39) ~[dubbo-2.5.5.jar:2.5.5]
    at com.alibaba.dubbo.registry.zookeeper.ZookeeperRegistry.doRegister(ZookeeperRegistry.java:110) ~[dubbo-2.5.5.jar:2.5.5]
    ... 28 common frames omitted
Caused by: org.apache.zookeeper.KeeperException$UnimplementedException: KeeperErrorCode = Unimplemented for /dubbo_develop
    at org.apache.zookeeper.KeeperException.create(KeeperException.java:103) ~[zookeeper-3.5.1-alpha.jar:3.5.1-alpha-1693007]
    at org.apache.zookeeper.KeeperException.create(KeeperException.java:51) ~[zookeeper-3.5.1-alpha.jar:3.5.1-alpha-1693007]
    at org.apache.zookeeper.ZooKeeper.create(ZooKeeper.java:1297) ~[zookeeper-3.5.1-alpha.jar:3.5.1-alpha-1693007]
    at org.apache.curator.framework.imps.CreateBuilderImpl$17.call(CreateBuilderImpl.java:1075) ~[curator-framework-3.3.0.jar:3.3.0]
    at org.apache.curator.framework.imps.CreateBuilderImpl$17.call(CreateBuilderImpl.java:1058) ~[curator-framework-3.3.0.jar:3.3.0]
    at org.apache.curator.connection.StandardConnectionHandlingPolicy.callWithRetry(StandardConnectionHandlingPolicy.java:67) ~[curator-client-3.3.0.jar:na]
    at org.apache.curator.RetryLoop.callWithRetry(RetryLoop.java:100) ~[curator-client-3.3.0.jar:na]
    at org.apache.curator.framework.imps.CreateBuilderImpl.pathInForeground(CreateBuilderImpl.java:1055) ~[curator-framework-3.3.0.jar:3.3.0]
    at org.apache.curator.framework.imps.CreateBuilderImpl.protectedPathInForeground(CreateBuilderImpl.java:524) ~[curator-framework-3.3.0.jar:3.3.0]
    at org.apache.curator.framework.imps.CreateBuilderImpl.forPath(CreateBuilderImpl.java:514) ~[curator-framework-3.3.0.jar:3.3.0]
    at org.apache.curator.framework.imps.CreateBuilderImpl.forPath(CreateBuilderImpl.java:492) ~[curator-framework-3.3.0.jar:3.3.0]
    at org.apache.curator.framework.imps.CreateBuilderImpl.forPath(CreateBuilderImpl.java:44) ~[curator-framework-3.3.0.jar:3.3.0]
    at com.alibaba.dubbo.remoting.zookeeper.curator.CuratorZookeeperClient.createPersistent(CuratorZookeeperClient.java:56) ~[dubbo-2.5.5.jar:2.5.5]
    ... 33 common frames omitted

3. 问题分析

UnimplementedException 的描述是 Operation is unimplemented,即该操作未实现异常

最后的异常调用栈 ZooKeeper.create(ZooKeeper.java:1297) 方法的源代码:

    /**
     * Create a node with the given path and returns the Stat of that node. The
     * node data will be the given data and node acl will be the given acl.
     */
    public String create(final String path, byte data[], List<ACL> acl,
            CreateMode createMode, Stat stat)
            throws KeeperException, InterruptedException {
        final String clientPath = path;
        PathUtils.validatePath(clientPath, createMode.isSequential());

        final String serverPath = prependChroot(clientPath);

        RequestHeader h = new RequestHeader();
        h.setType(createMode.isContainer() ? ZooDefs.OpCode.createContainer : ZooDefs.OpCode.create2);
        CreateRequest request = new CreateRequest();
        Create2Response response = new Create2Response();
        request.setData(data);
        request.setFlags(createMode.toFlag());
        request.setPath(serverPath);
        if (acl != null && acl.size() == 0) {
            throw new KeeperException.InvalidACLException();
        }
        request.setAcl(acl);
        ReplyHeader r = cnxn.submitRequest(h, request, response, null);
        if (r.getErr() != 0) {
            throw KeeperException.create(KeeperException.Code.get(r.getErr()),
                    clientPath);
        }
        if (stat != null) {
            DataTree.copyStat(response.getStat(), stat);
        }
        if (cnxn.chrootPath == null) {
            return response.getPath();
        } else {
            return response.getPath().substring(cnxn.chrootPath.length());
        }
    }

ZooKeeper.create(ZooKeeper.java:1297) 方法看,是ZooKeeper创建节点(Dubbo注册中心的根目录节点,dubbo.registry.group=dubbo_develop)时发生了异常。

结合 zookeeper学习4之curator开源的zookeeper客户端 – 夢の殇
对该问题的解答:

使用curator的版本必须匹配服务器上安装zookeeper的版本,本人使用的是zookeeper的最新稳定版3.4.9(最新不稳定版是3.5.X),
所以curator不能使用最新版本,否则创建节点时就会报
org.apache.zookeeper.KeeperException$UnimplementedException: KeeperErrorCode = Unimplemented for /curator/test01 错误。
官方上有这段话:

Versions
The are currently two released versions of Curator, 2.x.x and 3.x.x:

Curator 2.x.x - compatible with both ZooKeeper 3.4.x and ZooKeeper 3.5.x
Curator 3.x.x - compatible only with ZooKeeper 3.5.x and includes support for new features such as dynamic reconfiguration, etc.

Apache Curator对ZooKeeper版本兼容性/ZooKeeper Version Compatibility
的最新解释:

ZooKeeper 3.5.x
    Curator 4.0 has a hard dependency on ZooKeeper 3.5.x
ZooKeeper 3.4.x
    Curator 4.0 supports ZooKeeper 3.4.x ensembles in a soft-compatibility mode.

可以确定,问题根源Curator的3.3.0版本和ZooKeeper的3.5.1-alpha版本不匹配ZooKeeper服务器的3.4.9版本

4. 解决方案

  • 将 ZooKeeper JAR版本降到 zookeeper-3.4.9,与服务器版本一样
  • 将 Curator 版本升级到最新的 curator-recipes-4.0.0,或者降到2.x.x最新的 curator-recipes-2.12.0

祝玩得开心!ˇˍˇ

来自为知笔记(Wiz)

CopyOnWriteArrayList和SynchronizedList的执行效率差别

CopyOnWriteArrayList和SynchronizedList这两者循环插入数据时,执行效率都有不利的地方,CopyOnWriteArrayList是每次都要弄个快照,添加完,把快照数据给主数据,再废掉快照,SynchronizedList则是到处加锁,加锁,就需要解锁。

今天闲来无事,自己写个demo来做测试,循环加一百万个数字。JDK版本1.8。
  1. Instant begin = Instant.now();
  2. long beginLong = begin.toEpochMilli();
  3. Collections.synchronizedList(new ArrayList<>(1048576));
  4. for(int i= 0; i<1_000_000; i++) {
  5. list.add(i);
  6. }
  7. Instant end = Instant.now();
  8. long endLong = end.toEpochMilli();
  9. System.out.println((endLong - beginLong));
使用SynchronizedList,执行时间:54ms
再用CopyOnWriteArrayList试试看
  1. Instant begin = Instant.now();
  2. long beginLong = begin.toEpochMilli();
  3. List<Integer> list = new CopyOnWriteArrayList<>(new ArrayList<>(1048576));
  4. for(int i= 0; i<1_000_000; i++) {
  5. list.add(i);
  6. }
  7. Instant end = Instant.now();
  8. long endLong = end.toEpochMilli();
  9. System.out.println((endLong - beginLong));
执行时间:598375ms,接近10分钟了。差距将近1000倍
再用CopyOnWriteArrayList的addAll试试看
  1. Instant begin = Instant.now();
  2. long beginLong = begin.toEpochMilli();
  3. List<Integer> list = Collections.synchronizedList(new ArrayList<>(1048576));
  4. for(int i= 0; i<1_000_000; i++) {
  5. list.add(i);
  6. }
  7. Instant end = Instant.now();
  8. long endLong = end.toEpochMilli();
  9. List<Integer> abcd = new CopyOnWriteArrayList<>();
  10. abcd.addAll(list);
  11. Instant end1 = Instant.now();
  12. long endLong1 = end1.toEpochMilli();
  13. System.out.println((endLong - beginLong));
  14. System.out.println((endLong1 - endLong));
执行结果:3ms。
  1. List<Integer> abcd = new CopyOnWriteArrayList<>(list);
直接将一个List作为初始化时的参数的执行时间也差不多是2ms-3ms。
所以,如果要循环插入大量信息,SynchronizedList的效率较高,CopyOnWriteArrayList基本可以放弃。但如果前面已经有获得的List数据,以addAll的方式或者使用构造方法一次性批量加入CopyOnWriteArrayList,这个效率还是很高的。
来自为知笔记(Wiz)

不常见的408错误

近日,用jenkins打包Java程序,并发布在容器中,打包发布并没有什么问题,当容器启动的时候,mybatis的配置文件发生了错误,是很不常见的408错误,报错:
Caused by: java.io.IOException: Server returned HTTP response code: 408 for URL: http://www.mybatis.org/dtd/mybatis-3-config.dtd 。但是重新发布就好了。

不过还是不要这么麻烦,害得容器都起不来,于是下载了dtd文件,放在src\main\resources目录下,并将配置文件相关部分修改为:
<!DOCTYPE configuration PUBLIC “-//mybatis.org//DTD Config 3.0//EN” “classpath:/mybatis-3-config.dtd”>

再次重新发布,OK。

来自为知笔记(Wiz)

升级Log4j到Log4j2的痛,找不到Zookeeper的类

日前项目将日志软件由Log4j升级到Log4j2,项目使用了dubbo做分布式服务,因此需要使用zkclient连接到Zookeeper集群。
使用Idea的依赖树图,将已有jar包中包含Log4j的,全部选择除去,然后引入Log4j2,slf4j。日志输出格式为Json,因此还需要引入jackson-core和jacksson-databind。修改完之后,将几个子项目一一发布。结果有三个服务报错,错误为
Caused by: java.lang.ClassNotFoundException: org.apache.zookeeper.Watcher$Event$KeeperState
顾名思义,这是表示org.apache.zookeeper.Watcher$Event$KeeperState这个类没找到。但仔细查证,发布的项目中是包含Zookeeper的jar包的。那为什么找不到呢?网上有篇文章说,是消费端启动的时候,服务端没处在服务状态,多次重连之后抛出异常,造成这个错误。这个观点结合我的情况来看,并不适用,报错的子项目所依赖的服务,都很稳定正常地在运行,不存在这种原因。

后来想起来,其他正常运行的子项目和这几个项目有什么区别呢?一比较Zookeeper和Zkclient的部分,果然,exclusions部分不一样,不正常的子项目的zkClient没有这一部分,正常的包含这一部分。于是按照正常的子项目的pom.xml文件进行修改,去除了有关Log4j的依赖,再次发布,项目正常启动了。

综合分析,是因为依赖关系出错,造成zkClient不能正常初始化,因此在调用远程的dubbo服务时,就不能正常连到Zookeeper集群,在程序看来,就类似于消费端启动的时候,服务端没处在服务状态,多次重连之后抛出异常的情况。因此会有这个错误。

来自为知笔记(Wiz)