spring boot + shiro 动态更新用户信息

场景

用户A在线,管理员在后台更改了用户A信息(资料或权限)之后;用户A再进行下一步操作时,会被拦截并退出登录状态,再登录才可以执行操作;来确保用户A的信息同步更新。

后台权限管理系统

技术实现

前篇:

基于前篇,新增功能:

  1. 新增用户表版本version字段;
  2. 更新用户操作时,通过version字段来保证数据一致;
  3. 新增通过拦截器实现动态更新用户信息(用户资料、用户权限);
  4. 新增登录成功后默认页面home.html;
  5. 页面操作细节优化

wyait-manage、wyait-manage-1.2.0源码都更新了以上功能!

以及新增了springboot项目,开发和线上jdk版本不一致导致项目无法启动、无法加载的问题的排查及解决思路。

后篇:

项目源码:(包含数据库源码)

github源码: https://github.com/wyait/manage.git
码云:https://gitee.com/wyait/manage.git
github对应项目源码目录:wyait-manage-1.2.0
码云对应项目源码目录:wyait-manage-1.2.0

动态更新用户权限实现

使用shiro可能会遇到修改了用户权限后,没有立即生效,需要等到用户重新登录后才能生效;不能立即同步更新,显然是不合理的。

解决方案

【实测无效】!!!
授权方法,是在shiro进行鉴权的时候才能触发。只是配置了authc/user/anon等,不会触发;
perms,port,rest,roles,ssl等,会触发授权方法doGetAuthorizationInfo。

  1. 自定义ShiroRealm中添加清除全部用户权限缓存的方法:
/**
 * 清除所有缓存
 */
public void clearCachedAuth(){
    this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
}
  1. 在更新用户权限的地方调用该方法,清除缓存:
//清除ehcache中所有用户权限缓存
RealmSecurityManager rsm = (RealmSecurityManager)SecurityUtils.getSecurityManager();
ShiroRealm authRealm = (ShiroRealm)rsm.getRealms().iterator().next();
authRealm.clearCachedAuth();
  1. 想要达到的效果:在用户进行后台请求的时候,无缓存就会再次请求授权方法,更新自己的权限。
    实测,无效!缓存依然存在!原因可能是使用ehcache导致的。

实际解决方案参考下文中的方案二!

动态更新用户信息(用户资料、用户权限)

在系统中,由管理员更改了用户A信息后,如果用户A在线,无法及时更新相关的改动;
更新用户资料、权限等信息,如果该用户在线,同步更新用户信息解决方案:

方案一【不推荐】:SessionDAO控制

在ShiroRealm中通过SessionDAO拿到所有在线的用户,

Collection<Session> sessions = sessionDAO.getActiveSessions();

遍历找到匹配的,根据情况,退出登录或更新用户信息:

@Autowired

private SessionDAO sessionDAO;

public void updateShiroUser(String loginName){

    Collection<Session> sessions = sessionDAO.getActiveSessions();
    for(Session session:sessions){
        if(loginName.equals(String.valueOf(session.getAttribute(DefaultSubjeContext.PRINCIPALS_SESSION_KEY))) {
            //设置session立即失效,即将其踢出系统
            session.setTimeout(0);
            //TODO 或更新下用户信息
            break;
        }   

    }

不推荐理由

  • 用户数量太大的时候,效率问题。

方案二【推荐】:用户表新增版本控制

用户信息新增version版本标记,写个拦截器,每次请求判断version是否一致,如有改动,根据情况,退出或更新用户信息(本文统一做了退出登录处理,可以结合实际需求做相应调整)。

这个方案,基于乐观锁原理实现。同样可解决动态更新用户权限的问题。

user用户表新增字段version

ALTER TABLE `user`
MODIFY COLUMN `id`  int(10) NOT NULL AUTO_INCREMENT FIRST ,
ADD COLUMN `version`  int(10) NULL DEFAULT 0 COMMENT '更新版本' AFTER `send_time`;

TODO【详见源码】

  • 更新对应的pojo实体类和mapper文件
  • 完善用户更新操作,版本控制;

新建拦截器类

  1. 自定义拦截器UserActionInterceptor实现接口HandlerInterceptor:
/**
 * 
 * @项目名称:wyait-manage
 * @类名称:UserActionInterceptor
 * @类描述:判断用户信息是否已被后台更改,并根据更改的情况做对应的处理
 * @创建人:wyait
 * @创建时间:2018年5月2日 上午9:36:43 
 * @version:
 */
public class UserActionInterceptor implements HandlerInterceptor {

    private static Logger logger = LoggerFactory
            .getLogger(UserActionInterceptor.class);

    @Autowired
    private UserService userService;

    ... ...

    @Override
    public void afterCompletion(HttpServletRequest request,
            HttpServletResponse response, Object obj, Exception e)
            throws Exception {
        // TODO Auto-generated method stub
    }

    @Override
    public void postHandle(HttpServletRequest request,
            HttpServletResponse response, Object obj, ModelAndView mv)
            throws Exception {
        // TODO Auto-generated method stub
    }

    @Override
    public boolean preHandle(HttpServletRequest request,
            HttpServletResponse response, Object obj) throws Exception {
        // TODO Auto-generated method stub
        logger.debug("请求到达后台方法之前调用(controller之前)");
        // 1. SecurityUtils获取session中的用户信息
        // HttpSession session=request.getSession();
        User user = (User) SecurityUtils.getSubject().getPrincipal();
        if (user != null && StringUtils.isNotEmpty(user.getMobile())
                && null != user.getVersion()) {
            // 2. 获取数据库中的用户数据
            User dataUser = this.userService.findUserByMobile(user.getMobile());
            // 3. 对比session中用户的version和数据库中的是否一致
            if (dataUser != null
                    && null != dataUser.getVersion()
                    && String.valueOf(user.getVersion()).equals(
                            String.valueOf(dataUser.getVersion()))) {
                // 3.1 一样,放行
                return true;
            }else{
                // 3.2 不一样,这里统一做退出登录处理;//TODO 使用redis缓存用户权限数据,根据用户更新、用户权限更新;做对应的处理。
                SecurityUtils.getSubject().logout();
                isAjaxResponse(request,response);
            }
        }
        return false;
    }

    ... ...
}
  1. 自定义MyWebMvcConfig继承WebMvcConfigurerAdapter
/**
 * 
 * @项目名称:wyait-manage
 * @类名称:MyWebMvcConfig
 * @类描述:自定义静态资源映射路径和静态资源存放路径
 * @创建人:wyait
 * @修改时间:2018年5月3日09:55:23
 * @version:
 */
@Configuration
public class MyWebMvcConfig extends WebMvcConfigurerAdapter {

    /**
     * 添加拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 路径根据后期项目的扩展,进行调整
        registry.addInterceptor(new UserActionInterceptor())
                .addPathPatterns("/user/**", "/auth/**")
                .excludePathPatterns("/user/sendMsg", "/user/login");
        super.addInterceptors(registry);
    }

}
  1. IStatusMessage接口和common.js中新增判断‘1102’状态码;【详见源码】
  2. 启动,测试

报错

错误信息:

java.lang.NullPointerException: null
    at com.wyait.manage.interceptor.UserActionInterceptor.preHandle(UserActionInterceptor.java:62) ~[classes/:?]
    at org.springframework.web.servlet.HandlerExecutionChain.applyPreHandle(HandlerExecutionChain.java:133) ~[spring-webmvc-4.3.13.RELEASE.jar:4.3.13.RELEASE]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:962) ~[spring-webmvc-4.3.13.RELEASE.jar:4.3.13.RELEASE]

userService对象为null,无法注入UserService。

    @Autowired
    private UserService userService;
  • 解决方案:
    在Spring添加拦截器之前先创建这个Spring Bean拦截器,这样就能在Spring映射这个拦截器前,把拦截器中的依赖注入的对象给初始化完成了。避免拦截器中注入的对象为null问题。
@Configuration
public class MyWebMvcConfig extends WebMvcConfigurerAdapter {
    /**
     * 
     * @描述:在Spring添加拦截器之前先创建拦截器对象,这样就能在Spring映射这个拦截器前,把拦截器中的依赖注入的对象给初始化完成了。
     * </br>避免拦截器中注入的对象为null问题。
     * @创建人:wyait
     * @创建时间:2018年5月3日 上午10:07:36
     * @return
     */
    @Bean
    public UserActionInterceptor userActionInterceptor(){
        return new UserActionInterceptor();
    }

    /**
     * 添加拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 路径根据后期项目的扩展,进行调整
        registry.addInterceptor(userActionInterceptor())
                .addPathPatterns("/user/**", "/auth/**")
                .excludePathPatterns("/user/sendMsg", "/user/login");
        super.addInterceptors(registry);
    }

}

注意

分布式或集群的时候,需要解决session共享问题;相关的方案有:session持久化、redis或其他中间件、nginx的ip_hash、cookie实现、服务器间Session同步等;这时候处理动态更新用户信息,需要结合实际情况而定;

后期会更新redis版本。

jdk版本引发的错误

部署环境

linux 6.* 系统

jdk 1.7
tomcat 7.*
maven 3.3.3

线上tomcat启动logs

五月 08, 2018 11:49:23 上午 org.apache.catalina.startup.TaglibUriRule body
信息: TLD skipped. URI: http://shiro.apache.org/tags is already defined
五月 08, 2018 11:49:23 上午 org.apache.catalina.startup.TldConfig execute
信息: At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.
五月 08, 2018 11:51:03 上午 org.apache.catalina.util.SessionIdGeneratorBase createSecureRandom
信息: Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [112,917] milliseconds.
五月 08, 2018 11:51:03 上午 org.apache.catalina.startup.HostConfig deployDirectory
信息: Deployment of web application directory /usr/tools/tomcat-9190/webapps/ROOT has finished in 117,517 ms
五月 08, 2018 11:51:03 上午 org.apache.catalina.startup.Catalina start
信息: Server startup in 117563 ms
五月 08, 2018 11:51:37 上午 org.apache.catalina.util.SessionIdGeneratorBase createSecureRandom
信息: Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [134,439] milliseconds.
...

无明显错误,但是项目没有加载成功,访问是404.

  • 可能的问题点:
At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.

对比本地tomcat7和线上tomcat7启动,会出现tomcat7和8版本冲突问题,本地解决了,线上依旧启动不成功;再百度、google方案,大致是tomcat版本问题而打印的日志信息,按照搜索的方案配置,均未解决。

考虑应该是环境配置的版本不一致导致的项目无法加载启动成功,确认并排查下开发、测试、线上的jdk、tomcat等版本是否一致。

原因及解决方案

  1. jdk版本
    确认linux和window的jdk版本是否一致
    • windows查看jdk版本,dos窗口
java -version

jdk 1.8

  • linux系统查看jdk版本
java -v

jdk 1.7
这就是导致项目在linux系统启动不起来的原因:开发和线上的jdk版本不一致!!!

将windows的jdk版本切换为jdk1.7,重新打开新的dos窗口:java -version;jdk显示为1.7.*。版本切换成功!

  1. maven重新打包,报错:
...
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.
1:compile (default-compile) on project wyait-manage: Fatal error compiling: 无效的
目标发行版: 1.8 -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e swit
ch.
...
  • 查看setting配置文件,找到<profile>配置:
<!--
<profile>    
        <id>jdk-1.8</id>    
        <activation>    
            <activeByDefault>true</activeByDefault>    
            <jdk>1.8</jdk>    
        </activation>    
        <properties>    
            <maven.compiler.source>1.8</maven.compiler.source>    
            <maven.compiler.target>1.8</maven.compiler.target>    
            <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>    
        </properties>     
    </profile> 
    -->

注释掉这一段maven指定jdk为1.8版本的配置;重新打包,成功!

  1. 重新部署到linux系统,启动并访问成功!
    问题解决。

总结

开发过程中,要确保开发、测试、线上配置的环境(jdk、maven、tomcat等开发依赖的环境支持)保持一致。避免出现由于开发环境中的版本不一致而出现问题,导致项目上线出问题和延迟项目上线时间!


前篇:

后篇:

项目源码:(包含数据库源码)
github源码: https://github.com/wyait/manage.git
码云:https://gitee.com/wyait/manage.git
github对应项目源码目录:wyait-manage-1.2.0
码云对应项目源码目录:wyait-manage-1.2.0