概述

Shiro是一个功能强大且灵活的开源Java安全框架,相比于SpringSecurity更加简单,Shiro可以执行身份验证、授权、加密和会话管理等

Shiro的主要功能如下图:

SpringBoot权限控制 权限管理springboot_spring boot

  • Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
  • SessionManager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境,也可以是Web环境的;
  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明储;
  • Web Support:Web支持,可以非常容易的集成到Web 环境;
  • Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
  • Concurrency:Shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
  • Testing:提供测试支持;
  • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
  • RememberMe:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了
    从应用程序的角度来看Shiro是如何完成工作的,如下图:
    主要涉及3个概念
  • Subject:应用代码直接交互的对象是Subject,也就是说Shiro的对外API 核心就是Subject。Subject代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;与Subject的所有交互都会委托给SecurityManager;Subject 其实是一个门面,SecurityManager才是实际的执行者;
  • SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且其管理着所有Subject;可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC中DispatcherServlet的角色
  • Realm:Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm 看成DataSource
    详细架构如下图:
    - Subject(org.apache.shiro.subject.Subject):可以看到主体可以是任何可以与应用交互的 “用户”;
  • SecurityManager(org.apache.shiro.mgt.SecurityManager):相当于SpringMVC中的DispatcherServlet;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证、授权、会话及缓存的管理。
  • Authenticator(org.apache.shiro.authc.Authenticator):负责Subject认证,是一个扩展点,可以自定义实现;可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
  • Authorizer (org.apache.shiro.authz.Authorizer):授权器、即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
  • SessionManager(org.apache.shiro.session.mgt.SessionManager):管理Session生命周期的组件;而Shiro并不仅仅可以用在Web 环境,也可以用在如普通的JavaSE环境
  • CacheManager (org.apache.shiro.cache.CacheManager):缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能
  • Cryptography(org.apache.shiro.crypto.*):密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密。
  • Realms(org.apache.shiro.realm.Realm):可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC 实现,也可以是内存实现等等;由用户提供;所以一般在应用中都需要实现自己的Realm;
    更多关于Shiro的知识,请阅读Shiro官网

SpringBoot整合Shiro

技术栈

本文主要使用的技术如下:
JDK1.8
Maven3.6.1
SpringBoot2.3.4
shiro-spring-boot-web-starter1.6.0(springboot提供)
spring-boot-starter-thymeleaf
bootstrap

引入依赖

核心依赖如下:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.6.0</version>
</dependency>

配置文件

@Configuration
public class ShiroConfig {
    /**
     * 配置自定义Realm
     * @return
     */
    @Bean
    public UserRealm userRealm() {
        UserRealm userRealm = new UserRealm();
        userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return userRealm;
    }
    /**
     * 配置安全管理器
     * @return
     */
    @Bean
    public SessionsSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm());
        return securityManager;
    }
    /**
     * 配置过滤条件和跳转条件
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SessionsSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //认证失败,跳转到登录页面
        shiroFilterFactoryBean.setLoginUrl("/login");
        //认证成功,跳转到首页
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //权限认证失败,跳转到无权限页面
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");
        //配置过滤条件
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        //过滤静态页面
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/images/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        //过滤swagger API接口页面
        filterChainDefinitionMap.put("/swagger-ui", "anon");
        filterChainDefinitionMap.put("/swagger-ui/**", "anon");
        filterChainDefinitionMap.put("/swagger-resources/**", "anon");
        filterChainDefinitionMap.put("/v3/api-docs", "anon");
        filterChainDefinitionMap.put("/csrf", "anon");
        //登录和退出不需要拦截
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/logout", "logout");
        filterChainDefinitionMap.put("/unauth", "anon");
        //其他都需要拦截
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }
    /**
     * 配置Shiro方言,用于shiro整合thymeleaf
     * @return
     */
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }
    /**
     * 开启Shiro注解通知器
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SessionsSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
    /**
     * 配置密码适配器
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //散列算法
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");
        //加密迭代次数
        hashedCredentialsMatcher.setHashIterations(2);
        return hashedCredentialsMatcher;
    }
    /**
     * 配置动态代理
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        advisorAutoProxyCreator.setUsePrefix(true);
        return advisorAutoProxyCreator;
    }
}

核心代码

自定义Realm实现AuthorizingRealm接口的两个方法,主要代码如下:

/**
 * 授权
 * @param principalCollection
 * @return
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    logger.info("Shiro授权....");
    SysUser user = (SysUser) principalCollection.getPrimaryPrincipal();
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
    //查询角色信息
    Set<String> roles = new HashSet<String>();
    //查询菜单权限信息
    Set<String> perms = new HashSet<String>();
    roles = roleService.selectRoleCodesByUserId(user.getId());
    perms = menuService.selectPermsByUserId(user.getId());
    authorizationInfo.setRoles(roles);
    authorizationInfo.setStringPermissions(perms);
    return authorizationInfo;
}
/**
 * 认证
 * @param authenticationToken
 * @return
 * @throws AuthenticationException
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    logger.info("Shiro认证...");
    UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
    String username = userToken.getUsername();
    SysUser user = null;
    try {
        user = userService.login(username);
    } catch (UserNotExistsException e) {
        throw new UnknownAccountException(e.getMessage(), e);
    } catch (UserBlockedException e) {
        throw new LockedAccountException(e.getMessage(), e);
    } catch (Exception e) {
        logger.warn("验证用户[" + username + "]未通过!");
        throw new AuthenticationException(e.getMessage(), e);
    }
    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName());
    return authenticationInfo;
}

登录接口如下:

@Controller
public class LoginController {
    private static Logger logger = LoggerFactory.getLogger(LoginController.class);
    @GetMapping("/login")
    public String login(HttpServletRequest request, HttpServletResponse response) {
        return "login";
    }
    @RequestMapping("/login")
    @ResponseBody
    public AjaxResult login(String username, String password) {
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
            logger.info("登录成功!");
            return AjaxResult.ok().message("登录成功!");
        } catch (UnknownAccountException e) {
            return AjaxResult.error().message("用户名或密码不正确!");
        } catch (IncorrectCredentialsException e) {
            return AjaxResult.error().message("用户名或密码不正确!");
        }
    }
    @GetMapping("/unauth")
    public String unauth() {
        return "unauth";
    }
}

前端使用bootstrap和jquery实现,登录界面代码如下:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
    <title>登录</title>
    <link href="../static/favicon.ico" th:href="@{favicon.ico}" rel="icon"/>
    <link href="../static/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" rel="stylesheet" type="text/css"/>
    <link href="../static/css/login.min.css" th:href="@{/css/login.min.css}" rel="stylesheet" type="text/css"/>
</head>
<body>
    <div class="container">
        <form class="form-signin">
            <h2 class="form-signin-heading">登录</h2>
            <label for="username" class="sr-only">用户名:</label>
            <input type="text" id="username" class="form-control" placeholder="用户名" autofocus>
            <label for="password" class="sr-only">密码</label>
            <input type="password" id="password" class="form-control" placeholder="密码" required>
            <!--<div class="checkbox">
                <label>
                    <input type="checkbox" value="rememberMe">记住我
                </label>
            </div>-->
            <button class="btn btn-lg btn-primary btn-block" type="button" onclick="login()">登录</button>
        </form>
    </div>
    <script src="../static/js/jquery-3.5.1.min.js" th:src="@{/js/jquery-3.5.1.min.js}" type="text/javascript"></script>
    <script src="../static/js/bootstrap.min.js" th:src="@{/js/bootstrap.min.js}" type="text/javascript"></script>
    <script>
        function login() {
            var username = $("#username").val();
            var password = $('#password').val();
            $.ajax({
                type: "post",
                url: "/login",
                data: {
                    username: username,
                    password: password
                },
                success: function(data) {
                    if (data.code == 20000) {
                        location.href = '/index';
                    } else {
                        alert(data.message);
                    }
                }
            });
            return false;
        }
    </script>
</body>
</html>

效果展示

启动项目后,浏览器输入http://localhost:8089

SpringBoot权限控制 权限管理springboot_css_02

输入用户名和密码:admin/123456进入首页

SpringBoot权限控制 权限管理springboot_css_03

点击有权限的用户查询会返回查询的数据,没有授权的会提示无权限访问

Shiro自带的常见异常有:

DisabledAccountException:账户被禁用

LockedAccountException:账户被锁定

UnknownAccountException:账户不存在

ExcessiveAttemptsException:登录失败次数过多

IncorrectCredentialsException:密码不正确

ExpiredCredentialsException:密码已过期

完整代码详见码云

若想了解SpringBoot整合SpringSecurity+jwt的步骤,请参阅SpringBoot整合SpringSecurity+Jwt实现权限管理 若想了解SpringBoot整合SpringSecurity的步骤,请参阅SpringBoot整合SpringSecurity实现权限管理