记录shiro新增微信免密登录的开发

需求:

    在原有shiro校验登录的pc项目上,新增一个微信客户端,并且微信客户端绑定后进行免密登录。原有项目是SSM+shiro安全校验项目,采用的是session作为用户区分的单应用项目。

改进方案:

    采用多个shiro Realm来区分来自pc和微信的登录授权鉴权,在微信端第一次登录时,采用用户名和密码校验,成功后把openid插入用户表,之后使用openid进行免密登录,并把sessionid放入请求头中。使用spring cache + ehcache做缓存处理,openid和sessionId作为K,V值存入,缓存失效时间与sessionid失效时间一致。

shiro多Realm登录

重写UsernamePasswordToken

    新增loginType属性,用来区分用户登录来源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class UsernamePasswordLoginTypeToken extends UsernamePasswordToken {
/**
* 0为用户密码登录,1为微信通过用户code获取openid免密登录
*/
private String loginType = "0";

public String getLoginType() {
return loginType;
}
public void setLoginType(String loginType) {
this.loginType = loginType;
}

public UsernamePasswordLoginTypeToken(final String username, final String password,
final boolean rememberMe, final String host,String loginType){
super(username, password, rememberMe, host);
this.loginType = loginType;
}

public UsernamePasswordLoginTypeToken(final String username, final String password) {
super(username, password);
}
}

重新定义一个WxShiroRealm

    继承原来pc端使用的shiroRealm,改写shiro的授权方法,采用openid校验

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
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authcToken) throws AuthenticationException {
logger.error("WxShiro登录认证开始。。。");
UsernamePasswordToken token = (UsernamePasswordToken)authcToken;
UserDTO dto = new UserDTO();
String openId = new String(token.getPassword());
dto.setOPENID(passwordHash.toHex(openId, null));
//查询用户
sys_user user = userService.selectUserByOpenId(dto);
if(null == user){//账号不存在
return null;
}
//获取用户菜单url和角色名称
Map<String, Set<String>> resourceMap =
roleService.selectResourceMapByUserid(user.getUSERID());
logger.error("用户拥有资源"+resourceMap.toString());
Set<String> urls = resourceMap.get("urls");
Set<String> roles = resourceMap.get("roles");
ShiroUser shiroUser = new ShiroUser(user.getUSERID(), user.getUSERNAME(),
user.getNAME(), user.getORGCODE(), urls);
shiroUser.setRoles(roles);
// 采用AuthenticatingRealm的CredentialsMatcher进行密码匹配
return new SimpleAuthenticationInfo(shiroUser,
user.getOPENID(), getName());
}

重写shiro模块化用户验证器

    重写shiroRealm中的support方法,自定义校验获取唯一匹配的realm;并新增一个supportedLoginType属性用来记录该realm的初始化登录校验来源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 private String supportedLoginType;
public String getSupportedLoginType() {
return supportedLoginType;
}

public void setSupportedLoginType(String supportedLoginType) {
this.supportedLoginType = supportedLoginType;
}
/**
*
*/
@Override
public boolean supports(AuthenticationToken token) {
if (token instanceof UsernamePasswordLoginTypeToken) {
UsernamePasswordLoginTypeToken usernamePasswordLoginTypeToken =
(UsernamePasswordLoginTypeToken)token;
return getSupportedLoginType().equals(usernamePasswordLoginTypeToken.getLoginType());
}
return false;
}

    shiro模块化用户验证器,来确定唯一进行校验的realm

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
/**
* @ClassName: MyModularRealmAuthenticator
* @Description: 重写shiro模块化用户验证器,根据loginType参数,获取唯一匹配的realm
* @author: stepwen
* @Date: 2018/11/16 15:09
**/
public class MyModularRealmAuthenticator extends ModularRealmAuthenticator {
private Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
Realm uniqueRealm = getUniqueRealm(realms, token);
if (null == uniqueRealm) {
throw new UnsupportedTokenException("没有匹配类型的realm");
}
return uniqueRealm.getAuthenticationInfo(token);
}

/**
* 判断realms是否匹配,并返回唯一匹配的realm;没有匹配的返回null
*
* @param realms realm 集合
* @param token 登录信息
* @return
*/
private Realm getUniqueRealm(Collection<Realm> realms, AuthenticationToken token) {
for (Realm realm : realms) {
if (realm.supports(token)) {
return realm;
}
}

logger.error("-----------------------------没有可用的realm-----------------------------");
return null;
}
}

配置shiro多realm

    在spring-shiro.xml中定义新建WxShiroRealm的bean,securityManager中定义的realm改成多个配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 安全管理器  -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="authenticator" ref="authenticator"></property>
<!--设置自定义Realm
<property name="realm" ref="MyShiroRealm"/>-->
<!-- 设置多个realm -->
<property name="realms">
<list>
<ref bean="MyShiroRealm"/>
<ref bean="WxShiroRealm"/>
</list>
</property>
<!--将缓存管理器,交给安全管理器-->
<property name="cacheManager" ref="shiroSpringCacheManager"/>
<!-- 记住密码管理 -->
<property name="rememberMeManager" ref="rememberMeManager"/>
<!-- 会话管理 -->
<property name="sessionManager" ref="sessionManager"/>
</bean>

    还需要加上一个认证策略配置,使shiro只要一个realm认证成功就返回。默认提供了三种认证策略:

  1. FirstSuccessfulStrategy:只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息,其他的忽略;
  2. AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和FirstSuccessfulStrategy不同,返回所有Realm身份验证成功的认证信息;
  3. AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,如果有一个失败就失败了。
    1
    2
    3
    4
    5
    6
    <bean id="authenticator" class="com.zxw.zhby.commons.shiro.MyModularRealmAuthenticator">
    <!-- 配置认证策略,只要有一个Realm认证成功即可,并且返回所有认证成功信息 -->
    <property name="authenticationStrategy">
    <bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
    </property>
    </bean>

微信免密登录

微信登陆接口

     登录成功之后,将用户openid写入用户表中

1
2
3
4
5
6
7
8
UsernamePasswordLoginTypeToken token = new UsernamePasswordLoginTypeToken(username, password);
// 设置记住密码
token.setRememberMe(1 == rememberMe);
try {
user.login(token);
//登录成功之后,把openid写入用户表中用来做免密登录
String openId = wxService.getOpenidByCode(code);
userService.updateOpenId(username, openId);

免密登录写入ehcache缓存

     @Cacheable springCache的注解,默认方式用参数作为K,返回结果作为V;每次调用添加注解的方法时,都会先从缓存中查询,能够查询到就直接返回,如果查询不到,调用方法重新获取返回值,并把参数、返回值写入缓存以便下次调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Cacheable
public String getSessionId(String openid) {
//通过openid,进行免密登录,获取返回的sessionId
Subject user = SecurityUtils.getSubject();
UsernamePasswordLoginTypeToken token = new UsernamePasswordLoginTypeToken("WX", openid);
//设置为微信免密登录
token.setLoginType("1");
try {
user.login(token);
return user.getSession().getId().toString();
} catch (Throwable e) {
e.printStackTrace();
return null;
}
}