Shiro安全框架

Shiro安全框架

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

主要功能

shiro主要有三大功能模块:

  1. Subject:主体,一般指用户。

  2. SecurityManager:安全管理器,管理所有Subject,可以配合内部安全组件。(类似于SpringMVC中的DispatcherServlet)

  3. Realms:用于进行权限信息的验证,一般需要自己实现。

细分功能

  1. Authentication:身份认证/登录(账号密码验证)。
  2. Authorization:授权,即角色或者权限验证。
  3. Session Manager:会话管理,用户登录后的session相关管理。
  4. Cryptography:加密,密码加密等。
  5. Web Support:Web支持,集成Web环境。
  6. Caching:缓存,用户信息、角色、权限等缓存到如redis等缓存中。
  7. Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问。
  8. Remember Me:记住我,登录后,下次再来的话不用登录了。

Maven依赖

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

快速入门语句

    // 得到DefaultSecurityManager对象
    DefaultSecurityManager defaultSecurityManager=new DefaultSecurityManager();
    // 读取ini配置文件
    IniRealm iniRealm=new IniRealm("classpath:shiro.ini");
    // 配置DefaultSecurityManager对象
    defaultSecurityManager.setRealm(iniRealm);
    // 获取SecurityUtils对象
    SecurityUtils.setSecurityManager(defaultSecurityManager);


    // 获取当前用户对象 Subject
    Subject currentUser = SecurityUtils.getSubject();

    // 通过当前用户获取Session
    Session session = currentUser.getSession();
    
    //判断用户是否被认证
    currentUser.isAuthenticated()
    
    //通过Token进行登录操作
    currentUser.login(token)
        
    //根据输入账户名和密码获取Token    
    UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
    
    //判断用户的身份
    currentUser.hasRole("schwartz")
        
    //判断用户拥有的权限
    currentUser.isPermitted("lightsaber:wield")
        
    //注销当前用户    
    currentUser.logout();    

SpringBoot继承Shiro

Maven依赖

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

创建Realm类

    public class CustomRealm extends AuthorizingRealm &#123;

    @Autowired
    private LoginService loginService;

    /**
     * @MethodName doGetAuthorizationInfo
     * @Description 权限配置类
     * @Param [principalCollection]
     * @Return AuthorizationInfo
     * @Author WangShiLin
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) &#123;
        //获取登录用户名
        String name = (String) principalCollection.getPrimaryPrincipal();
        //查询用户名称
        User user = loginService.getUserByName(name);
        //添加角色和权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        for (Role role : user.getRoles()) &#123;
            //添加角色
            simpleAuthorizationInfo.addRole(role.getRoleName());
            //添加权限
            for (Permissions permissions : role.getPermissions()) &#123;
                //将用户拥有的权限加载到获取权限中
                simpleAuthorizationInfo.addStringPermission(permissions.getPermissionsName());
            &#125;
        &#125;
        return simpleAuthorizationInfo;
    &#125;
    
    /**
     * @MethodName doGetAuthenticationInfo
     * @Description 认证配置类
     * @Param [authenticationToken]
     * @Return AuthenticationInfo
     * @Author WangShiLin
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException &#123;
         if (StringUtils.isEmpty(authenticationToken.getPrincipal())) &#123;
            return null;
        &#125;
        //获取用户信息
        String name = authenticationToken.getPrincipal().toString();
        User user = loginService.getUserByName(name);
        if (user == null) &#123;
            //这里返回后会报出对应异常
            return null;
        &#125; else &#123;
            //这里验证authenticationToken和simpleAuthenticationInfo的信息
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, user.getPassword().toString(),ByteSource.Util.bytes("x23*2d"),getName());
            return simpleAuthenticationInfo;
        &#125;
     &#125;

创建Realm类继承AuthorizingRealm,重写doGetAuthorizationInfo(授权配置)、doGetAuthenticationInfo(认证配置)方法。

其中AuthenticationToken 用于收集用户提交的身份(如用户名)及凭据(如密码)。

其中ByteSource.Util.bytes方法为用户设置时的随机盐值。

创建ShiroConfig配置类

@Configuration
public class ShiroConfig &#123;
    //将自己的验证方式加入容器
    @Bean
    public CustomRealm myShiroRealm() &#123;
        CustomRealm myShiroRealm = new CustomRealm();
        //设置realm hash验证
        HashedCredentialsMatcher credentialsMatcher= new HashedCredentialsMatcher();
        //使用加密方法
        credentialsMatcher.setHashAlgorithmName("md5");
        //散列次数
        credentialsMatcher.setHashIterations(1024);
        myShiroRealm.setCredentialsMatcher(credentialsMatcher);
        return myShiroRealm;
    &#125;

    //权限管理,配置主要是Realm的管理认证
    @Bean
    public DefaultWebSecurityManager  securityManager() &#123;
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setSessionManager(sessionManager());
        //绑定Reaml
        securityManager.setRealm(myShiroRealm);
        return securityManager;
    &#125;

    //Filter工厂,设置对应的过滤条件和跳转条件
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) &#123;
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> map = new HashMap<>();
        //登出
        map.put("/logout", "logout");
        //对所有用户认证
        map.put("/**", "authc");
        //登录
        shiroFilterFactoryBean.setLoginUrl("/login");
        //首页
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //错误页面,认证不通过跳转
        shiroFilterFactoryBean.setUnauthorizedUrl("/error");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    &#125;
    
    // 必须使用session管理器,才能够解决前后端分离shiro的subject未认证的问题
    @Bean
    public SessionManager sessionManager()&#123;
        //将我们继承后重写的shiro session 注册
        ShiroSession shiroSession = new ShiroSession();
        //如果后续考虑多tomcat部署应用,可以使用shiro-redis开源插件来做session 的控制,或者nginx 的负载均衡
        shiroSession.setSessionDAO(new EnterpriseCacheSessionDAO());
        return shiroSession;
    &#125;

    /**
     * Shiro生命周期处理器
     */
    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() &#123;
        return new LifecycleBeanPostProcessor();
    &#125;

    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
     */
    @Bean
    @DependsOn(&#123;"lifecycleBeanPostProcessor"&#125;)
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() &#123;
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    &#125;

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() &#123;
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    &#125;
&#125;

其中shiro内置过滤器:

    anno:无需认证即可访问
    authc:必须认证才可以访问
    user :不许拥有记住我功能才能访问
    perms:拥有对某个资源访问权限才能使用  ((perms认证必须放在authc认证前,否则无效))
    role:拥有某个角色权限才能访问

权限限定访问:

map.put("/set","perms[user:set]");   //只限定拥有‘user:set’权限的用户访问

ShiroSessionManager类

/**
 *      目的: shiro 的 session 管理
 *      自定义session规则,实现前后分离,在跨域等情况下使用token 方式进行登录验证才需要,否则没必须使用本类。
 *      shiro默认使用 ServletContainerSessionManager 来做 session 管理,它是依赖于浏览器的 cookie 来维护 session 的,
 *      调用 storeSessionId  方法保存sesionId 到 cookie中
 *      为了支持无状态会话,我们就需要继承 DefaultWebSessionManager
 *      自定义生成sessionId 则要实现 SessionIdGenerator
 *
 */
public class ShiroSession extends DefaultWebSessionManager &#123;
    /**
     * 定义的请求头中使用的标记key,用来传递 token
     */
    private static final String AUTH_TOKEN = "authToken";
    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

    public ShiroSession() &#123;
        super();
        //设置 shiro session 失效时间,默认为30分钟,这里现在设置为35分钟
        setGlobalSessionTimeout(MILLIS_PER_MINUTE * 35);
    &#125;

    /**
     * 获取sessionId,原本是根据sessionKey来获取一个sessionId
     * 重写的部分多了一个把获取到的token设置到request的部分。这是因为app调用登陆接口的时候,是没有token的,登陆成功后,产生了token,我们把它放到request中,返回结
     * 果给客户端的时候,把它从request中取出来,并且传递给客户端,客户端每次带着这个token过来,就相当于是浏览器的cookie的作用,也就能维护会话了
     * @param request ServletRequest
     * @param response ServletResponse
     * @return Serializable
     */
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) &#123;
        //获取请求头中的 AUTH_TOKEN 的值,如果请求头中有 AUTH_TOKEN 则其值为sessionId。shiro就是通过sessionId 来控制的
        String sessionId = WebUtils.toHttp(request).getHeader(AUTH_TOKEN);

        if (StringUtils.isEmpty(sessionId))&#123;
            //如果没有携带id参数则按照父类的方式在cookie进行获取sessionId
            return super.getSessionId(request, response);

        &#125; else &#123;
            //请求头中如果有 authToken, 则其值为sessionId
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            //sessionId
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return sessionId;
        &#125;
    &#125;
&#125;

shiro认证是通过SessionId来进行判断是否认证.

在用户登录成功时获取SecurityUtils的Session ID(不是值)用作autoToken:

            Subject Usersubject = SecurityUtils.getSubject();
            // shiro的sessionID
            String authToken = (String) Usersubject.getSession().getId();

这个Session管理类是用作跨域访问时要求 前台request请求头部传递一个 token (内容为SessionId) 来认证其是否 认证通过.

Controller类

@RestController
@Slf4j
public class LoginController &#123;

    @GetMapping("/login")
    public String login(User user) &#123;
        if (StringUtils.isEmpty(user.getUserName()) || StringUtils.isEmpty(user.getPassword())) &#123;
            return "请输入用户名和密码!";
        &#125;
        //用户认证信息
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
                user.getUserName(),
                user.getPassword()
        );
        try &#123;
            //进行验证,这里可以捕获异常,然后返回对应信息
            subject.login(usernamePasswordToken);
//            subject.checkRole("admin");
//            subject.checkPermissions("query", "add");
        &#125; catch (UnknownAccountException e) &#123;
            log.error("用户名不存在!", e);
            return "用户名不存在!";
        &#125; catch (AuthenticationException e) &#123;
            log.error("账号或密码错误!", e);
            return "账号或密码错误!";
        &#125; catch (AuthorizationException e) &#123;
            log.error("没有权限!", e);
            return "没有权限";
        &#125;
        return "login success";
    &#125;
    
    ...
&#125;

1.用 SecurityUtils.getSubject()获取Subject类。

2.将用户输入进去的账户密码信息封装入UsernamePasswordToken类。

3.使用Subject类的login方法判断登录结果,并捕捉相关错误异常。

登录错误异常

  • ​ UnknownAccountException: 用户名不存在

  • ​ AuthenticationException: 账户或者密码错误

  • ​ AuthorizationException: 没有权限

  • ​ Account Exception : 账号异常

    • ConcurrentAccessException: 并发访问异常(多个用户同时登录时抛出)
    • UnknownAccountException:未知的账号
    • ExcessiveAttemptsException: 认证次数超过限制
    • DisabledAccountException: 禁用的账号
    • LockedAccountException: 账号被锁定
    • UnsupportedTokenException: 使用了不支持的Token

Shiro跨域过滤

/**
 * @description: Shiro跨域请求过滤
 * @author: Zhaotianyi
 * @time: 2021/5/18 15:56
 */

@Component
@ServletComponentScan
@WebFilter(urlPatterns = "/*",filterName = "shiroLoginFilter")
public class ShiroLoginFilter  implements Filter &#123;

    private FilterConfig config = null;
    @Override
    public void init(FilterConfig config) throws ServletException &#123;
        this.config = config;
    &#125;
    @Override
    public void destroy() &#123;
        this.config = null;
    &#125;
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException &#123;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        // 允许哪些Origin发起跨域请求,nginx下正常
        // response.setHeader( "Access-Control-Allow-Origin", config.getInitParameter( "AccessControlAllowOrigin" ) );
        response.setHeader( "Access-Control-Allow-Origin", "*" );
        // 允许请求的方法
        response.setHeader( "Access-Control-Allow-Methods", "HEAD,POST,GET,OPTIONS,DELETE,PUT" );
        // 多少秒内,不需要再发送预检验请求,可以缓存该结果
        response.setHeader( "Access-Control-Max-Age", "3600" );
        // 表明它允许跨域请求包含xxx头
        response.setHeader( "Access-Control-Allow-Headers", "*" );
        //是否允许浏览器携带用户身份信息(cookie)
        response.setHeader( "Access-Control-Allow-Credentials", "true" );
        // response.setHeader( "Access-Control-Expose-Headers", "*" );
        if (request.getMethod().equals( "OPTIONS" )) &#123;
            response.setStatus( 200 );
            return;
        &#125;
        filterChain.doFilter( servletRequest, response );
    &#125;
&#125;

Shiro+Thymeleaf页面整合

Maven依赖:

<!-- https://mvnrepository.com/artifact/com.github.theborakompanioni/thymeleaf-extras-shiro -->
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

Themeleaf页面头部加入 xmlns:shiro=”http://www.pollix.at/thymeleaf/shiro" 开启代码提示。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
      ... 
</html>

常用标签:

The has Permission tag

shiro:hasPermission=”xxx” 判断当前用户是否拥有xxx权限

<div shiro:hasPermission="user:set"></div>
The authenticated tag

authenticated=“” 已经用户得到认证

<a shiro:authenticated="" href="updateAccount.html">Update your contact information</a>
The hasRole tag

shiro:hasRole=”xxx” 判断当前用户为xxx权限

<a shiro:hasRole="administrator" href="admin.html">Administer the system</a>

权限、角色访问控制

方法一:直接在页面控制(以Thymeleaf为例)

    <!--拥有user:add权限的任何人才能看见-->
    <div shiro:hasPermission="user:add:*">
        <a th:href="@&#123;/user/add&#125;">Add</a>
    </div>
    <!--拥有admin角色才能看见-->
    <div shiro:hasRole="admin">
        <a th:href="@&#123;/user/update&#125;">Update</a>
    </div>

方法二:Controller代码层中控制

    //获取当前用户
    Subject subject = SecurityUtils.getSubject();
    if (subject.hasRole("admin")) &#123;
        System.out.println("添加成功!");
    &#125;else&#123;
        System.out.println("添加失败!");

方法三:代码注释控制

    @RequestMapping("/user/add")
    @RequiresRoles("admin") //判断角色
     @RequiresPermissions("user:add:*") //判断权限
    public String add() &#123;
        return "user/add";
    &#125;

Ehcache缓存持久化

Shiro支持很多第三方缓存工具。官方提供了shiro-ehcache,实现了把EHCache当做Shiro的缓存工具的解决方案。其中最好用的一个功能是就是缓存认证执行的Realm方法,减少对数据库的访问。

<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>2.10.2</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.4.2</version>
</dependency>

shiro-ehcache是Shiro官方与Ehcache进行对接的依赖包。

我们只需要在其ShiroConfig配置类中进行增加其Ehcache功能即可:

...
@Bean
public DefaultWebSecurityManager securityManager() &#123;
    DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
    ...
    manager.setCacheManager(ehCacheManager());
    return manager;
&#125;

@Bean
public EhCacheManager ehCacheManager()&#123;
    EhCacheManager ehCacheManager = new EhCacheManager();
    InputStream is = null;
    try &#123;
        is = ResourceUtils.getInputStreamForPath("classpath:ehcache/ehcache-shiro.xml");
    &#125; catch (IOException e) &#123;
        e.printStackTrace();
    &#125;
    net.sf.ehcache.CacheManager cacheManager = new net.sf.ehcache.CacheManager(is);
    ehCacheManager.setCacheManager(cacheManager);
    return ehCacheManager;
&#125;
...