Springboot整合(9)——Shiro

visionzheng 2018-02-09

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使用不给出user,role,permission的具体实现,只演示基本流程,role和permission的设置都直接使用硬编码的方式写在代码里,实际项目使用时将硬编码改成相应的实现逻辑即可。这里将上述realm的实现逻辑流程说明一下

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

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

@Configuration

publicclass ShiroConfiguration {

 

    /**

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

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

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

     *

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

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

     *

     */

    @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,分别点击4个button,有权限的会返回结果,没有权限的会显示服务器内部错误(其实是被全局异常处理器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的三种清空策略;

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

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

            LRU,Least 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方法,之后就不再调用

使用Shiro的Remember Me

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

1. 定义Cookie Bean

    /**

     * cookie对象;

     *

     * @return

     */

    @Bean

    public SimpleCookie rememberMeCookie() {

        // 这个参数是cookie的名称,对应前端的checkbox的name = 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是否已经工作,关闭浏览器,直接输入url:user/list,也可正常访问。说明rememberMe已经正常工作。

 

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

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

Springboot整合(9)——Shiro
 

 

再次测试,一切正常

相关推荐