Shiro框架

一.Shiro框架认证拦截实现(filter)

添加依赖


    org.apache.shiro
    shiro-spring
    1.5.3

//第一步:创建SpringShiroConfig类。关键代码如下:

package com.cy.pj.common.config;
/*@Configuration 注解描述的类为一个配置对象,
*此对象也会交给spring管理
*/
@Configuration
public class SpringShiroConfig {
    //第二步:在Shiro配置类中添加SecurityManager配置(这里一定要使用org.apache.shiro.mgt.SecurityManager这个接口对象),关键代码如下:
    /*
     *SecurityManager是Shrio框架的核心,用于实现认证授权等功能
     */
     /*@Bean 用来描述方法,功能类似于@component/@service(用来描述类)。
      Spring容器中整合三方对象时,可以将其bean对象的创建放在一个方法中,然后使用@Bean注解进行描述,
      Spring容器管理bean对象时就会将方法名作为key对Bean对象进行储存*/
    @Bean
    public SecurityManager securityManager() {                 
        DefaultWebSecurityManager sManager=new DefaultWebSecurityManager();
        return sManager;
    }
    
   //第三步: 在Shiro配置类中添加ShiroFilterFactoryBean对象的配置。通过此对象设置资源匿名访问、认证访问。关键代码如下:
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactory (SecurityManager securityManager) {
         //构建ShiroFilterFactoryBean对象,此对象负责创建过滤器工厂
         ShiroFilterFactoryBean sfBean=new ShiroFilterFactoryBean();
        //关联setSecurityManager(要借助此对象对URL进行认证)
        sfBean.setSecurityManager(securityManager);
        //设置默认静态页面             
        sfBean.setLoginUrl("/doLoginUI");
             //定义map指定请求过滤规则(哪些资源允许匿名访问,哪些必须认证访问)
        LinkedHashMap map= new LinkedHashMap<>();
             //以下资源允许匿名问:"anon"关键字“允许”
             map.put("/bower_components/**","anon");
             map.put("/build/**","anon");
             map.put("/dist/**","anon");
             map.put("/plugins/**","anon");
             //除了上述匿名访问的资源,其它都要认证("authc")后访问
             map.put("/**","authc");
        sfBean.setFilterChainDefinitionMap(map);
        return sfBean;
     }
}

二.认证服务端实现

核心业务分析:
Shiro框架_第1张图片
客户端提交用户账号和密码,在Controller中拿到账号和密码封装到token对象,然后借助subject的login方法,把数据提交给DefaultWebSecurityManager,通过Realm获取持久层数据,交给authenticate进行比对

2.1 DAO接口定义

SysUser findUserByUserName(String username)。

2.2 Mapper元素定义

  • 业务描述及设计实现。

根据SysUserDao中定义的方法,在SysUserMapper文件中添加元素定义。

  • 关键代码分析及实现。

基于用户名获取用户对象的方法,关键代码如下:

2.3 Service接口及实现

package com.cy.pj.sys.service.realm;
@Service
public class ShiroUserRealm extends AuthorizingRealm {

    @Autowired
    private SysUserDao sysUserDao;
    /**
     * 设置凭证匹配器(与用户添加操作使用相同的加密算法)
     */
    @Override
    public void setCredentialsMatcher(
          CredentialsMatcher credentialsMatcher) {
            //构建凭证匹配对象
            HashedCredentialsMatcher cMatcher=new HashedCredentialsMatcher();
            //设置加密算法
            cMatcher.setHashAlgorithmName("MD5");
            //设置加密次数
            cMatcher.setHashIterations(1);
            super.setCredentialsMatcher(cMatcher);
    }
  /*  @Override
    public CredentialsMatcher getCredentialsMatcher() {
        HashedCredentialsMatcher cMatcher=new HashedCredentialsMatcher();
        cMatcher.setHashAlgorithmName("MD5");
        cMatcher.setHashIterations(1);
        return cMatcher;
}*/

    /**负责认证信息的获取和封装
     * 通过此方法完成认证数据的获取及封装,系统
     * 底层会将认证数据传递认证管理器,由认证
     * 管理器完成认证操作。
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
                    AuthenticationToken token)
                    throws AuthenticationException {
            //1.获取用户名(用户页面输入)
            UsernamePasswordToken upToken=(UsernamePasswordToken)token;
            String username=upToken.getUsername();
            //2.基于用户名查询用户信息
            SysUser user=  sysUserDao.findUserByUserName(username);
            //3.判定用户是否存在
            if(user==null)
            throw new UnknownAccountException();
            //4.判定用户是否已被禁用。
            if(user.getValid()==0)
            throw new LockedAccountException();
            //5.封装用户信息
            ByteSource credentialsSalt=ByteSource.Util.bytes(user.getSalt());
            //记住:构建什么对象要看方法的返回值
            SimpleAuthenticationInfo info=new SimpleAuthenticationInfo(
                            user,//principal (身份)
                            user.getPassword(),//hashedCredentials 已加密的密码
                            credentialsSalt, //credentialsSalt
                            getName());//realName
            //6.返回封装结果
            return info;//返回值会传递给认证管理器(后续认证管理器会通过此信息完成认证操作)
        }
    }

//第二步:对此realm,需要在SpringShiroConfig配置类中,注入给SecurityManager对象,修改securityManager方法,见黄色背景部分,例如:

    @Bean
    public SecurityManager securityManager(Realm realm) {
             DefaultWebSecurityManager sManager=new DefaultWebSecurityManager();
             sManager.setRealm(realm);

             return sManager;
    }

2.4Controller 类实现

  • 业务描述及设计实现。

在此对象中定义相关方法,处理客户端的登陆请求,例如获取用户名,密码等然后提交该shiro框架进行认证。

  • 关键代码分析及实现。

第一步:在SysUserController中添加处理登陆的方法。关键代码如下:

        @RequestMapping("doLogin")
        public JsonResult doLogin(String username,String password){
               //1.获取Subject对象
               Subject subject=SecurityUtils.getSubject();
               //2.通过Subject提交用户信息,交给shiro框架进行认证操作
               //2.1对用户进行封装
               UsernamePasswordToken token=new UsernamePasswordToken(
                                                   username,//身份信息
                                                   password//凭证信息
                                                                    );
               /*或者调用set方法,底层提供了get/set方法赋值,也提供了构造方法用来赋值
               UsernamePasswordToken token=new UsernamePasswordToken();
                        token.setUsername(username);
                        token.setPassword(password.toCharArray());
               */
               //2.2对用户信息进行身份认证
               subject.login(token);
               //分析:
               //1)token会传给shiro的SecurityManager
               //2)SecurityManager将token传递给认证管理器
               //3)认证管理器会将token传递给realm
               return new JsonResult("login ok");
       }
       

第二步:修改shiroFilterFactory的配置,对/user/doLogin这个路径进行匿名访问的配置,查看如下黄色标记部分的代码:

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactory (SecurityManager securityManager) {

             ShiroFilterFactoryBean sfBean = new ShiroFilterFactoryBean();
             sfBean.setSecurityManager(securityManager);
             sfBean.setLoginUrl("/doLoginUI");
             LinkedHashMap map= new LinkedHashMap<>();
             map.put("/bower_components/**","anon");
             map.put("/build/**","anon");
             map.put("/dist/**","anon");
             map.put("/plugins/**","anon");
             
             map.put("/user/doLogin","anon");//放行登录页面
             
             map.put("/**","authc");
             sfBean.setFilterChainDefinitionMap(map);
             return sfBean;
     }

第三步:当我们在执行登录操作时,为了提高用户体验,可对系统中的异常信息进行处理,例如,在统一异常处理类中添加如下方法:

    @ExceptionHandler(ShiroException.class)
    @ResponseBody
    public JsonResult doHandleShiroException(
                    ShiroException e) {
            JsonResult r=new JsonResult();
            r.setState(0);
            if(e instanceof UnknownAccountException) {
                    r.setMessage("账户不存在");
            }else if(e instanceof LockedAccountException) {
                    r.setMessage("账户已被禁用");
            }else if(e instanceof IncorrectCredentialsException) {
                    r.setMessage("密码不正确");
            }else if(e instanceof AuthorizationException) {
                    r.setMessage("没有此操作权限");
            }else {
                    r.setMessage("系统维护中");
            }
            e.printStackTrace();
            return r;
    }
    

三.退出操作配置实现

在SpringShiroConfig配置类中,修改过滤规则,添加黄色标记部分代码的配置,请看如下代码:

@Bean
public ShiroFilterFactoryBean shiroFilterFactory(
    SecurityManager securityManager) {
    ShiroFilterFactoryBean sfBean=
    new ShiroFilterFactoryBean();
    sfBean.setSecurityManager(securityManager);
    sfBean.setLoginUrl("/doLoginUI");
    LinkedHashMap map=new LinkedHashMap<>();
    map.put("/bower_components/**","anon");
    map.put("/build/**","anon");
    map.put("/dist/**","anon");
    map.put("/plugins/**","anon");
    map.put("/user/doLogin","anon");

    map.put("/doLogout","logout"); //logout关键词对应着shiro中的一个默认filter,将登陆用户信息从session中清除

    map.put("/**","authc");
    sfBean.setFilterChainDefinitionMap(map);
    return sfBean;
    }
    

四.Shiro框架授权过程实现

1.授权流程分析如下:

  1. 系统调用subject相关方法将用户信息(例如isPermitted)递交给SecurityManager。
  2. SecurityManager将权限检测操作委托给Authorizer对象。
  3. Authorizer将用户信息委托给realm。
  4. Realm访问数据库获取用户权限信息并封装。
  5. Authorizer对用户授权信息进行判定。

2.添加授权配置
在SpringShiroConfig配置类中,添加授权时的相关配置:
第一步:配置bean对象的生命周期管理(SpringBoot可以不配置)。

@Bean
public LifecycleBeanPostProcessor   lifecycleBeanPostProcessor() {
    return new LifecycleBeanPostProcessor();
}

第二步: 通过如下配置要为目标业务对象创建代理对象(SpringBoot中可省略)。

@DependsOn("lifecycleBeanPostProcessor")
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    return new DefaultAdvisorAutoProxyCreator();
}

第三步:配置advisor对象,shiro框架底层会通过此对象的matchs方法返回值(类似切入点)决定是否创建代理对象,进行权限控制。

@Bean

public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor (
                                                                SecurityManager securityManager) {

    AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
    return advisor;
}

说明:使用框架最重要的尊重规则,框架规则指定了什么方式就使用什么方式。

3. 授权服务端实现

  1. 核心业务分析

image.png

2.Dao实现

  • 业务描述及设计实现。

基于登陆用户ID,认证信息获取登陆用户的权限信息,并进行封装。

  • 关键代码分析及实现。

第一步:在SysUserRoleDao中定义基于用户id查找角色id的方法(假如方法已经存在则无需再写),关键代码如下:

List findRoleIdsByUserId(Integer id);

第二步:在SysRoleMenuDao中定义基于角色id查找菜单id的方法,关键代码如下:

List findMenuIdsByRoleIds(@Param("roleIds")List roleIds);

第三步:在SysMenuDao中基于菜单id查找权限标识的方法,关键代码如下:

List findPermissions(@Param("menuIds")
                                List menuIds);

3.Mapper实现

  • 业务描述及设计实现。

基于Dao中方法,定义映射元素。

  • 关键代码分析及实现。

第一步:在SysUserRoleMapper中定义findRoleIdsByUserId元素。关键代码如下:


第二步:在SysRoleMenuMapper中定义findMenuIdsByRoleIds元素。关键代码如下:


第三步:在SysMenuMapper中定义findPermissions元素,关键代码如下:


4.Service实现

  • 业务描述及设计实现。

在ShiroUserReam类中,重写对象realm的doGetAuthorizationInfo方法,并完成用户权限信息的获取以及封装,最后将信息传递给授权管理器完成授权操作。

  • 关键代码分析及实现。

修改ShiroUserRealm类中的doGetAuthorizationInfo方法,关键代码如下:

@Service
public class ShiroUserRealm extends AuthorizingRealm {
    @Autowired
    private SysUserDao sysUserDao;
    @Autowired
    private SysUserRoleDao sysUserRoleDao;
    @Autowired
    private SysRoleMenuDao sysRoleMenuDao;
    @Autowired
    private SysMenuDao sysMenuDao;

    /**通过此方法完成授权信息的获取及封装*/
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //1.获取登录用户信息,例如用户id
        SysUser user=(SysUser)principals.getPrimaryPrincipal();
        Integer userId=user.getId();
        //2.基于用户id获取用户拥有的角色(sys_user_roles)
        List roleIds=sysUserRoleDao.findRoleIdsByUserId(userId);
        if(roleIds==null||roleIds.size()==0)
            throw new AuthorizationException();
        //3.基于角色id获取菜单id(sys_role_menus)
        List menuIds=sysRoleMenuDao.findMenuIdsByRoleIds(roleIds);
        if(menuIds==null||menuIds.size()==0)
            throw new AuthorizationException();
        //4.基于菜单id获取权限标识(sys_menus)
        List permissions=sysMenuDao.findPermissions(menuIds);
        //5.对权限标识信息进行封装并返回
        Set set=new HashSet<>();
        for(String per:permissions){
            if(!StringUtils.isEmpty(per)){
                set.add(per);
            }
        }
        SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
        info.setStringPermissions(set);
        return info;//返回给授权管理器
    }
。。。。
}

4.授权访问实描述现

在需要进行授权访问的业务层(Service)方法上,添加执行此方法需要的权限标识,参考代码 @RequiresPermissions(“sys:user:update”)
说明:此要注解一定要添加到业务层方法上。

**5.Shiro缓存配置
当我们进行授权操作时,每次都会从数据库查询用户权限信息,为了提高授权性能,可以将用户权限信息查询出来以后进行缓存,下次授权时从缓存取数据即可。
Shiro中内置缓存应用实现,其步骤如下:
第一步:在SpringShiroConfig中配置缓存Bean对象(Shiro框架提供)。

@Bean
public CacheManager shiroCacheManager(){
    return new MemoryConstrainedCacheManager();
}
说明:这个CacheManager对象的名字不能写cacheManager,因为spring容器中已经存在一个名字为cacheManager的对象了.

第二步:修改securityManager的配置,将缓存对象注入给SecurityManager对象。

@Bean
public SecurityManager securityManager(Realm realm,
                                       CacheManager cacheManager) {
    DefaultWebSecurityManager sManager=new DefaultWebSecurityManager();
    sManager.setRealm(realm);
    sManager.setCacheManager(cacheManager);
    return sManager;
}
说明:对于shiro框架而言,还可以借助第三方的缓存产品(例如redis)对用户的权限信息进行cache操作.

五.Shiro记住我

记住我功能是要在用户登录成功以后,假如关闭浏览器,下次再访问系统资源(例如首页doIndexUI)时,无需再执行登录操作。
1.客户端业务实现
在页面上选中记住我,然后执行提交操作,将用户名,密码,记住我对应的值提交到控制层,如图-9所示:

image.png

其客户端login.html中关键JS实现:

function doLogin(){
    var params={
        username:$("#usernameId").val(),
        password:$("#passwordId").val(),
        isRememberMe:$("#rememberId").prop("checked"),
    }
    var url="user/doLogin";
    console.log("params",params);
    $.post(url,params,function(result){
    if(result.state==1){
        //跳转到indexUI对应的页面
        location.href="doIndexUI?t="+Math.random();
        }else{
        $(".login-box-msg").html(result.message);
        }
    return false;//防止刷新时重复提交
    });
}

2.服务端业务实现
服务端业务实现的具体步骤如下:
第一步:在SysUserController中的doLogin方法中基于是否选中记住我,设置token的setRememberMe方法。

@RequestMapping("doLogin")
@ResponseBody
public JsonResult doLogin(
    boolean isRememberMe,
    String username,
    String password) {
    //1.封装用户信息
    UsernamePasswordToken token=new UsernamePasswordToken(username, password);
    if(isRememberMe) {
        token.setRememberMe(true);
    }
    //2.提交用户信息
    Subject subject=SecurityUtils.getSubject();
    subject.login(token);//token会提交给securityManager
    return new JsonResult("login ok");
}

第二步:在SpringShiroConfig配置类中添加记住我配置,关键代码如下:

@Bean
public RememberMeManager rememberMeManager() {
    CookieRememberMeManager cManager=new CookieRememberMeManager();
    SimpleCookie cookie=new SimpleCookie("rememberMe");
    cookie.setMaxAge(7*24*60*60);
    cManager.setCookie(cookie);
    return cManager;
}

第三步:在SpringShiroConfig中修改securityManager的配置,为securityManager注入rememberManager对象。参考黄色部分代码。

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

第四步:修改shiro的过滤认证级别,将/=author修改为/=user,查看黄色背景部分。

@Bean
public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager) {
    ShiroFilterFactoryBean sfBean=new ShiroFilterFactoryBean();
    sfBean.setSecurityManager(securityManager);
    sfBean.setLoginUrl("/doLoginUI");
    LinkedHashMap map=new LinkedHashMap<>();
    map.put("/bower_components/**","anon");
    map.put("/build/**","anon");
    map.put("/dist/**","anon");
    map.put("/plugins/**","anon");
    map.put("/user/doLogin","anon");
    map.put("/doLogout", "logout");//自动查LoginUrl
    
    //除了匿名访问的资源,其它都要认证("authc")后访问
    map.put("/**","user");//user表示认证可以从cookie取信息来实现
    
    sfBean.setFilterChainDefinitionMap(map);
    return sfBean;
}

说明:查看浏览器cookie设置,可在浏览器中输入如下语句。chrome://settings/content/cookies

六.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)获取登陆用户信息


2.修改登陆用户的密码?(参考用户模块文档)
分析:
1)确定都要修改谁?(密码,盐值,修改时间)
2)服务端的设计实现?(dao,service,controller)
3)客户端的设计实现?(异步提交用户密码信息)

七.Shiro总结

=======

1. 重点和难点分析

  1. shiro 认证过程分析及实现(判定用户身份的合法性)。
  2. Shiro 授权过程分析及实现(对资源访问进行权限检测和授权)。
  3. Shiro 缓存,会话时长,记住我等功能实现。
  4. 常见FAQ
  5. 说说shiro的核心组件?
  6. 说说shiro的认证流程,你如何知道的,为什么要认证?
  7. 说说shiro的授权流程,你如何知道流程是这样的,为什么要进行授权?
  8. Shiro中内置缓存应用实现?为什么使用此缓存?是否可以使用第三方缓存?
  9. Shiro中的记住我功能如何实现?为什么要使用这个功能?
  10. Shiro中会话session的默认时长是多少,你怎么知道的?

2. Bug分析

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

你可能感兴趣的:(java)