欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Springboot整合(9)——Shiro

程序员文章站 2022-03-01 20:34:45
...

Springboot整合(9)——Shiro

Shiro基本配置

1. pom增加依赖

        <!-- shiro spring. -->

        <dependency>

            <groupId>org.apache.shiro</groupId>

            <artifactId>shiro-spring</artifactId>

            <version>1.2.2</version>

        </dependency>

2. 编写自己的shiro

/**

 * 身份校验核心类;

 *

 * @version v.0.1

 */

publicclass MyShiroRealm extends AuthorizingRealm {

 

    privatestaticfinal Log LOG = LogFactory.getLog(MyShiroRealm.class);

 

    @Resource

    UserService userService;

 

    /**

     * 认证信息.(身份验证) : Authentication 是用来验证用户身份

     *

     * @param token

     * @return

     * @throws AuthenticationException

     */

    @Override

    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        System.out.println("MyShiroRealm.doGetAuthenticationInfo()");

 

        // 获取用户的输入的账号.

        String username = (String) token.getPrincipal();

        System.out.println(token.getCredentials());

 

        // 通过username从数据库中查找 User对象,如果找到,没找到.

        // 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法

        SysUser userInfo = userService.getByLoginName(username);

        System.out.println("----->>userInfo=" + userInfo);

        if (userInfo == null) {

            returnnull;

        }

 

        // 加密方式;

        // 交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以自定义实现

        /*

         * 这里调的是SimpleAuthenticationInfo(principal,

         * hashedCredentials,credentialsSalt,realmName)

         * 第三个参数是盐值,本处示例用的是用户id,实际可根据需要使用任意值或者干脆不用,盐值的具体用处请自行百度

         */

        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userInfo, // 用户名

                userInfo.getLoginPassword(), // 密码

                ByteSource.Util.bytes(userInfo.getId()), // salt=username+salt,这里直接使用的userId

                getName() // realm name

        );

 

        // init session

        Subject currentUser = SecurityUtils.getSubject();

        Session session = currentUser.getSession();

        session.setAttribute("user", userInfo);

 

        returnauthenticationInfo;

    }

 

    /**

     * 此方法调用 hasRole,hasPermission的时候才会进行回调.

     *

     * 权限信息.(授权): 1、如果用户正常退出,缓存自动清空; 2、如果用户非正常退出,缓存自动清空;

     * 3、如果我们修改了用户的权限,而用户不退出系统,修改的权限无法立即生效。(需要手动编程进行实现;放在service进行调用)

     * 在权限修改后调用realm中的方法,realm已经由spring管理,所以从spring中获取realm实例,调用clearCached方法;

     * :Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。

     *

     * @param principals

     * @return

     */

    @Override

    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        /*

         * 当没有使用缓存的时候,不断刷新页面的话,这个代码会不断执行,当其实没有必要每次都重新设置权限信息,所以我们需要放到缓存中进行管理;

         * 当放到缓存中时,这样的话,doGetAuthorizationInfo就只会执行一次了,缓存过期之后会再次执行。

         */

        System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");

 

        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

        authorizationInfo.addRole("testAdmin");

        authorizationInfo.addStringPermission("test:Permission");

 

        returnauthorizationInfo;

    }

}

注:本文演示shiro使用不给出userrolepermission的具体实现,只演示基本流程,rolepermission的设置都直接使用硬编码的方式写在代码里,实际项目使用时将硬编码改成相应的实现逻辑即可。这里将上述realm的实现逻辑流程说明一下

doGetAuthenticationInfo,这个方法在用户登陆时调用,入参AuthenticationToken包含了用户名密码→根据用户名调service获取是否有这个用户并拿到这个用户的信息→验证用户名密码是否正确→做session初始化

doGetAuthorizationInfo,这个方法在hasRole,hasPermission的时候才会进行回调,该方法的具体实现就是为当前用户设置rolepermission信息。
3. shiro
的配置类ShiroConfiguration

@Configuration

publicclass ShiroConfiguration {

 

    /**

     * ShiroFilterFactoryBean 处理拦截资源文件问题。

     * 注意:单独一个ShiroFilterFactoryBean配置是或报错的,以为在

     * 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager

     *

     * Filter Chain定义说明 1、一个URL可以配置多个Filter,使用逗号分隔 2、当设置多个过滤器时,全部验证通过,才视为通过

     * 3、部分过滤器可指定参数,如permsroles

     *

     */

    @Bean

    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {

        System.out.println("ShiroConfiguration.shiroFilter()");

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

 

        // 必须设置 SecurityManager

        shiroFilterFactoryBean.setSecurityManager(securityManager);

 

        // 拦截器.

        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();

 

        // 配置静态资源匿名访问

        filterChainDefinitionMap.put("/vendors/**", "anon");

        filterChainDefinitionMap.put("/resources/**", "anon");

        // 配置druid连接池后台可以匿名访问

        filterChainDefinitionMap.put("/druid/**", "anon");

        // 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了

        filterChainDefinitionMap.put("/logout", "logout");

 

        // <!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;

        // <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->

         filterChainDefinitionMap.put("/**", "authc");

 

        // 配置登陆链接

        shiroFilterFactoryBean.setLoginUrl("/user/login");

        // 登录成功后要跳转的链接

        shiroFilterFactoryBean.setSuccessUrl("/user/list");

        // 未授权界面;

        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

 

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        returnshiroFilterFactoryBean;

    }

 

    @Bean

    public SecurityManager securityManager() {

        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        securityManager.setRealm(myShiroRealm());

        returnsecurityManager;

    }

 

    @Bean

    public MyShiroRealm myShiroRealm() {

        MyShiroRealm myShiroRealm = new MyShiroRealm();

        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());

        returnmyShiroRealm;

    }

 

    @Bean

    public HashedCredentialsMatcher hashedCredentialsMatcher() {

        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();

 

        hashedCredentialsMatcher.setHashAlgorithmName("md5");// 散列算法:这里使用MD5算法;

        // hashedCredentialsMatcher.setHashIterations(2);// 散列的次数,比如散列两次,相当于

        // md5(md5(""));

 

        returnhashedCredentialsMatcher;

    }

}

4. controller配置

@RequestMapping(value = "user/login", method = RequestMethod.GET)

    public ModelAndView login(HttpServletRequest request) {

        returnnew ModelAndView("user/login");

    }

 

    @RequestMapping(value = "user/login", method = RequestMethod.POST)

    public ModelAndView login(HttpServletRequest request, String username, String password) {

        Exception exception = null;

        try {

            SecurityUtils.getSubject().login(new UsernamePasswordToken(username, password)); // 完成登录

        } catch (Exception e) {

            exception = e;

        }

 

        String msg = "";

        if (exception != null) {

            if (exceptioninstanceof UnknownAccountException) {

                System.out.println("UnknownAccountException -- > 账号不存在");

                msg = "账号不存在";

            } elseif (exceptioninstanceof IncorrectCredentialsException) {

                System.out.println("IncorrectCredentialsException -- > 密码不正确");

                msg = "密码不正确";

            } else {

                msg = "else >> " + exception;

                System.out.println("else -- >" + exception.getMessage());

            }

        }

 

        Map<String, Object> model = new HashMap<String, Object>();

        model.put("error", msg);

        returnnew ModelAndView("user/login", model);

    }

5. 将数据库中的用户密码加密

写一个密码加密的工具类:

publicclass PasswordEncoder {

 

    publicstatic String MD5Encoding(String password, String userId) {

        returnnew SimpleHash("md5", password, userId, 1).toString();

    }

}

产生加密密码

    @Test

    publicvoid test() {

        System.out.println(PasswordEncoder.MD5Encoding("123456", "1"));

    }

将产生的密码eeafb716f93fa090d7716749a6eefa72写入数据库

 

5. 编写login.jsp

<head>

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<title>login</title>

<%

    String path = request.getContextPath();

    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()

            + path + "/";

%>

</head>

<body>

    <form id="loginForm" action="<%=basePath%>user/login" method="post">

        loginName : <input type="text" id="username" name="username"><br>

        password : <input type="password" id="password" name="password"><br>

        <input type="submit" value="submit"><br>

    </form>

    <p id="message">${error}</p>

</body>

 

6. 测试

访问任意非login页面均会跳转至login页面,登陆后跳转至最后访问的页面url,如果没有则跳转至设置的sucessful页面,即:访问user/add→未登录,shiro将页面跳转至login→登陆通过直接跳转至user/add; 访问user/login→未登录,shiro将页面跳转至login→登陆通过直接跳转至user/list(配置的sucessful页面)

Shiro注解的使用

1. ShiroConfiguration中开启注解

    /**

     * 开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持;

     *

     * @param securityManager

     * @return

     */

    @Bean

    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {

        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();

        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);

        returnauthorizationAttributeSourceAdvisor;

    }

后面你会发现只是加了上面的代码shiro的注解@RequireRoles@RequiresPermissions还是无法生效,还需要加入下面的代码,开启spring的自动代理

    @Bean

    @ConditionalOnMissingBean

    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {

        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();

        defaultAAP.setProxyTargetClass(true);

        returndefaultAAP;

    }

 

2. 编写controller层测试代码

    @RequestMapping(value = "user/testShiro", method = { RequestMethod.GET, RequestMethod.POST })

    public String testShiro() {

        return"user/testShiro";

    }

 

    @RequiresRoles("noExistRole")

    @ResponseBody

    @RequestMapping(value = "user/testRequiresRolesNotExist", method = { RequestMethod.GET, RequestMethod.POST })

    public ReturnResult testRequiresRolesNotExist() {

        ReturnResult rs = new ReturnResult();

        rs.setMessage("noExistRole");

        returnrs;

    }

 

    @RequiresRoles("testAdmin")

    @ResponseBody

    @RequestMapping(value = "user/testRequiresRolesExist", method = { RequestMethod.GET, RequestMethod.POST })

    public ReturnResult testRequiresRolesExist() {

        ReturnResult rs = new ReturnResult();

        rs.setMessage("ExistRole");

        returnrs;

    }

 

    @RequiresPermissions("notExistPermission")

    @ResponseBody

    @RequestMapping(value = "user/testRequiresPermissionsNotExist", method = { RequestMethod.GET, RequestMethod.POST })

    public ReturnResult testRequiresPermissionsNotExist() {

        ReturnResult rs = new ReturnResult();

        rs.setMessage("notExistPermission");

        returnrs;

    }

 

    @RequiresPermissions("test:Permission")

    @ResponseBody

    @RequestMapping(value = "user/testRequiresPermissionsExist", method = { RequestMethod.GET, RequestMethod.POST })

    public ReturnResult testRequiresPermissionsExist() {

        ReturnResult rs = new ReturnResult();

        rs.setMessage("ExistPermission");

        returnrs;

    }

 

3. 编写jsp测试代码

<%@ page language="java" contentType="text/html; charset=UTF-8"

    pageEncoding="UTF-8"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>

<%

    String path = request.getContextPath();

    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()

            + path + "/";

%>

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

<script src="<%=basePath%>vendors/jquery/jquery.min.js"></script>

<title>Test Shiro </title>

<script type="text/javascript">

    function add(btn) {

       

        var url = "<%=basePath%>user/"+$(btn).val();

 

        $.ajax({

            type : 'POST',

            cache : false,

            url : url,

            async : false,

            success : function(result) {

                $("#message").html(result.message);

            },

            error : function(result) {

                alert(result);

            }

        });

    }

</script>

</head>

<body>

<input type="button" value="testRequiresRolesNotExist" onclick="add(this);">

<input type="button" value="testRequiresRolesExist" onclick="add(this);">

<input type="button" value="testRequiresPermissionsNotExist" onclick="add(this);">

<input type="button" value="testRequiresPermissionsExist" onclick="add(this);">

<p id="message"></p>

</body>

</html>

 

4. 测试,访问http://localhost:8088/KnowledgeIsland/user/testShiro,分别点击4button,有权限的会返回结果,没有权限的会显示服务器内部错误(其实是被全局异常处理器RuntimeException处理了),说明注解已经生效


Springboot整合(9)——Shiro
            
    
    博客分类: 框架整合
 

 

Shiro全局异常处理

上一节最后测试的时候异常被RuntimeException处理了,实际这里报的异常是org.apache.shiro.authz.UnauthorizedException,我们显然希望全局异常中能对这种异常单独做处理,所以在BaseController里加入这个异常的处理逻辑即可(顺便把登陆异常也加进去了)

 

   

    /**

     * 授权异常

     */

    @ExceptionHandler({ UnauthorizedException.class })

    @ResponseBody

    public ReturnResult unauthorizedException() {

        returnnew ReturnResult(0, "权限不足!");

    }

 

    /**

     * 登录异常

     */

    @ExceptionHandler({ AuthenticationException.class })

    @ResponseBody

    public ReturnResult authenticationException() {

        returnnew ReturnResult(0, "未登陆!");

    }

这样再运行前一节的测试代码就会提示权限不足了
Springboot整合(9)——Shiro
            
    
    博客分类: 框架整合

 

Shiro配置Cache

我们在前文中测试shiro的权限控制时,每点击一次testRequiresRolesExist或者testRequiresPermissionsExist,观察后台都会发现每次都会去调用一次MyShiroRealm中的doGetAuthorizationInfo方法,即读取一次用户权限,而实际开发中用户的权限信息是不会频繁发生变化的,不需要每次访问的时候都去读取,所以我们可以为Shiro配置缓存,将用户权限信息放在缓存里,避免重复读取,具体配置如下(本文使用ehCache做缓存)

 

1. pom中添加依赖

        <!-- shiro ehcache -->

        <dependency>

            <groupId>org.apache.shiro</groupId>

            <artifactId>shiro-ehcache</artifactId>

            <version>1.2.2</version>

        </dependency>

2.  配置文件ehcache-shiro.xml
Springboot整合(9)——Shiro
            
    
    博客分类: 框架整合

 

<?xml version="1.0" encoding="UTF-8"?>

<ehcache name="es">

 

    <diskStore path="java.io.tmpdir" />

 

    <!--

       name:缓存名称。

       maxElementsInMemory:缓存最大数目

       maxElementsOnDisk:硬盘最大缓存个数。

       eternal:对象是否永久有效,一但设置了,timeout将不起作用。

       overflowToDisk:是否保存到磁盘,当系统当机时

       timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。

       timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。

       diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.

       diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。

       diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。

       memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。

        clearOnFlush:内存数量最大时是否清除。

         memoryStoreEvictionPolicy:

            Ehcache的三种清空策略;

            FIFOfirst in first out,这个是大家最熟的,先进先出。

            LFU Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。

            LRULeast Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。

    -->

    <defaultCache maxElementsInMemory="10000" eternal="false"

        timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="false"

        diskPersistent="false" diskExpiryThreadIntervalSeconds="120" />

 

 

    <!-- 登录记录缓存锁定10分钟 -->

    <cache name="passwordRetryCache" maxEntriesLocalHeap="2000"

        eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0"

        overflowToDisk="false" statistics="true">

    </cache>

 

</ehcache>

 

3. ShiroConfiguration里进行配置

定义缓存管理器bean

    /**

     * shiro缓存管理器; 需要注入对应的其它的实体类中: 1、安全管理器:securityManager

     * 可见securityManager是整个shiro的核心;

     *

     * @return

     */

    @Bean

    public EhCacheManager ehCacheManager() {

        System.out.println("ShiroConfiguration.getEhCacheManager()");

        EhCacheManager cacheManager = new EhCacheManager();

        cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");

        returncacheManager;

    }

SecurityManager里注入ehCacheManager

    @Bean

    public SecurityManager securityManager() {

        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        securityManager.setRealm(myShiroRealm());

        // 注入缓存管理器;

        securityManager.setCacheManager(ehCacheManager());

        returnsecurityManager;

    }

 

4. 再次测试,点击按钮时,仅在登陆后第一次调用一次MyShiroRealm中的doGetAuthorizationInfo方法,之后就不再调用

使用ShiroRemember Me

Shiro中使用RememberMe功能只需要在ShiroConfiguration里做一些配置即可

1. 定义Cookie Bean

    /**

     * cookie对象;

     *

     * @return

     */

    @Bean

    public SimpleCookie rememberMeCookie() {

        // 这个参数是cookie的名称,对应前端的checkboxname = rememberMe

        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");

        // <!-- 记住我cookie生效时间30 ,单位秒;-->

        simpleCookie.setMaxAge(259200);

        returnsimpleCookie;

    }

 

2. 定义Cookie管理bean

    /**

     * cookie管理对象;

     *

     * @return

     */

    @Bean

    public CookieRememberMeManager rememberMeManager() {

        System.out.println("ShiroConfiguration.rememberMeManager()");

        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();

        cookieRememberMeManager.setCookie(rememberMeCookie());

        returncookieRememberMeManager;

    }

 

3. cookieManager注入SecurityManager

    @Bean

    public SecurityManager securityManager() {

        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        securityManager.setRealm(myShiroRealm());

        // 注入缓存管理器;

        securityManager.setCacheManager(ehCacheManager());

        // 注入记住我管理器;

        securityManager.setRememberMeManager(rememberMeManager());

        returnsecurityManager;

    }

 

4. shiro过滤器工厂ShiroFilterFactoryBean中的需要认证的过滤器authc改为通过rememberMe即可访问

 

Springboot整合(9)——Shiro
            
    
    博客分类: 框架整合

注:这2个过滤器是可以共存的,非常敏感的url可以设置为必须认证,不算特别敏感的就可以设置为通过rememberMe即可访问,如可以配置如下:

filterChainDefinitionMap.put("security/**", "authc");

filterChainDefinitionMap.put("normal/**", "user");

5. login.jsp里增加rememberMe参数

Springboot整合(9)——Shiro
            
    
    博客分类: 框架整合

 

6. controller中对login方法做相应修改

Springboot整合(9)——Shiro
            
    
    博客分类: 框架整合

 

7. 测试

Springboot整合(9)——Shiro
            
    
    博客分类: 框架整合

 

submit登陆,页面不跳转,等下再说怎么解决这个问题。手动将url改为user/list,发现已经可以访问,说明登陆成功。再试试rememberMe是否已经工作,关闭浏览器,直接输入urluser/list,也可正常访问。说明rememberMe已经正常工作。

 

最后来说这个页面不跳转的问题,解决方法:在filterChain中增加如下配置即可

filterChainDefinitionMap.put("/user/login", "authc");

Springboot整合(9)——Shiro
            
    
    博客分类: 框架整合
 

 

 

再次测试,一切正常

  • Springboot整合(9)——Shiro
            
    
    博客分类: 框架整合
  • 大小: 15.1 KB
  • Springboot整合(9)——Shiro
            
    
    博客分类: 框架整合
  • 大小: 25.6 KB
  • Springboot整合(9)——Shiro
            
    
    博客分类: 框架整合
  • 大小: 6.5 KB
  • Springboot整合(9)——Shiro
            
    
    博客分类: 框架整合
  • 大小: 4 KB
  • Springboot整合(9)——Shiro
            
    
    博客分类: 框架整合
  • 大小: 11.6 KB
  • Springboot整合(9)——Shiro
            
    
    博客分类: 框架整合
  • 大小: 15.6 KB
  • Springboot整合(9)——Shiro
            
    
    博客分类: 框架整合
  • 大小: 11.7 KB
  • Springboot整合(9)——Shiro
            
    
    博客分类: 框架整合
  • 大小: 30.9 KB