5.3 Shiro会话时长配置

使用Shiro框架实现认证操作,用户登录成功会将用户信息写入到会话对象中,其默认时长为30分钟,假如需要对此进行配置,可参考如下配置:

第一步:在SpringShiroConfig类中,添加会话管理器配置。关键代码如下:

@Bean  
public SessionManager sessionManager() {
                 DefaultWebSessionManager sManager = new DefaultWebSessionManager();
                 sManager.setGlobalSessionTimeout(60*60*1000);
                 return sManager;

}

第二步:在SpringShiroConfig配置类中,对安全管理器 securityManager 增加 sessionManager值的注入,关键代码如下:

@Bean
public SecurityManager securityManager(
                        Realm realm,CacheManager cacheManager,
						RememberMeManager rememberManager,
						SessionManager sessionManager) {
                 DefaultWebSecurityManager sManager = new DefaultWebSecurityManager();
                 sManager.setRealm(realm);
                 sManager.setCacheManager(cacheManager);
                 sManager.setRememberMeManager(rememberMeManager);
                 sManager.setSessionManager(sessionManager);
                 return sManager;
}

课堂练习:

1.获取用户登陆信息,并将登陆用户名呈现在系统主页(starter.html)上.

第一步:定义一个工具类(ShiroUtils),获取用户登陆信息.

package com.cy.pj.common.util;
import org.apache.shiro.SecurityUtils;
import com.cy.pj.sys.entity.SysUser;
public class ShiroUtils {
          public static String getUsername() {
                  return getUser().getUsername();
          }
          public static SysUser getUser() {
                  return  (SysUser)SecurityUtils.getSubject().getPrincipal();
          }
}

第二步:修改PageController中的doIndexUI方法,代码如下:

@RequestMapping("doIndexUI")
    public String doIndexUI(Model model) {
            SysUser user = ShiroUtils.getUser();
            model.addAttribute("user",user);
            return "starter";
    }

第三步:借助thymeleaf中的表达式直接在页面上(starter.html)获取登陆用户信息

<span class="hidden-xs" id="loginUserId">[[${user.username}]]</span>

2.修改密码页面呈现

  • 服务端关键业务设计及实现

检查PageController中是否有返回UI页面的方法,有则无需添加。例如:

@RequestMapping("{module}/{moduleUI}")
public String doModuleUI(@PathVariable String moduleUI) {
                return "sys/"+moduleUI;
}
  • 客户端关键业务设计及实现

准备密码编辑页面
准备密码编辑页面(/templates/pages/sys/pwd_edit.html)

密码编辑页面呈现
业务描述与设计实现
在系统首页左侧操作菜单中点击修改密码时,呈现密码编辑页面。

关键代码设计与实现
在starter.html页面尾部的页面加载完成的事件处理函数中添加事件处理,关键代码如下:

$(function(){
     …
     doLoadUI("load-pwd-id","pwd/pwd_edit")
})

function doLoadUI(id,url){
         $("#"+id).click(function(){
                    $("#mainContentId").load(url);
    });
}
  • 密码修改页面数据持久化实现

服务端关键业务设计及实现
DAO接口定义
业务描述及设计实现
基于用户id,修改用户密码和盐值。

关键代码设计及实现:
在创建SysUserDao中添加修改用户密码信息的方法。关键代码如下:

int updatePassword(String password,String salt,Integer id);

Mapper映射文件定义
业务描述及设计实现
基于用户SysUserDao中修改密码方法的定义,在映射文件中定义映射元素。

关键代码设计及实现:
在创建SysUserMapper.xml中定义修改密码对应的映射元素。关键代码如下:

<update id="updatePassword">
         update sys_users
         set password=#{password},
             salt=#{salt},
             modifiedTime=now()
         where id=#{id}
</update>

Service接口定义及实现
业务描述及设计实现
基于控制层提交的用户相关信息,实现修改密码业务。

关键代码设计及实现:
第一步:在SysUserService接口中添加,用于实现密码修改业务的方法。关键代码如下:

int updatePassword(String password,String newPassword,String cfgPassword);

第二步:在SysUserService接口的实现类SysUserServiceImpl中添加密码修改业务的具体实现。关键代码如下:

@Override
        public int updatePassword(String password, String newPassword,String cfgPassword) {
                //1.判定新密码与密码确认是否相同
                if(StringUtils.isEmpty(newPassword))
                	throw new IllegalArgumentException("新密码不能为空");
                if(StringUtils.isEmpty(cfgPassword))
                	throw new IllegalArgumentException("确认密码不能为空");
                if(!newPassword.equals(cfgPassword))
                	throw new IllegalArgumentException("两次输入的密码不相等");
                //2.判定原密码是否正确
                if(StringUtils.isEmpty(password))
                	throw new IllegalArgumentException("原密码不能为空");
                //获取登陆用户
                SysUser user=(SysUser)SecurityUtils.getSubject().getPrincipal();
                SimpleHash sh=new SimpleHash("MD5",password, user.getSalt(), 1);
                if(!user.getPassword().equals(sh.toHex()))
                	throw new IllegalArgumentException("原密码不正确");
                //3.对新密码进行加密
                String salt=UUID.randomUUID().toString();
                sh=new SimpleHash("MD5",newPassword,salt, 1);
                //4.将新密码加密以后的结果更新到数据库
                int rows=sysUserDao.updatePassword(sh.toHex(), salt,user.getId());
                if(rows==0)
                	throw new ServiceException("修改失败");
                return rows;
        }

Controller类定义
业务描述及设计实现
基于客户端提交的修改密码请求,定义处理请求的相关方法及映射。

关键代码设计及实现:
在SysUserController类中添加用于实现密码修改的控制层方法。关键代码如下:

@RequestMapping("doUpdatePassword")
public JsonResult doUpdatePassword(String pwd,String newPwd,String cfgPwd) {
                 sysUserService.updatePassword(pwd, newPwd, cfgPwd);
                 return new JsonResult("update ok");
}

客户端关键业务设计及实现
业务描述及设计实现
获取修改页面表单中用户填写的数据,然后向服务端发起异步请求进行数据更新。

关键代码设计及实现:
第一步:对保存和取消按钮进行事件注册,关键代码如下:

$(function(){
           $(".box-footer")
           .on("click",".btn-cancel",doCancel)
           .on("click",".btn-save",doUpdateObject)
   });

第二步:定义保存和取消事件处理函数,关键代码如下:

function doCancel(){
           $("#mainContentId").html("");
   }
function doUpdateObject(){
           //1.获取用户提交数据
           var params={pwd:$("#pwdId").val(),
                      newPwd:$("#newPwdId").val(),
                      cfgPwd:$("#cfgPwdId").val()
           }
           //2.定义请求的url
           var url="user/doUpdatePassword";
           //3.异步提交请求,执行更新操作
           $.post(url,params,function(result){
                        alert(result.message);
           })
   }

6. Shiro总结

6.1 重点和难点分析

shiro 认证过程分析及实现(判定用户身份的合法性)。
Shiro 授权过程分析及实现(对资源访问进行权限检测和授权)。
Shiro 缓存,会话时长,记住我等功能实现。

6.2 常见FAQ

说说shiro的核心组件?
说说shiro的认证流程,你如何知道的,为什么要认证?
说说shiro的授权流程,你如何知道流程是这样的,为什么要进行授权?
Shiro中内置缓存应用实现?为什么使用此缓存?是否可以使用第三方缓存?
Shiro中的记住我功能如何实现?为什么要使用这个功能?
Shiro中会话session的默认时长是多少,你怎么知道的?

6.3Bug分析

SecurityManager包名错误。
MD5加密算法设置错误。
Realm对象没有交给spring管理
用户名和密码接收错误
CacheManager名字与Spring中内置的CacheManager名字冲突。
过滤规则配置错误?

1. 首页菜单动态化实现

1.1 背景

用户希望在登陆以后基于权限的不同,在首页左侧呈现不同的菜单,如何实现呢?

1.2 初步分析

基于登陆用户id,查询用户对应的菜单信息然后进行呈现

1.3 原理应用分析

方案实现上可以基于同步或异步查询然后进行菜单数据呈现。

1.4 最终解决方案

用户登陆以后,基于用户登陆id查询用户对应的一级菜单,二级菜单然后存储到指定作用域,当进入系统首页后基于thymeleaf呈现用户菜单。

1.4.1 Vo类的定义

基于用户需求将查询到的一级菜单以及一级菜单对应的二级菜单查询出来,并进行封装。

package com.cy.pj.sys.vo;
    @Setter
    @Getter
    @ToString
public class SysUserMenuVo implements Serializable{
        private static final long serialVersionUID = -8126757329276920059L;
        private Integer id;
        private String name;
        private String url;
        private List<SysUserMenuVo> childs;
}

Dao接口实现
在SysMenuDao中添加方法,代码如下

/**
     * 基于菜单获取菜单信息
     * @param menuIds
     * @return
     */
    List<SysUserMenuVo> findMenusByIds(List<Integer> menuIds);

1.4.3 Mapper元素定义

在SysMenuMapper.xml文件中添加如下映射元素:

<select id="findMenusByIds" 
            resultMap="sysUserMenuVo">
           select p.id,p.name,p.url,c.id cid,c.name cname,c.url curl
           from sys_menus p join sys_menus c
           on c.parentId=p.id
           <where>
           <foreach collection="menuIds"
                    open="("
                    close=")"
                    separator="or"
                    item="menuId">
               c.id=#{menuId}
           </foreach>
           and p.parentId is null
           </where>
 </select>
<resultMap type="com.cy.pj.common.vo.SysUserMenuVo" id="sysUserMenuVo">
           <!-- 一级菜单映射 -->
           <id  property="id" column="id"/>
           <result property="name" column="name"/>
           <result property="url" column="url"/>
           <!-- 二级菜单映射 -->
           <collection property="childs" ofType="com.cy.pj.common.vo.SysUserMenuVo">
               <id  property="id" column="cid"/>
               <result property="name" column="cname"/>
               <result property="url" column="curl"/>
           </collection>
     </resultMap>

1.4.4 Service接口及实现

在SysUserService接口及实现类中定义如下方法,代码如下

@Override
    public List<SysUserMenuVo> findUserMenusByUserId(Integer id) {
            //1.对用户id进行判断
            //2.基于用户id查找用户对应的角色id
            List<Integer> roleIds = sysUserRoleDao.findRoleIdsByUserId(id);
            //3.基于角色id获取角色对应的菜单信息,并进行封装.
            List<Integer> menuIds = sysRoleMenuDao.findMenuIdsByRoleIds(roleIds.toArray(new Integer[] {}));
            //4.基于菜单id获取用户对应的菜单信息并返回
            return sysMenuDao.findMenusByIds(menuIds);
    }

1.4.5 Controller类实现

修改PageController中的doIndexUI方法

@RequestMapping("doIndexUI")
 public String doIndexUI(Model model) {
        SysUser user = ShiroUtils.getUser();
        model.addAttribute("username", user.getUsername());
        List<SysUserMenuVo> userMenus = sysUserService.findUserMenusByUserId(user.getId());
        model.addAttribute("userMenus",userMenus);
        return "starter";
 }

1.4.6 Starter页面实现

将如下代码替换starter.html中Sidebar Menu对应的位置。

<ul class="sidebar-menu" data-widget="tree">
    <li class="header">HEADER</li>
    <li class="treeview"  th:each="m:${userMenus}">
      <a href="#"><i class="fa fa-link"></i> 
      <span>[[${m.name}]]</span>
        <span class="pull-right-container">
            <i class="fa fa-angle-left pull-right"></i>
          </span>
      </a>
      <ul class="treeview-menu">
        <li th:each="c:${m.childs}">
            <a th:onclick="javascript:doLoadRS([[${c.url}]])">[[${c.name}]]</a>
        </li>
      </ul>
    </li>
  </ul>

当点击对应的菜单元素时,其事件处理函数如下:

<script type="text/javascript">
         function doLoadRS(url){
                 $("#mainContentId").load(url);
    }
   </script>

2. 控制层访问拦截实现

2.1 背景

最近项目业务上有新的需求,要求系统登陆操作要有时间限制。

2.2 初步分析

对于类似需求的实现,可采用过滤器,SpringMVC拦截器,AOP等进行实现。对于过滤器而言一般主要应用在项目中共性的过滤,AOP需要依托于动态代理以及切面对象性能方面相对较差,所以最终选择使用Spring MVC拦截器进行实现。

2.3 原理应用分析

Spring MVC中的拦截器基于回调机制,可以在目标方法执行之前,先进行业务检测,满足条件则放行,不满足条件则进行拦截,拦截器原理分析如下图所示:

shiro session 管理 shiro sessionmanager配置_代码设计

2.4 最终解决方案实现

第一步:拦截器定义,关键代码如下:

package com.cy.pj.common.web;
/**
 * Spring MVC中拦截器
 * @author Administrator
 */
public class TimeAccessInterceptor implements HandlerInterceptor {
        /**
         * preHandle在控制层目标方法执行之前执行
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
                        throws Exception {
                System.out.println("preHandler()");
                //获取java中的日历对象
                Calendar c = Calendar.getInstance();
                c.set(Calendar.HOUR_OF_DAY, 6);
                c.set(Calendar.MINUTE, 0);
                c.set(Calendar.SECOND, 0);
                long start = c.getTimeInMillis();
                c.set(Calendar.HOUR_OF_DAY,24);
                long end = c.getTimeInMillis();
                long cTime = System.currentTimeMillis();
                if(cTime<start||cTime>end)
                	throw new ServiceException("不在访问时间之内");
                return true;
        }
}

第二步:拦截器配置,关键代码如下

package com.cy.pj.common.config;
@Configuration
public class SpringWebConfig implements WebMvcConfigurer{//web.xml
         //配置spring mvc 拦截器
         @Override
         public void addInterceptors(InterceptorRegistry registry) {
                 registry.addInterceptor(new TimeAccessInterceptor()).addPathPatterns("/user/doLogin");
         }
}