shiro学习笔记

2017-12-16  本文已影响55人  wch853

权限管理

为了实现对用户访问系统的控制,按照安全规则或安全策略控制用户可以访问且只能访问自己被授权的资源。

用户认证

为了验证用户访问系统的合法性。

用户授权

在用户认证通过后,只能访问被系统授权的资源,授权过程可以理解为who对what(which)进行how操作

关键对象

权限模型

权限控制

基于角色的访问控制

RBAC(Role Based Access Control),基于角色的访问控制

基于资源的访问控制

RBAC(Resource Based Access Control),基于资源的访问控制

权限粒度

shiro架构

shiro架构
shiro缓存

当需要访问受限资源时,会实时去查询权限数据,这样的查询是频繁的,而权限信息又不是经常变化的,所以需要配置缓存来提高性能。
缓存带来的问题:当用户不退出系统(正常退出、非正常退出),是不会清空缓存的,如果权限发生变更,不能及时改变用户所拥有的权限。

shiro会话

shiro支持通过SessionManager取代web容器来管理会话,可以通过配置SessionDao(对Session的CRUD)集成Reis集群来对session进行共享、更新、删除。

使用Spring集成Shiro

数据库设计
DROP TABLE IF EXISTS users;
CREATE TABLE users (
  id       INT          NOT NULL AUTO_INCREMENT
  COMMENT '用户编号',
  name     VARCHAR(255) NOT NULL
  COMMENT '用户名称',
  username VARCHAR(255) NOT NULL
  COMMENT '账号',
  password VARCHAR(255) NOT NULL
  COMMENT '密码',
  salt     VARCHAR(255) NOT NULL
  COMMENT '盐',
  status   TINYINT      NOT NULL DEFAULT 1
  COMMENT '用户状态 0-无效,1-有效',
  PRIMARY KEY (id),
  UNIQUE KEY (username)
)
  ENGINE = INNODB
  DEFAULT CHARSET = utf8
  COMMENT = '用户';

DROP TABLE IF EXISTS roles;
CREATE TABLE roles (
  id        INT          NOT NULL AUTO_INCREMENT
  COMMENT '角色编号',
  role_name VARCHAR(255) NOT NULL
  COMMENT '角色名称',
  PRIMARY KEY (id)
)
  ENGINE = INNODB
  DEFAULT CHARSET = utf8
  COMMENT = '角色';

DROP TABLE IF EXISTS permission;
CREATE TABLE permission (
  id       INT          NOT NULL AUTO_INCREMENT
  COMMENT '权限编号',
  url      VARCHAR(255) NOT NULL
  COMMENT 'url地址',
  url_name VARCHAR(255) NOT NULL
  COMMENT 'url描述',
  perm     VARCHAR(255) NOT NULL
  COMMENT '权限标识符',
  PRIMARY KEY (id)
)
  ENGINE = INNODB
  DEFAULT CHARSET = utf8
  COMMENT = '权限';

DROP TABLE IF EXISTS user_roles;
CREATE TABLE user_roles (
  user_id INT NOT NULL
  COMMENT '用户编号',
  role_id INT NOT NULL
  COMMENT '角色编号',
  PRIMARY KEY (user_id, role_id)
)
  ENGINE = INNODB
  DEFAULT CHARSET = utf8
  COMMENT = '用户-角色';

DROP TABLE IF EXISTS role_permissions;
CREATE TABLE role_permissions (
  role_id       INT NOT NULL
  COMMENT '角色编号',
  permission_id INT NOT NULL
  COMMENT '权限编号',
  PRIMARY KEY (role_id, permission_id)
)
  ENGINE = INNODB
  DEFAULT CHARSET = utf8
  COMMENT = '角色-权限';
依赖

除了基本的Spring依赖,还需要shiro-spring、shiro-cache、aspectj。

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.4.0</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.11</version>
        </dependency>
spring-shiro配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- shiroFilter -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <!-- 登录地址(登录页面地址,不拦截,登录失败跳回该页) -->
        <property name="loginUrl" value="/"/>
        <!-- 成功登录跳转地址 -->
        <property name="successUrl" value="/home"/>
        <!-- 自定义表单验证filter配置 -->
        <property name="filters">
            <map>
                <entry key="authc" value-ref="authFormFilter" />
            </map>
        </property>
        <!-- 过滤器链定义,由上往下顺序执行 -->
        <property name="filterChainDefinitions">
            <value>
                <!-- 设置静态资源匿名访问 -->
                /resources/** = anon
                <!-- ajax登录url,不拦截 -->
                /login = anon
                <!-- 配置登出url -->
                /logout = logout
                <!-- 此处可以配置权限,也可在类或方法上标注
                /home = authc
                /query = perms[/query]
                /add = perms[/add]
                /update = perms[/update]
                /delete = perms[/delete]
                -->
            </value>
        </property>
    </bean>

    <!-- securityManager -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="authRealm"/>
        <property name="cacheManager" ref="cacheManager"/>
        <property name="sessionManager" ref="sessionManager"/>
    </bean>

    <!-- 配置realm,用于认证、授权 -->
    <bean id="systemRealm" class="com.wch.ssm.shiro.realm.SystemRealm">
        <property name="credentialsMatcher" ref="credentialsMatcher"/>
    </bean>

    <!-- 配置凭证匹配器,加密方式和hash次数 -->
    <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
        <property name="hashAlgorithmName" value="md5"/>
        <property name="hashIterations" value="1"/>
    </bean>

    <!-- cacheManager -->
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:config/shiro-ehcache.xml"/>
    </bean>

    <!-- sessionManager -->
    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <!-- 设置session的失效时长 -->
        <property name="globalSessionTimeout" value="600000"/>
        <!-- 删除失效的session -->
        <property name="deleteInvalidSessions" value="true"/>
    </bean>

    <!-- 配置自定义表单验证过滤器 -->
    <bean id="authFormFilter" class="com.wch.ssm.shiro.AuthFormFilter"/>

</beans>
自定义Realm
/**
 * 自定义Realm,用于认证和授权
 */
public class AuthRealm extends AuthorizingRealm {

    @Resource
    private SecurityService securityService;

    private static final Logger LOGGER = LoggerFactory.getLogger(AuthRealm.class);

    /**
     * 认证
     *
     * @param token token
     * @return AuthenticationInfo
     * @throws AuthenticationException AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        User user = securityService.getPasswordAndSalt(username);

        if (null == user) {
            throw new UnknownAccountException("不存在该账户!");
        }

        String name = user.getName();
        String password = user.getPassword();
        String salt = user.getSalt();
        if (null == name || null == password || null == salt) {
            throw new AccountException("账户异常!");
        }

        // 身份信息,密码(数据库中加密后的密码),salt,realmName
        return new SimpleAuthenticationInfo(user, password, ByteSource.Util.bytes(salt), this.getName());
    }

    /**
     * 授权
     *
     * @param principals principals
     * @return AuthorizationInfo
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo info = null;
        try {
            // 获取身份信息
            User user = (User) principals.getPrimaryPrincipal();
            // 查询权限信息
            Set<String> permissions = securityService.getStringPermissions(user.getId());
            info = new SimpleAuthorizationInfo();
            info.addStringPermissions(permissions);
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
        return info;
    }

    /**
     * 用户权限发生变动,调用此方法清除缓存
     */
    public void clearCache() {
        PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
        super.clearCache(principals);
    }
}
控制器
    /**
     * 验证登录
     *
     * @return json data
     * @throws ShiroException ShiroException
     */
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public @ResponseBody
    Result login(String username, String password) {
        try {
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            // 登录失败:包括账户不存在、密码错误等,都会抛出ShiroException
            SecurityUtils.getSubject().login(token);
            return Result.response(ResultEnum.SUCCESS);
        } catch (ShiroException e) {
            LOGGER.error("登录失败,{},{}", e.getClass().getName(), e.getMessage());
            return Result.response(ResultEnum.FAIL);
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
            return Result.response(ResultEnum.FAIL);
        }
    }

    /**
     * successUrl
     * 使用注解 @RequiresAuthentication 来标注该访问该url需要认证
     *
     * @param model model
     * @return Page
     */
    @RequestMapping("/home")
    @RequiresAuthentication
    public String home(Model model) {
        // 获取在身份认证时放入的身份信息
        User user = (User) SecurityUtils.getSubject().getPrincipal();
        model.addAttribute("name", user.getName());
        return "home";
    }

    /**
     * unauthorizedUrl,未授权时跳转该url
     *
     * @return json
     */
    @ExceptionHandler(UnauthorizedException.class)
    @RequiresAuthentication
    public @ResponseBody
    String forbidden() {
        return "403";
    }

    /**
     * 使用 @RequiresPermissions 注解来标注访问该url需要 "user:query" 权限
     *
     * @return json
     */
    @RequestMapping("/query")
    @RequiresPermissions("user:query")
    public @ResponseBody
    String query() {
        return "permit query.";
    }
登录交互
<script type="text/javascript">
    $('#submit').click(function () {
        $.ajax({
            url: 'login',
            type: 'POST',
            data: {
                username: $('#username').val().trim(),
                password: $('#password').val().trim()
            },
            success: function (res) {
                if (res.code === 200) {
                    window.location.href = 'home'
                } else {
                    alert("Login Failed!");
                }
            }
        });
    });
</script>

使用SpringBoot集成shiro

配置ShiroConfig

对于需要配置权限的url,每个都配置注解是很不方便的,可以通过应用启动时查询持久化到数据库中的权限配置来生成拦截器链。
ShiroConfig加载到容器中时,查询权限的Service可能还未注入,导致空指针异常。因此在ShiroConfig中应使用手动注入的方式来获取查询权限Service。

获取ApplicationContext

为了获取ApplicationContext,ShiroConfig需要实现ApplicationContextAware接口,实现setApplicationContext()方法。

    private ApplicationContext context;

    /**
     * 获取ApplicationContext
     *
     * @param applicationContext applicationContext
     * @throws BeansException BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
适配权限标识

在自定义Realm中重写的doGetAuthorizationInfo()方法,返回类型SimpleAuthorizationInfo,添加权限的方式是通过ddStringPermissions(Collection<String> permissions)添加权限的字符串形式,例如sys:add,但是在拦截器链中配置权限的要求是perms[sys:add]的形式,因此需要对权限标识进行适配。

     /**
     * 适配拦截器权限标识符
     *
     * @param perm perm
     * @return perms[]
     */
    private String adaptPerms(String perm) {
        StringBuilder sb = new StringBuilder();
        sb.append("perms[").append(perm).append("]");
        return sb.toString();
    }
配置拦截器链
    // 拦截器链,由上到下顺序执行
    Map<String, String> filterChain = new LinkedHashMap<>();

    // 动态添加权限
    SecurityService securityService = null;
    while (securityService == null) {
        securityService = (SecurityService) context.getBean("securityServiceImpl");
    }
    List<Permission> permissions = securityService.getPermissions();
    for (Permission permission : permissions) {
      filterChain.put(permission.getUrl(), this.adaptPerms(permission.getPerm()));
    }
完整配置
@Configuration
public class ShiroConfig implements ApplicationContextAware {

    private ApplicationContext context;

    /**
     * 配置realm,用于认证、授权
     *
     * @return Realm
     */
    @Bean
    public Realm authRealm() {
        // 凭证匹配器,配置加密方式和hash次数
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(CommonConstants.HASH_CREDENTIAL_NAME);
        credentialsMatcher.setHashIterations(CommonConstants.HASH_ITERATIONS);

        AuthRealm authRealm = new AuthRealm();
        authRealm.setCredentialsMatcher(credentialsMatcher);
        return authRealm;
    }

    /**
     * 配置EhCache缓存管理器,用于授权信息缓存
     *
     * @return CacheManager
     */
    private CacheManager getEhCacheManager() {
        EhCacheManager cacheManager = new EhCacheManager();
        cacheManager.setCacheManagerConfigFile("classpath:config/shiro-ehcache.xml");
        return cacheManager;
    }

    /**
     * 配置SecurityManager
     *
     * @return SecurityManager
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(authRealm());
        securityManager.setCacheManager(getEhCacheManager());
        return securityManager;
    }

    /**
     * 设置由servlet容器管理filter生命周期
     *
     * @return LifecycleBeanPostProcessor
     */
    @Bean
    public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 开启aop,对类代理
     *
     * @return Proxy
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /**
     * 开启shiro注解支持
     *
     * @return Advisor
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager());
        return advisor;
    }

    /**
     * 配置shiroFilter,beanName必须为shiroFilter
     *
     * @return ShiroFilter
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter() {
        ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
        // 配置SecurityManager
        filter.setSecurityManager(securityManager());
        // 配置登录页
        filter.setLoginUrl("/");
        // 登录成功跳转链接
        filter.setSuccessUrl("/sys");
        // 未授权界面
        filter.setUnauthorizedUrl("/403");
        // 拦截器链,由上到下顺序执行
        Map<String, String> filterChain = new LinkedHashMap<>();
        // 配置ajax登录url匿名访问
        filterChain.put("/login", "anon");
        // 配置登出路径
        filterChain.put("/logout", "logout");
        // 静态资源处理
        filterChain.put("/js/**", "anon");
        filterChain.put("/css/**", "anon");
        filterChain.put("/img/**", "anon");

        // 动态添加权限
        SecurityService securityService = null;
        while (securityService == null) {
            securityService = (SecurityService) context.getBean("securityServiceImpl");
        }
        List<Permission> permissions = securityService.getPermissions();
        for (Permission permission : permissions) {
            filterChain.put(permission.getUrl(), this.adaptPerms(permission.getPerm()));
        }

        // 认证后访问
        filterChain.put("/**", "authc");
        filter.setFilterChainDefinitionMap(filterChain);
        return filter;
    }

    /**
     * 适配拦截器权限标识符
     *
     * @param perm perm
     * @return perms[]
     */
    private String adaptPerms(String perm) {
        StringBuilder sb = new StringBuilder();
        sb.append("perms[").append(perm).append("]");
        return sb.toString();
    }

    /**
     * 获取ApplicationContext
     *
     * @param applicationContext applicationContext
     * @throws BeansException BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
}
上一篇 下一篇

猜你喜欢

热点阅读