Shiro完成RestfulApi的会话保持实例

背景

针对用户身份权限管理包含账户权限登录认证+会话保持两个部分,在 移动端+服务平台前后端分离 的项目框架下,一般会涉及到通过token来进行用户登录会话的保持。
以下我将通过在HTTP Header中增加token的方式在RestfulApi的服务端进行权限校验与会话保持。

扩展分析

  1. 扩展org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO: 完成用户Session的存取。
  2. 扩展org.apache.shiro.web.session.mgt.DefaultWebSessionManager: 完成用户Session标识的获取。

源码

org.wujianjun.apps.web.auth.TokenSessionManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TokenSessionManager extends DefaultWebSessionManager {

public static final String ACCESS_TOKEN = "x-access-token";
private final Logger logger = LoggerFactory.getLogger(getClass());

@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
final String accessToken = WebUtils.toHttp(request).getHeader(this.ACCESS_TOKEN);
if (StringUtils.isBlank(accessToken)) {
return null;
}
// 设置当前session状态
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.URL_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, accessToken);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return accessToken;
}
}

org.wujianjun.apps.web.auth.RedisSessionDAO

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
@Component
public class RedisSessionDAO extends EnterpriseCacheSessionDAO {

@Resource
private RedisTemplate<String, String> redisTemplate;

public RedisSessionDAO(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}

public RedisTemplate<String, String> getRedisTemplate() {
return redisTemplate;
}

public void setRedisTemplate(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}

@Override
protected Session doReadSession(Serializable serializable) {
final Object validAccessToken = redisTemplate.opsForHash().get(RedisConst.REDIS_ACCESS_TOKEN_KEY, serializable);
if (validAccessToken == null) {
return null;
}
final SimpleSession simpleSession = new SimpleSession();
simpleSession.setId(serializable);
final SysUser sysUser = JSON.parseObject(validAccessToken.toString(), SysUser.class);
simpleSession.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, new SimplePrincipalCollection(sysUser, "authorRealm"));
simpleSession.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);
return simpleSession;
}

@Override
protected void doUpdate(Session session) {
PrincipalCollection existingPrincipals = (PrincipalCollection)session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (existingPrincipals == null) {
return;
}
final Object primaryPrincipal = existingPrincipals.getPrimaryPrincipal();
if (primaryPrincipal instanceof SysUser) {
final SysUser sysUser = (SysUser)primaryPrincipal;
redisTemplate.opsForHash().put(RedisConst.REDIS_ACCESS_TOKEN_KEY, session.getId(), JSON.toJSONString(sysUser));
}
}

@Override
protected void doDelete(Session session) {
redisTemplate.opsForHash().delete(RedisConst.REDIS_ACCESS_TOKEN_KEY, session.getId());
}
}

spring-shiro.xml

1
2
3
4
5
6
7
8
9
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager" p:realm-ref="authorRealm">
<property name="cacheManager">
<bean class="org.apache.shiro.cache.ehcache.EhCacheManager" />
</property>
<property name="sessionManager">
<bean class="org.wujianjun.apps.web.auth.TokenSessionManager" p:sessionDAO-ref="redisSessionDAO"
p:deleteInvalidSessions="false" p:sessionIdCookieEnabled="false" p:sessionValidationSchedulerEnabled="false"/>

</property>
</bean>

SessoinId扩展

如果需要自己定义sessionId的生成,只需要给 org.apache.shiro.session.mgt.eis.AbstractSessionDAO 设置sessionIdGenerator的属性值即可。

1
2
3
4
5
<bean class="org.wujianjun.apps.web.auth.RedisSessionDAO">
<property name="sessionIdGenerator">
<bean class="org.wujianjun.apps.web.auth.JWTSessionIdGenerator" />
</property>
</bean>


观点仅代表自己,期待你的留言。

使用openssl与keytool完成https配置实例(转)

keytool单向认证实例

1) 为服务器生成证书

1
wujianjun@smzc ~ $ keytool -genkey -keyalg RSA -dname "cn=127.0.0.1,ou=inspur,o=none,l=shandong,st=jinan,c=cn" -alias server -keypass 111111 -keystore server.keystore -storepass 111111 -validity 3650

注:cn=127.0.0.1配置的是服务器IP

2) 生成csr

生成csr文件用于提交CA认证生成证书使用。

1
wujianjun@smzc ~ $ keytool -certReq -alias server -keystore server.keystore -file ca.csr

3) 生成cer

这个ca.cer是为了解决不信任时要导入的

1
wujianjun@smzc ~ $ keytool -export -alias server -keystore server.keystore -file ca.cer -storepass 111111

4) tomcat配置ssl

clientAuth=”false”代表单向认证,配置如下:

1
2
3
4
5
<Connector SSLEnabled="true" clientAuth="false"
maxThreads="150" port="8443"
protocol="org.apache.coyote.http11.Http11Protocol"或者HTTP/1.1
scheme="https" secure="true" sslProtocol="TLS"
keystoreFile="/home/server.keystore" keystorePass="111111"/>

注: Http11Protocol支持HTTP/1.1协议,是http1.1协议的ProtocolHandler实现。

5) 常见问题

  • 服务器的证书不受信任
    解决方法:
    1、选择“继续前往(不安全)”,也能访问,但是此时就是以普通的HTTP方式进行信息传输了。
    2、选择生成的ca.cer文件,将证书存储在 “受信任的证书颁发机构” ,就可以通过HTTPS正常访问了。
  • 程序访问Https异常
    sun.security.validator.ValidatorException: PKIX path building failed…
    需要将生成的证书(ca.cer ) 导入到jdk中
    执行以下命令:
    1
    wujianjun@smzc ~ $ keytool -import -alias tomcatsso -file "ca.cer" -keystore "D:\java\jdk1.6.0_11\jre\lib\security\cacerts" -storepass changeit

其中changeit是jre默认的密码。
No subject alternative names present,请在生成keystore 注意CN必须要为域名(或机器名称)例如 localhost 不能为IP 。
No name matching localhost found,表示你生成keystore CN的名称和你访问的名称不一致。

openssl双向认证实例

Linux环境下,在home下建立out32dll目录,在此目录下建立ca、client、server三个文件夹。以下命令均在out32dll目录下执行。

1) 模拟CA生成证书

  • 创建私钥

    1
    wujianjun@smzc ~/out32dll $ openssl genrsa -out ca/ca-key.pem 1024
  • 创建证书请求

    1
    wujianjun@smzc ~/out32dll $ openssl req -new -out ca/ca-req.csr -key ca/ca-key.pem
  • 自签署证书

    1
    wujianjun@smzc ~/out32dll $ openssl x509 -req -in ca/ca-req.csr -out ca/ca-cert.pem -signkey ca/ca-key.pem -days 3650
  • 将证书导出成浏览器支持的.p12格式 (供浏览器不受信任时导入)

    1
    wujianjun@smzc ~/out32dll $ openssl pkcs12 -export -clcerts -in ca/ca-cert.pem -inkey ca/ca-key.pem -out ca/ca.p12

密码:111111

2) 生成Server证书

  • 创建私钥

    1
    wujianjun@smzc ~/out32dll $ openssl genrsa -out server/server-key.pem 1024
  • 创建Server证书请求

    1
    wujianjun@smzc ~/out32dll $ openssl req -new -out server/server-req.csr -key server/server-key.pem
  • 使用CA证书签署Server证书

    1
    wujianjun@smzc ~/out32dll $ openssl x509 -req -in server/server-req.csr -out server/server-cert.pem -signkey server/server-key.pem -CA ca/ca-cert.pem -CAkey ca/ca-key.pem -CAcreateserial -days 3650
  • 将证书导出成浏览器支持的.p12格式

    1
    wujianjun@smzc ~/out32dll $ openssl pkcs12 -export -clcerts -in server/server-cert.pem -inkey server/server-key.pem -out server/server.p12

密码:111111

3) 生成Clinet证书

  • 创建私钥

    1
    wujianjun@smzc ~/out32dll $ openssl genrsa -out client/client-key.pem 1024
  • 创建Client证书请求

    1
    wujianjun@smzc ~/out32dll $ openssl req -new -out client/client-req.csr -key client/client-key.pem
  • 使用CA证书签署Client证书

    1
    wujianjun@smzc ~/out32dll $ openssl x509 -req -in client/client-req.csr -out client/client-cert.pem -signkey client/client-key.pem -CA ca/ca-cert.pem -CAkey ca/ca-key.pem -CAcreateserial -days 3650
  • 将证书导出成浏览器支持的.p12格式

    1
    wujianjun@smzc ~/out32dll $ openssl pkcs12 -export -clcerts -in client/client-cert.pem -inkey client/client-key.pem -out client/client.p12

密码:111111

4) 根据CA证书生成jks文件

1
wujianjun@smzc ~/out32dll $ keytool -keystore truststore.jks -keypass 222222 -storepass 222222 -alias ca -import -trustcacerts -file /home/out32dll/ca/ca-cert.pem

5) 服务器配置(tomcat6示例)

修改conf/server.xml。 将keystoreFile、truststoreFile的路径填写为正确的放置路径。

1
2
3
4
5
<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"
maxThreads="150" scheme="https" secure="true"
clientAuth="true" sslProtocol="TLS"
keystoreFile="/home/out32dll/server/server.p12" keystorePass="111111" keystoreType="PKCS12"
truststoreFile="/home/out32dll/truststore.jks" truststorePass="222222" truststoreType="JKS"/>

6) 导入证书(IE示例)

将ca.p12,client.p12分别导入到IE中去(打开IE->Internet选项->内容->证书)。 ca.p12导入至 受信任的根证书颁发机构,client.p12导入至个人。
进行浏览器访问双向Https时会弹出客户端证书供选择


观点仅代表自己,期待你的留言。

Linux常用命令备注(nslookup,find,grep,sed,awk)

nslookup 指定DNS服务器解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
wujianjun@smzc ~ $ nslookup m.vvip-u.com 10.28.17.101
Server: 10.28.17.101
Address: 10.28.17.101#53

Name: m.vvip-u.com
Address: 10.28.17.80

wujianjun@smzc ~ $ nslookup m.vvip-u.com
Server: 127.0.1.1
Address: 127.0.1.1#53

Non-authoritative answer:
Name: m.vvip-u.com
Address: 120.77.124.39

BIND 局域网DNS程序

https://www.isc.org/downloads/bind/

find包含某关键字的文件内容

1
wujianjun@smzc ~ $ find /smapp/logs -name “*” | xargs grep “keywords”
1
wujianjun@smzc ~ $ grep -nR "keywords" /smapp/logs

grep + RegExp (提取行首不是abc的行)

1
wujianjun@smzc ~ $ grep “^[^abc]” /smapp/logs

文本替换

1
wujianjun@smzc ~ $ sed -n ‘s/oldk/newk/g’ file

先删除1到3行,然后用bb替换aa;

1
wujianjun@smzc ~ $ sed -e ’1,3d’ -e ‘s/aa/bb/g’ file

文本处理

打印所有内容行(相当于cat)

1
2
3
4
5
wujianjun@smzc ~ $ awk '{print $0}' result.txt
18100000011 - "status":"SUCCESS"
18100000012 - "status":"SUCCESS"
18100000013 - "status":"SUCCESS"
18100000014 - "status":"SUCCESS"

空格分隔逐行内容并打印第一个内容

1
2
3
4
5
wujianjun@smzc ~ $ awk '{print $1}' result.txt
18100000011
18100000012
18100000013
18100000014


观点仅代表自己,期待你的留言。

Shiro完成短信验证码登录的实例

分析

Shiro通过org.apache.shiro.realm.Realm进行身份与权限的校验,通过org.apache.shiro.realm.jdbc.JdbcRealm来查看,
我决定继承自org.apache.shiro.realm.AuthorizingRealm来实现身份校验逻辑和权限标识符获取的逻辑。

org.apache.shiro.realm.AuthorizingRealm
两个抽象方法:
1、 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection):
主要用于通过当前身份来获取Permissions。

2、 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException
主要是用户在登录时调用此方法完成用户身份初步校验,注意:校验凭证(密码、验证码等)由Shiro进行校验不需要手动在此进行校验。

源码

spring-shiro.xml

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
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm">
<bean class="com.smzc.apps.order.web.auth.OrderAuthorizingRealm" />
</property>
<property name="cacheManager">
<bean class="org.apache.shiro.cache.ehcache.EhCacheManager" />
</property>
<property name="sessionManager">
<bean class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="sessionIdCookie">
<bean class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="SESSIONID"/>
<property name="httpOnly" value="true"/>
<property name="maxAge" value="-1"/>
</bean>
</property>
</bean>
</property>
</bean>

<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager" />
<property name="arguments">
<list>
<ref bean="securityManager"/>
</list>
</property>
</bean>

<!-- shiroFilter -->
<bean id="shiroFilterFactoryBean" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- Shiro的核心安全接口,这个属性是必须的 -->
<property name="securityManager" ref="securityManager" />
<!-- 要求登录时的链接,非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面 -->
<property name="loginUrl" value="/views/login" />
<!-- 登录成功后要跳转的连接 -->
<property name="successUrl" value="/views/index" />
<!-- 用户访问未对其授权的资源时,所显示的连接 -->
<property name="unauthorizedUrl" value="/views/common/unauthorized" />
<property name="filters">
<map>
<entry key="logout">
<bean class="org.apache.shiro.web.filter.authc.LogoutFilter">
<property name="redirectUrl" value="/views/login"/>
</bean>
</entry>
</map>
</property>
<!-- 配置Shiro过滤链 -->
<property name="filterChainDefinitions">
<value>
/api/user/login/** = anon
/api/** = authc
/logout = logout
/** = anon
</value>
</property>
</bean>

OrderAuthorizingRealm.java

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
public class OrderAuthorizingRealm extends AuthorizingRealm implements InitializingBean {
@Resource
private AuthorizingService authorizingService;
@Value("${app.role.permissions:}")
private String rolePermissionsString;
private Map<String, Set<String>> rolePermissions;

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
final SysUser sysUser = (SysUser)super.getAvailablePrincipal(principalCollection);
final String role = sysUser.getUserRole().getRole();
final SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(Sets.newHashSet(role));
authorizationInfo.setStringPermissions(rolePermissions.get(role));
return authorizationInfo;
}

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
final UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
final String mobile = usernamePasswordToken.getUsername();
final String validateCode = authorizingService.getCodeByMobile(mobile);
if (StringUtils.isBlank(validateCode)) {
throw new ExpiredCredentialsException("验证码已失效#[" + mobile + "]");
}
final List<SysUser> sysUserList = authorizingService.getUserByMobile(mobile);
if (CollectionUtils.isEmpty(sysUserList)) {
throw new UnknownAccountException("用户账户不存在#[" + mobile + "]");
}
if (sysUserList.size() > 1) {
throw new ConcurrentAccessException("用户存在多个账户#[" + mobile + "]");
}
final SysUser sysUser = sysUserList.iterator().next();
if (!sysUser.getStatus().isAllowLogin()) {
throw new DisabledAccountException("用户账户不可用#[" + mobile + "]");
}
// 注意:SimpleAuthenticationInfo中principal表示验证主体,供后续获取权限标识符和当前登录用户信息使用, credentials表示正确的凭证串,shiro会自动与用户登录时填入的值进行密钥匹配后进行对比。
// credentials也可以通过setCredentialsSalt设置加密的salt
return new SimpleAuthenticationInfo(sysUser, validateCode.toCharArray(), super.getName());
}

@Override
public void afterPropertiesSet() throws Exception {
if (StringUtils.isBlank(rolePermissionsString)) {
throw new NullPointerException("未初始化权限配置");
}
rolePermissions = Maps.newHashMap();
final JSONObject jsonObject = JSON.parseObject(rolePermissionsString);
final Iterator<Map.Entry<String, Object>> entryIterator = jsonObject.entrySet().iterator();
while (entryIterator.hasNext()) {
final Map.Entry<String, Object> objectEntry = entryIterator.next();
rolePermissions.put(objectEntry.getKey(), Sets.newHashSet(objectEntry.getValue().toString().split(",")));
}
}
}

UserApiController.java

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
@RestController
@RequestMapping(value = "/api/user", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public class UserApiController extends BasicApiController {

@Resource
private SysUserService userService;

/**
* 用户登录
*/

@RequestMapping(value = "/login/{mobileNo}/{code}", method = RequestMethod.GET)
public Output login(@PathVariable String mobileNo, @PathVariable String code) throws ServiceException {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(mobileNo, code);
subject.login(token);
token.clear();
return MapOutput.createSuccess();
}

/**
* 用户登出
*/

@RequestMapping(value = "/logout/{mobileNo}", method = RequestMethod.GET)
public Output logout(@PathVariable String mobileNo) throws ServiceException {
SecurityUtils.getSubject().logout();
return MapOutput.createSuccess();
}
}

Shiro凭证匹配器配置

加密方式都为org.apache.shiro.authc.credential.CredentialsMatcher的实现类
通过org.apache.shiro.realm.AuthenticatingRealmprivate CredentialsMatcher credentialsMatcher设置凭据的匹配实现类
如:

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
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm">
<!--BEGIN: 设置凭证器------------------>
<bean class="com.smzc.apps.order.web.auth.OrderAuthorizingRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.PasswordMatcher" />
</property>
</bean>
<!--END: 设置凭证器------------------>
</property>
<property name="cacheManager">
<bean class="org.apache.shiro.cache.ehcache.EhCacheManager" />
</property>
<property name="sessionManager">
<bean class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="sessionIdCookie">
<bean class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="SESSIONID"/>
<property name="httpOnly" value="true"/>
<property name="maxAge" value="-1"/>
</bean>
</property>
</bean>
</property>
</bean>

Shiro内置的FilterChain

  1. Shiro验证URL时,URL匹配成功便不再继续匹配查找(所以要注意配置文件中的URL顺序,尤其在使用通配符时)
    故filterChainDefinitions的配置顺序为自上而下,以最上面的为准
  2. 当运行一个Web应用程序时,Shiro将会创建一些有用的默认Filter实例,并自动地在[main]项中将它们置为可用
    自动地可用的默认的Filter实例是被DefaultFilter枚举类定义的,枚举的名称字段就是可供配置的名称
    anon—————org.apache.shiro.web.filter.authc.AnonymousFilter
    authc————–org.apache.shiro.web.filter.authc.FormAuthenticationFilter
    authcBasic———org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
    logout————-org.apache.shiro.web.filter.authc.LogoutFilter
    noSessionCreation–org.apache.shiro.web.filter.session.NoSessionCreationFilter
    perms————–org.apache.shiro.web.filter.authz.PermissionAuthorizationFilter
    port—————org.apache.shiro.web.filter.authz.PortFilter
    rest—————org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
    roles————–org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
    ssl—————-org.apache.shiro.web.filter.authz.SslFilter
    user—————org.apache.shiro.web.filter.authz.UserFilter

  3. 通常可将这些过滤器分为两组
    anon,authc,authcBasic,user是第一组认证过滤器
    perms,port,rest,roles,ssl是第二组授权过滤器
    注意user和authc不同:当应用开启了rememberMe时,用户下次访问时可以是一个user,但绝不会是authc,因为authc是需要重新认证的
    user表示用户不一定已通过认证,只要曾被Shiro记住过登录状态的用户就可以正常发起请求,比如rememberMe
    说白了,以前的一个用户登录时开启了rememberMe,然后他关闭浏览器,下次再访问时他就是一个user,而不会authc

  4. 举几个例子
    /admin=authc,roles[admin] 表示用户必需已通过认证,并拥有admin角色才可以正常发起’/admin’请求
    /edit=authc,perms[admin:edit] 表示用户必需已通过认证,并拥有admin:edit权限才可以正常发起’/edit’请求
    /home=user 表示用户不一定需要已经通过认证,只需要曾经被Shiro记住过登录状态就可以正常发起’/home’请求

  5. 各默认过滤器常用如下(注意URL Pattern里用到的是两颗星,这样才能实现任意层次的全匹配)
    /admins/=anon 无参,表示可匿名使用,可以理解为匿名用户或游客
    /admins/user/
    =authc 无参,表示需认证才能使用
    /admins/user/=authcBasic 无参,表示httpBasic认证
    /admins/user/
    =user 无参,表示必须存在用户,当登入操作时不做检查
    /admins/user/=ssl 无参,表示安全的URL请求,协议为https
    /admins/user/
    =perms[user:add:]
    参数可写多个,多参时必须加上引号,且参数之间用逗号分割,如/admins/user/**=perms[“user:add:
    ,user:modify:“]
    当有多个参数时必须每个参数都通过才算通过,相当于isPermitedAll()方法
    /admins/user/=port[8081]
    当请求的URL端口不是8081时,跳转到schemal://serverName:8081?queryString
    其中schmal是协议http或https等,serverName是你访问的Host,8081是Port端口,queryString是你访问的URL里的?后面的参数
    /admins/user/
    =rest[user]
    根据请求的方法,相当于/admins/user/=perms[user:method],其中method为post,get,delete等
    /admins/user/
    =roles[admin]
    参数可写多个,多个时必须加上引号,且参数之间用逗号分割,如/admins/user/*
    =roles[“admin,guest”]
    当有多个参数时必须每个参数都通过才算通过,相当于hasAllRoles()方法


观点仅代表自己,期待你的留言。

Java对中国夏令时的展示

中国夏令时制度实行时间

中华人民共和国在1986年~1991年实行了夏令时制度,每年夏令时实行时间如下:

1
2
3
4
5
6
1986年5月4日至9月14日(1986年因是实行夏令时的第一年,从5月4日开始到9月14日结束)
1987年4月12日至9月13日,
1988年4月10日至9月11日,
1989年4月16日至9月17日,
1990年4月15日至9月16日,
1991年4月14日至9月15日。

JDK已有对夏令时的处理

Java的jdk在Date的toString中已经包含夏令时的计算,以下代码可以印证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws Exception {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String sTime = "1986-09-13 22:00:00";
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
Date time = sdf.parse(sTime);
System.out.println(time.getTime());
System.out.println(time);
Calendar cd = Calendar.getInstance();
cd.setTime(time);
// 2小时以后是几点?
cd.add(Calendar.HOUR, 2);
time = cd.getTime();
System.out.println("------------------------------");
System.out.println(time.getTime());
System.out.println(time);
}

打印结果:

1
2
3
4
5
527000400000
Sat Sep 13 22:00:00 CDT 1986
------------------------------
527007600000
Sat Sep 13 23:00:00 CST 1986

分析: 上面代码中1986-09-1322:00:00加上2小时,应该变为1986-09-13 24:00:00(或者1986-09-14 00:00:00),但由于在9月14日零点退出夏令时,时钟向后调整1小时,实际变为1986-09-13 23:00:00。
注意:从9月14日零点退出夏令时,java的Date.toString打印的时区也从CDT恢复为CST( ChinaStandard Time UT+8:00)。

又如:

1
2
3
4
5
6
wujianjun@smzc ~ $ date
2018年 08月 24日 星期五 19:20:41 CST
wujianjun@smzc ~ $ date -d @579279600
1988年 05月 11日 星期三 00:00:00 CDT
wujianjun@smzc ~ $ date -d @599587200
1989年 01月 01日 星期日 00:00:00 CST

结论: 只要是在实行夏令时的时段都是CDT时间,其它都是CST


观点仅代表自己,期待你的留言。

同一微服务连接多套RocketMQ集群

需要实现的通讯消息架构

通讯消息连接架构

分析

: 以下分析均在rocketmq4.0.0-incubating源码上进行

org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#start(boolean)

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
public void start(final boolean startFactory) throws MQClientException {
switch (this.serviceState) {
case CREATE_JUST:
this.serviceState = ServiceState.START_FAILED;

this.checkConfig();

if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
this.defaultMQProducer.changeInstanceNameToPID();
}

this.mQClientFactory = MQClientManager.getInstance().getAndCreateMQClientInstance(this.defaultMQProducer, rpcHook);
// 从上一句可以看出:MQClientManager为Producer连接管理器,用户管理连接MQ的TCP客户端的连接

boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
if (!registerOK) {
this.serviceState = ServiceState.CREATE_JUST;
throw new MQClientException("The producer group[" + this.defaultMQProducer.getProducerGroup()
+ "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
null);
}

this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());

if (startFactory) {
mQClientFactory.start();
}

log.info("the producer [{}] start OK. sendMessageWithVIPChannel={}", this.defaultMQProducer.getProducerGroup(),
this.defaultMQProducer.isSendMessageWithVIPChannel());
this.serviceState = ServiceState.RUNNING;
break;
case RUNNING:
case START_FAILED:
case SHUTDOWN_ALREADY:
throw new MQClientException("The producer service state not OK, maybe started once, "//
+ this.serviceState//
+ FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK),
null);
default:
break;
}

this.mQClientFactory.sendHeartbeatToAllBrokerWithLock();
}

org.apache.rocketmq.client.impl.MQClientManager#getAndCreateMQClientInstance(org.apache.rocketmq.client.ClientConfig, org.apache.rocketmq.remoting.RPCHook)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public MQClientInstance getAndCreateMQClientInstance(final ClientConfig clientConfig, RPCHook rpcHook) {
String clientId = clientConfig.buildMQClientId();
MQClientInstance instance = this.factoryTable.get(clientId);
// 从上一句可以看出:找到对应的客户端通讯实例是通过一个clientId在factoryTable内存缓存中进行查询的

if (null == instance) {
instance =
new MQClientInstance(clientConfig.cloneClientConfig(),
this.factoryIndexGenerator.getAndIncrement(), clientId, rpcHook);
MQClientInstance prev = this.factoryTable.putIfAbsent(clientId, instance);
if (prev != null) {
instance = prev;
log.warn("Returned Previous MQClientInstance for clientId:[{}]", clientId);
} else {
log.info("Created new MQClientInstance for clientId:[{}]", clientId);
}
}

return instance;
}

org.apache.rocketmq.client.ClientConfig#buildMQClientId

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public String buildMQClientId() {
StringBuilder sb = new StringBuilder();
sb.append(this.getClientIP());

sb.append("@");
sb.append(this.getInstanceName());
if (!UtilAll.isBlank(this.unitName)) {
sb.append("@");
sb.append(this.unitName);
}

return sb.toString();
}
// 从以上方法可以看出:这个clientId信息中包含当前微服务的IP,当前模块实例名(默认通过changeInstanceNameToPID更改为进程ID值)和一个unitName

结论: 经以上源码可以得到一个模块需要连接多个RocketMQ集群,则需要生产多个MQClientInstance,换言之,则需要在获取MQClientInstance时传递不同的ClientID即可。
由于ClientID=localIP + instanceName + unitName,所以只需要创建Producer对象时传入不同的instanceName或unitName值即可。


观点仅代表自己,期待你的留言。

Java Script Engine

Java Script Engine

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
public static void main(String[] agrs) throws ScriptException {
final ScriptEngine javascriptEngine = new ScriptEngineManager().getEngineByName("javascript");
final Bindings globalBindings = javascriptEngine.getBindings(ScriptContext.GLOBAL_SCOPE);
globalBindings.put("a", 5);
System.out.println("-------Engine bindings scope--------------");
final Bindings javascriptEngineBindings = javascriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
javascriptEngineBindings.put("x", 20);
javascriptEngineBindings.put("y", 20.4);
javascriptEngineBindings.put("z", 1);
final String[] scriptArray = {"x*y+z", "x*(y+z)", "a+x*(y+z)"};
eval(scriptArray, javascriptEngine);

System.out.println("-------Local bindings scope--------------");
final Bindings localBinding = javascriptEngine.createBindings();
localBinding.put("x", 2);
localBinding.put("y", 3);
localBinding.put("z", 1);
eval(scriptArray, javascriptEngine, localBinding);
}

private static void eval(String[] scriptArray, ScriptEngine javascriptEngine) throws ScriptException {
Bindings aBindings = javascriptEngine.getBindings(ScriptContext.GLOBAL_SCOPE);
for (String key : aBindings.keySet()) {
System.out.println("Args (Global bindings scope) > " + key + "=" + aBindings.get(key));
}
aBindings = javascriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
for (String key : aBindings.keySet()) {
System.out.println("Args (Engine bindings scope) > " + key + "=" + aBindings.get(key));
}
for (String script : scriptArray) {
System.out.println("script > " + script + " = " + javascriptEngine.eval(script));
}
}

private static void eval(String[] scriptArray, ScriptEngine javascriptEngine, Bindings localBinding) throws ScriptException {
Bindings aBindings = javascriptEngine.getBindings(ScriptContext.GLOBAL_SCOPE);
for (String key : aBindings.keySet()) {
System.out.println("Args (Global bindings scope) > " + key + "=" + aBindings.get(key));
}
aBindings = javascriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
for (String key : aBindings.keySet()) {
System.out.println("Args (Engine bindings scope) > " + key + "=" + aBindings.get(key));
}
for (String s : localBinding.keySet()) {
System.out.println("Args (Local bindings scope) > " + s + "=" + aBindings.get(s));
}
for (String script : scriptArray) {
System.out.println("script > " + script + " = " + javascriptEngine.eval(script, localBinding));
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-------Engine bindings scope--------------
Args (Global bindings scope) > a=5
Args (Engine bindings scope) > z=1
Args (Engine bindings scope) > y=20.4
Args (Engine bindings scope) > x=20
script > x*y+z = 409.0
script > x*(y+z) = 428.0
script > a+x*(y+z) = 433.0
-------Local bindings scope--------------
Args (Global bindings scope) > a=5
Args (Engine bindings scope) > println=sun.org.mozilla.javascript.internal.InterpretedFunction@45a23f67
Args (Engine bindings scope) > context=javax.script.SimpleScriptContext@1ef0a6e8
Args (Engine bindings scope) > z=1
Args (Engine bindings scope) > print=sun.org.mozilla.javascript.internal.InterpretedFunction@495dd936
Args (Engine bindings scope) > y=20.4
Args (Engine bindings scope) > x=20
Args (Local bindings scope) > z=1
Args (Local bindings scope) > y=20.4
Args (Local bindings scope) > x=20
script > x*y+z = 7.0
script > x*(y+z) = 8.0
script > a+x*(y+z) = 13.0

Process finished with exit code 0

Bindings变量的有效范围

  1. Global对应到ScriptEngineFactory,通过scriptEngine.getBindings(ScriptContext.GLOBAL_SCOPE)获得。
  2. Engine对应到ScriptEngine,通过scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE)获得。
  3. Local Binding每一次执行script,通过scriptEngine.createBindings()获得。

使用场景适用于

  1. 规则引擎
  2. 流程流转条件判定

观点仅代表自己,期待你的留言。

redis进阶

redis主从(读写分离)

问题:

  • redis为单进程程序,只能占用单核,无法充分使用多核的系统资源。
  • 单节点redis在出现故障时则无法继续提供数据存储服务,无法达到高可用要求。
    解决方法:
  • 增加redis slave节点通过replicas进行master-slave的数据复制,故障时手动恢复。
  • 通过增加sentinel(哨兵)监控redis master-slave节点的存活状态,当master出现故障时自动将slave升级为master继续提供服务。

data sharding(数据分片)

问题:
单台redis节点的内存总量有限,达到上限后想要扩容除了增加内存别无它法
解决方法:

  • redis client (部署多个独立的redis节点,通过在客户端代码中针对key进行hash,然后将数据按hash映射存储到不同的redis中)
  • Twemproxy (部署多个独立的redis节点,通过引入twitter开源中间件封装key的hash操作,最终将数据存储到不同的redis中)

redis cluster集群

问题:
单台redis节点的内存容量有限,达到上限后想要扩容除了增加内存别无它法
解决方法:

  • 将所有的redis节点的内存总容量划分为n个哈希槽(hash slot)(默认为16384个),每一个redis节点负责一段solt,存取数据时通过CRC16算法对key进行取余来决定应该从哪一个redis节点进行存取操作。
    redis节点两两连接共同形成一个集群,客户端代码连接集群中任意节点进行存取服务,集群中的节点会通过CRC16算法来将存取请求转发到目标redis节点完成数据的存取。
  • 通过在集群内挑选一部分节点设置为slave节点,通过master-slave构建高可用redis服务

RocketMQ使用指南及参数详解

一、使用指南

  • 客户端寻址方式

在代码中指定NameServer地址

1
Producer.setNamesrvAddr(“192.168.8.106:9876”);


1
Consumer.setNamesrvAddr(“192.168.8.106:9876”);

Java启动参数中指定NameServer地址

1
-Drocketmq.namesrv.addr=192.168.8.106:9876

环境变量指定NameServer地址·

1
export NAMESRV_ADDR=192.168.8.106:9876

  • http静态服务器寻址

客户端启动后,会定时访问一个静态的HTTP服务器,地址如下:

http://jmenv.tbsite.net:8080/rocketmq/msaddr

这个URL的返回内容如下:

192.168.8.106:9876

客户端默认每隔2分钟访问一次这个HTTP服务器,并更新本地的NameServer地址。URL已经在代码中写死,可通过修改/etc/hosts文件来改变要访问的服务器,例如在/etc/hosts增加如下配置:

10.232.22.67 jmenv.taobao.net

二、参数详解

  • 客户端的公共配置类:ClientConfig
参数名 默认值 说明
NamesrvAddr NameServer地址列表,多个nameServer地址用分号隔开
clientIP 本机IP 客户端本机IP地址,某些机器会发生无法识别客户端IP地址情况,需要应用在代码中强制指定
instanceName DEFAULT 客户端实例名称,客户端创建的多个Producer,Consumer实际是共用一个内部实例(这个实例包含网络连接,线程资源等)
clientCallbackExecutorThreads 4 通信层异步回调线程数
pollNameServerInteval 30000 轮训Name Server 间隔时间,单位毫秒
heartbeatBrokerInterval 30000 向Broker发送心跳间隔时间,单位毫秒
persistConsumerOffsetInterval 5000 持久化Consumer消费进度间隔时间,单位毫秒
  • Producer配置
参数名 默认值 说明
producerGroup DEFAULT_PRODUCER Producer组名,多个Producer如果属于一个应用,发送同样的消息,则应该将它们归为同一组。
createTopicKey TBW102 在发送消息时,自动创建服务器不存在的topic,需要指定key
defaultTopicQueueNums 4 在发送消息时,自动创建服务器不存在的topic,默认创建的队列数
sendMsgTimeout 10000 发送消息超时时间,单位毫秒
compressMsgBodyOverHowmuch 4096 消息Body超过多大开始压缩(Consumer收到消息会自动解压缩),单位字节
retryAnotherBrokerWhenNotStoreOK FALSE 如果发送消息返回sendResult,但是sendStatus!=SEND_OK,是否重试发送
maxMessageSize 131072 客户端限制的消息大小,超过报错,同时服务端也会限制(默认128K)
transactionCheckListener 事物消息回查监听器,如果发送事务消息,必须设置
checkThreadPoolMinSize 1 Broker回查Producer事务状态时,线程池大小
checkThreadPoolMaxSize 1 Broker回查Producer事务状态时,线程池大小
checkRequestHoldMax 2000 Broker回查Producer事务状态时,Producer本地缓冲请求队列大小
  • PushConsumer配置
参数名 默认值 说明
consumerGroup DEFAULT_CONSUMER Consumer组名,多个Consumer如果属于一个应用,订阅同样的消息,且消费逻辑一致,则应将它们归为同一组
messageModel CLUSTERING 消息模型,支持以下两种1.集群消费2.广播消费
consumeFromWhere CONSUME_FROM_LAST_OFFSET Consumer启动后,默认从什么位置开始消费
allocateMessageQueueStrategy AllocateMessageQueueAveragely Rebalance算法实现策略
Subscription {} 订阅关系
messageListener 消息监听器
offsetStore 消费进度存储
consumeThreadMin 10 消费线程池数量
consumeThreadMax 20 消费线程池数量
consumeConcurrentlyMaxSpan 2000 单队列并行消费允许的最大跨度
pullThresholdForQueue 1000 拉消息本地队列缓存消息最大数
Pullinterval 0 拉消息间隔,由于是长轮询,所以为0,但是如果应用了流控,也可以设置大于0的值,单位毫秒
consumeMessageBatchMaxSize 1 批量消费,一次消费多少条消息
pullBatchSize 32 批量拉消息,一次最多拉多少条
  • PullConsumer配置
参数名 默认值 说明
consumerGroup Conusmer组名,多个Consumer如果属于一个应用,订阅同样的消息,且消费逻辑一致,则应该将它们归为同一组
brokerSuspendMaxTimeMillis 20000 长轮询,Consumer拉消息请求在Broker挂起最长时间,单位毫秒
consumerPullTimeoutMillis 10000 非长轮询,拉消息超时时间,单位毫秒
consumerTimeoutMillisWhenSuspend 30000 长轮询,Consumer拉消息请求咋broker挂起超过指定时间,客户端认为超时,单位毫秒
messageModel BROADCASTING 消息模型,支持以下两种:1集群消费 2广播模式
messageQueueListener 监听队列变化
offsetStore 消费进度存储
registerTopics 注册的topic集合
allocateMessageQueueStrategy Rebalance算法实现策略
  • Broker配置参数
    查看Broker默认配置
1
sh mqbroker -m
参数名 默认值 说明
consumerGroup Conusmer组名,多个Consumer如果属于一个应用,订阅同样的消息,且消费逻辑一致,则应该将它们归为同一组
listenPort 10911 Broker对外服务的监听端口
namesrvAddr Null Name Server地址
brokerIP1 本机IP 本机IP地址,默认系统自动识别,但是某些多网卡机器会存在识别错误的情况,这种情况下可以人工配置。
brokerName 本机主机名
brokerClusterName DefaultCluster Broker所属哪个集群
brokerId 0 BrokerId,必须是大等于0的整数,0表示Master,>0表示Slave,一个Master可以挂多个Slave,Master和Slave通过BrokerName来配对
storePathCommitLog $HOME/store/commitlog commitLog存储路径
storePathConsumeQueue $HOME/store/consumequeue 消费队列存储路径
storePathIndex $HOME/store/index 消息索引存储队列
deleteWhen 4 删除时间时间点,默认凌晨4点
fileReservedTime 48 文件保留时间,默认48小时
maxTransferBytesOnMessageInMemory 262144 单次pull消息(内存)传输的最大字节数
maxTransferCountOnMessageInMemory 32 单次pull消息(内存)传输的最大条数
maxTransferBytesOnMessageInDisk 65535 单次pull消息(磁盘)传输的最大字节数
maxTransferCountOnMessageInDisk 8 单次pull消息(磁盘)传输的最大条数
messageIndexEnable TRUE 是否开启消息索引功能
messageIndexSafe FALSE 是否提供安全的消息索引机制,索引保证不丢
brokerRole ASYNC_MASTER Broker的角色 -ASYNC_MASTER异步复制Master -SYNC_MASTER同步双写Master -SLAVE
flushDiskType ASYNC_FLUSH 刷盘方式 -ASYNC_FLUSH异步刷盘 -SYNC_FLUSH同步刷盘
cleanFileForciblyEnable TRUE 磁盘满,且无过期文件情况下TRUE表示强制删除文件,优先保证服务可用 FALSE标记服务不可用,文件不删除

转自:https://www.cnblogs.com/xiaodf/p/5075167.html

观点仅代表自己,期待你的留言。