Shiro安全框架

一、前言

Apache Shiro 是 Java 的一个安全框架。功能强大,使用简单的Java安全框架,它为开发人员提供一个直观而全面的认证,授权,加密及会话管理的解决方案。

二、介绍

2.1 功能特点

Shiro 包含 10 个内容,如下图:

37ed236b7d56d0a815eece1c947f21a876d.jpg

1) Authentication:身份认证/登录,验证用户是不是拥有相应的身份。

2) Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限。

3) Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的。

4) Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。

5) Web Support:Web支持,可以非常容易的集成到 web 环境。

6) Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率。

7) Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去。

8) Testing:提供测试支持。

9) Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问。

10) Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

2.2 运行原理

Shiro 运行原理图1(应用程序角度)如下:

182a0fa359c8059ca6c7b9212d3762a0a41.jpg

1) Subject:主体,代表了当前“用户”。这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等。所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager。我们可以把 Subject 认为是一个门面,SecurityManager 才是实际的执行者。

2) SecurityManager:安全管理器。即所有与安全有关的操作都会与 SecurityManager 交互,且它管理着所有 Subject。可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,我们可以把它看成 DispatcherServlet 前端控制器。

3) Realm:域。Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法,也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作。我们可以把 Realm 看成 DataSource,即安全数据源。

Shiro 运行原理图2(Shiro 内部架构角度)如下:

0e4ede72e9a41252597f23aaba8475eae9f.jpg

1) Subject:主体,可以看到主体可以是任何与应用交互的“用户”。

2) SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher。它是 Shiro 的核心,所有具体的交互都通过 SecurityManager 进行控制。它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。

3) Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,我们可以自定义实现。其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了。

4) Authrizer:授权器,或者访问控制器。它用来决定主体是否有权限进行相应的操作,即控制着用户能访问应用中的哪些功能。

5) Realm:可以有1个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的。它可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等。

6) SessionManager:如果写过 Servlet 就应该知道 Session 的概念,Session 需要有人去管理它的生命周期,这个组件就是 SessionManager。而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境。

7) SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD。我们可以自定义 SessionDAO 的实现,控制 session 存储的位置。如通过 JDBC 写到数据库或通过 jedis 写入 redis 中。另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能。

8) CacheManager:缓存管理器。它来管理如用户、角色、权限等的缓存的。因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能。

9) Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于如密码加密/解密的。

2.3 过滤器

当 Shiro 被运用到 web 项目时,Shiro 会自动创建一些默认的过滤器对客户端请求进行过滤。以下是 Shiro 提供的过滤器:

过滤器简称对应的 Java 类
anonorg.apache.shiro.web.filter.authc.AnonymousFilter
authcorg.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasicorg.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
permsorg.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
portorg.apache.shiro.web.filter.authz.PortFilter
restorg.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
rolesorg.apache.shiro.web.filter.authz.RolesAuthorizationFilter
sslorg.apache.shiro.web.filter.authz.SslFilter
userorg.apache.shiro.web.filter.authc.UserFilter
logoutorg.apache.shiro.web.filter.authc.LogoutFilter
noSessionCreationorg.apache.shiro.web.filter.session.NoSessionCreationFilter

解释:

  1. /admins/**=anon # 表示该 uri 可以匿名访问
  2. /admins/**=auth # 表示该 uri 需要认证才能访问
  3. /admins/**=authcBasic # 表示该 uri 需要 httpBasic 认证
  4. /admins/**=perms[user:add:*] # 表示该 uri 需要认证用户拥有 user:add:* 权限才能访问
  5. /admins/**=port[8081] # 表示该 uri 需要使用 8081 端口
  6. /admins/**=rest[user] # 相当于 /admins/**=perms[user:method],其中,method 表示 get、post、delete 等
  7. /admins/**=roles[admin] # 表示该 uri 需要认证用户拥有 admin 角色才能访问
  8. /admins/**=ssl # 表示该 uri 需要使用 https 协议
  9. /admins/**=user # 表示该 uri 需要认证或通过记住我认证才能访问
  10. /logout=logout # 表示注销,可以当作固定配置

注意:

anon,authcBasic,auchc,user 是认证过滤器。

perms,roles,ssl,rest,port 是授权过滤器。

三、基础入门

3.1 添加依赖

<!-- Shiro核心框架 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.0</version>
        </dependency>
        <!-- Shiro使用Srping框架 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <!-- Shiro使用redis缓存框架 -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>2.4.2.1-RELEASE</version>
        </dependency>

3.2 配置文件

@Configuration
public class ShiroConfig {

    @Value("${shiro.redis.host}")
    private String host;

    @Value("${shiro.redis.password}")
    private String password;

    @Value("${shiro.redis.port}")
    private int port;

    @Value("${shiro.redis.timeout}")
    private int timeout;

    /**
     * Session超时时间,单位为毫秒(默认30分钟)
     */
    @Value("${shiro.session.expireTime}")
    private int expireTime;

    /**
     * 相隔多久检查一次session的有效性,单位毫秒,默认就是10分钟
     */
    @Value("${shiro.session.validationInterval}")
    private int validationInterval;

    /**
     * 验证码开关
     */
    @Value("${shiro.user.captchaEbabled}")
    private boolean captchaEbabled;

    /**
     * 验证码类型
     */
    @Value("${shiro.user.captchaType}")
    private String captchaType;

    /**
     * 设置Cookie的域名
     */
    @Value("${shiro.cookie.domain}")
    private String domain;

    /**
     * 设置cookie的有效访问路径
     */
    @Value("${shiro.cookie.path}")
    private String path;

    /**
     * 设置HttpOnly属性
     */
    @Value("${shiro.cookie.httpOnly}")
    private boolean httpOnly;

    /**
     * 设置Cookie的过期时间,秒为单位
     */
    @Value("${shiro.cookie.maxAge}")
    private int maxAge;

    /**
     * 登录地址
     */
    @Value("${shiro.user.loginUrl}")
    private String loginUrl;

    /**
     * 权限认证失败地址
     */
    @Value("${shiro.user.unauthorizedUrl}")
    private String unauthorizedUrl;

    /**
     * Shiro过滤器配置
     * Filter Chain定义说明
     * 1、一个URL可以配置多个Filter,使用逗号分隔
     * 2、当设置多个过滤器时,全部验证通过,才视为通过
     * 3、部分过滤器可指定参数,如perms,roles
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        // Shiro的核心安全接口,这个属性是必须的
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
        shiroFilterFactoryBean.setLoginUrl(loginUrl);

        /*// 登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/index");
        // 权限认证失败,则跳转到指定页面
        shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);*/

        // 自定义拦截器
        Map<String, Filter> filters = new LinkedHashMap<>();
        filters.put("captchaValidate", captchaValidateFilter());
        // 注销成功,则跳转到指定页面
        filters.put("logout", logoutFilter());
        // 重写user认证
        filters.put("myUser", myUserFilter());
        shiroFilterFactoryBean.setFilters(filters);

        // Shiro连接约束配置,即权限控制
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 对静态资源设置匿名访问
        filterChainDefinitionMap.put("/swagger**", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/v2/api-docs", "anon");
        filterChainDefinitionMap.put("/swagger-resources/**", "anon");
        filterChainDefinitionMap.put("/", "anon");
        // 退出logout地址,shiro去清除session
        filterChainDefinitionMap.put("/logout", "logout");
        // 不需要拦截的访问
        filterChainDefinitionMap.put("/login", "anon,captchaValidate");

        // 所有请求需要认证 user:配置记住我或认证通过可以访问 authc: 需要认证才能进行访问 anon:所有url都都可以匿名访问
//        filterChainDefinitionMap.put("/**", "user");
        filterChainDefinitionMap.put("/**", "myUser");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }

    @Bean
    public MyRedisCacheManager redisCacheManager() {
        MyRedisCacheManager redisCacheManager = new MyRedisCacheManager();
        redisCacheManager.setRedisTemplate(myRedisTemplate());
        redisCacheManager.setExpireTime(expireTime);
        return redisCacheManager;
    }

    /**
     * 安全管理器
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置realm
        securityManager.setRealm(myShiroRealm());
        // 注入缓存管理器,使用redis
        securityManager.setCacheManager(redisCacheManager());
        // session管理器,使用redis
        securityManager.setSessionManager(sessionManager());
        // 记住我
        securityManager.setRememberMeManager(rememberMeManager());
        return securityManager;
    }

    /**
     * 凭证匹配器
     * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了)
     *
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new RetryLimitHashedCredentialsMatcher();
        // 散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        // 散列的次数,比如散列两次,相当于 md5(md5(""));
        hashedCredentialsMatcher.setHashIterations(2);
        return hashedCredentialsMatcher;
    }

    /**
     * 身份认证realm
     */
    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return myShiroRealm;
    }

    /**
     * 缓存管理器,使用redis实现
     */
    @Bean
    public MyRedisCacheManager cacheManager() {
        MyRedisCacheManager redisCacheManager = new MyRedisCacheManager();
        redisCacheManager.setRedisTemplate(myRedisTemplate());
        return redisCacheManager;
    }

    /**
     * 配置shiro redisManager
     * 使用的是shiro-redis开源插件
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        redisManager.setExpire(expireTime * 60);
        redisManager.setTimeout(timeout);
        redisManager.setPassword(password);
        return redisManager;
    }

    /**
     * 自定义sessionManager
     * @return
     */
    @Bean
    public SessionManager sessionManager() {
        MySessionManager mySessionManager = new MySessionManager();
        mySessionManager.setSessionDAO(redisSessionDAO());
        mySessionManager.setCacheManager(redisCacheManager());
        //设置的最大时间,正负都可以,为负数时表示永不超时。系统默认超时时间是180000毫秒(30分钟)(现在设置为一天)
        mySessionManager.setGlobalSessionTimeout(1000*60*expireTime);
        return mySessionManager;
    }

    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public MyRedisSessionDao redisSessionDAO() {
        MyRedisSessionDao redisSessionDAO = new MyRedisSessionDao();
        redisSessionDAO.setRedisTemplate(myRedisTemplate());
        redisSessionDAO.setExpireTime(expireTime);
        return redisSessionDAO;
    }

    /**
     * 退出过滤器
     */
    public LogoutFilter logoutFilter() {
        LogoutFilter logoutFilter = new LogoutFilter();
        logoutFilter.setLoginUrl(loginUrl);
        return logoutFilter;
    }

    /**
     * 自定义用户认证拦截器
     * @return
     */
    public MyUserFilter myUserFilter() {
        return new MyUserFilter();
    }

    /**
     * 自定义验证码过滤器
     */
    @Bean
    public CaptchaValidateFilter captchaValidateFilter() {
        CaptchaValidateFilter captchaValidateFilter = new CaptchaValidateFilter();
        captchaValidateFilter.setCaptchaEbabled(captchaEbabled);
        captchaValidateFilter.setCaptchaType(captchaType);
        return captchaValidateFilter;
    }

    /**
     * cookie属性设置
     */
    public SimpleCookie rememberMeCookie() {
        // 这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
        SimpleCookie cookie = new SimpleCookie("rememberMe");
        cookie.setDomain(domain);
        cookie.setPath(path);
        cookie.setHttpOnly(httpOnly);
        // 记住我cookie生效时间30天,单位秒
        cookie.setMaxAge(maxAge * 24 * 60 * 60);
        return cookie;
    }

    /**
     * cookie管理对象,记住我功能
     */
    public CookieRememberMeManager rememberMeManager() {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        // rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
        cookieRememberMeManager.setCipherKey(Base64.decode("fCq+/xW488hMTCD+cmJ3aQ=="));
        return cookieRememberMeManager;
    }

    /**
     * 开启Shiro代理
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
        proxyCreator.setProxyTargetClass(true);
        return proxyCreator;
    }

    /**
     * 开启shiro aop注解支持
     * 使用代理方式,所以需要开启代码支持
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
            @Qualifier("securityManager") SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    @Bean
    public JedisConnectionFactory redisConnectionFactory() {
        JedisConnectionFactory factory = new JedisConnectionFactory();
        factory.setHostName(host);
        factory.setPort(port);
        //设置连接超时时间
        factory.setTimeout(timeout);
        factory.setPassword(password);
        return factory;
    }

    @Bean
    public RedisTemplate<String, Object> myRedisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory());

        //设置序列化工具,这样ReportBean不需要实现Serializable接口
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new RedisObjectSerializer());
        template.afterPropertiesSet();
        return template;
    }

    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
        StringRedisTemplate template = new StringRedisTemplate(factory);
        //设置序列化工具,这样ReportBean不需要实现Serializable接口
        setSerializer(template);
        template.afterPropertiesSet();
        return template;
    }

    private void setSerializer(StringRedisTemplate template) {
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // 存储Map时value需要的序列化配置
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
    }

}

3.3 自定义Realm

Shiro 自带的 IniRealm,从 ini 配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义 realm。

e80490b1f2063f5387be9f4b078a9e655a3.jpg

其中,最基础的是 Realm 接口,CachingRealm 负责缓存处理,AuthenticationRealm 负责认证,AuthorizingRealm负责授权,通常自定义的 realm 继承 AuthorizingRealm

自定义 Realm:

/**
 * shiro身份校验核心类
 */
public class MyShiroRealm extends AuthorizingRealm {

    @Autowired
    private IMenuService menuService;

    @Autowired
    private IRoleService roleService;

    @Autowired
    private LoginService loginService;

    /**
     * 身份认证:验证用户输入的账号和密码是否正确
     * 只有用户登录的时候才会执行身份认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)
            throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
        String username = token.getUsername();
        String password = String.valueOf(token.getPassword());

        SysUser sysUser = loginService.authentication(username, password);

        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(
                sysUser,
                sysUser.getPassword(),
                ByteSource.Util.bytes(sysUser.getCredentialsSalt()),
                getName());

        return info;
    }

    /**
     * 授权
     * 每次URL请求时,都要经过这边授权,通过才放行。
     * 注意:第二次请求会走缓存,所以有任何权限变更都需要去清理缓存权限,否则权限变更不生效
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
        Long sysUserId = ShiroUtils.getSysUserId();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 角色加入AuthorizationInfo认证对象
        authorizationInfo.setRoles(roleService.selectRoleKeys(sysUserId));
        // 权限加入AuthorizationInfo认证对象
        authorizationInfo.setStringPermissions(menuService.selectPermsByUserId(sysUserId));
        return authorizationInfo;
    }

    /**
     * 清理缓存权限
     */
    public void clearCachedAuthorizationInfo() {
        this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
    }

}

参考

https://www.cnblogs.com/moonlightL/p/8126910.html

转载于:https://my.oschina.net/lienson/blog/3029143

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值