组件整合之权限框架Shiro

shiro简介

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

Apache Shiro相当简单,对比Spring Security,可能没有Spring Security做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的Shiro就足够了。不用去纠结到底哪个比较好,适合自己的最好。

shiro可以做什么

  • 验证用户身份
  • 用户访问控制,比如用户是否被赋予了某个角色;是否允许访问某些资源
  • 在任何环境都可以使用Session API,即使不是WEB项目或没有EJB容器
  • 事件响应(在身份验证,访问控制期间,或是session生命周期中)
  • 集成多种用户信息数据源
  • SSO-单点登陆
  • Remember Me,记住我
  • Shiro尝试在任何应用环境下实现这些功能,而不依赖其他框架、容器或应用服务器。

shiro的整体架构

(1)上面标记为1的是shiro的主体部分subject,可以理解为当前的操作用户。

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

(2)Security Manager为shiro的核心,shiro是通过Security Manager来提供安全服务的。

Security Manager管理着Session Manager、Cache Manager等其他组件的实例:

  • Authenticator:认证器,管理我们的登录登出;
  • Authorizer:授权器,负责赋予主体subject有哪些权限;
  • Session Manager:shiro自己实现的一套session管理机制,可以不借助任何web容器的情况下使用session;
  • Session Dao:提供了session的增删改查操作;
  • Cache Manager:缓存管理器,用于缓存角色数据和权限数据;
  • Pluggable Realms:shiro与数据库/数据源之间的桥梁,shiro获取认证信息、权限数据、角色数据都是通过Realms来获取。

(3)上图标记为2的cryptography是用来做加密的,使用它可以非常方便快捷的进行数据加密。

(4)上面箭头的流程可以这样理解:主体提交请求到Security Manager,然后由Security Manager调用Authenticator去做认证,而Authenticator去获取认证数据的时候是通过Realms从数据源中来获取的,然后把从数据源中拿到的认证信息与主体提交过来的认证信息做比对。授权器Authorizer也是一样。

shiro的三个核心组件:

  • Subject:当前操作用户。并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。
  • SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
  • Realm:Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。 从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。 

对于我们而言,最简单的一个Shiro应用:

  • 应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;
  • 我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。

注意:Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入。

shiro快速入门

pom配置

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

Java Config配置

1、配置DelegatingFilterProxy

在项目的程序入门类(继承AbstractAnnotationConfigDispatcherServletInitializer)中,重写getServletFilters方法,配置DelegatingFilterProxy。

/**
 * @Description 整个项目的程序入口
 */
public class SpringContainer extends AbstractAnnotationConfigDispatcherServletInitializer {

    /**
     * 根容器,用于获取Spring应用容器的配置文件
     *
     * @return
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        //return new Class[0];
        return new Class[]{RootConfig.class};
    }

    /**
     * Spring mvc容器,是根容器的子容器
     *
     * @return
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        //return new Class[0];
        return new Class[]{WebConfig.class};
    }

    /**
     * "/"表示由DispatcherServlet处理所有向该应用发起的请求。
     *
     * @return
     */
    @Override
    protected String[] getServletMappings() {
        //return new String[0];
        return new String[]{"/"};
    }

    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");
        characterEncodingFilter.setForceEncoding(true);

        // 必须配置DelegatingFilterProxy ,否则shiro的filter就会不起作用
        // DelegatingFilterProxy作用是自动到Spring 容器查找名字为shiroFilter(filter-name)的bean并把所有Filter的操作委托给它。
        // DelegatingFilterProxy 实际上是 Filter 的一个代理对象. 默认情况下, Spring 会到 IOC 容器中查找和<filter-name>
        // 对应的 filter bean. 也可以通过 targetBeanName 的初始化参数来配置 filter bean 的 id。
        DelegatingFilterProxy delegatingFilterProxy = new DelegatingFilterProxy();
        delegatingFilterProxy.setTargetFilterLifecycle(true);
        delegatingFilterProxy.setTargetBeanName("shiroFilterFactoryBean");
        return new Filter[]{characterEncodingFilter, delegatingFilterProxy};
    }
}

2、shiro配置 

(1)基础配置

import com.spring.shiro.realm.CustomerRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Configuration
public class ShiroConfig {

    /**
     * 开启shiro权限注解支持
     *
     * @param webSecurityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager webSecurityManager) {
        AuthorizationAttributeSourceAdvisor sourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        sourceAdvisor.setSecurityManager(webSecurityManager);
        return sourceAdvisor;
    }

    /**
     * 工厂,该bean的名称必须与DelegatingFilterProxy中的targetBeanName属性一致。不设置targetBeanName属性,默认是shiroFilter。
     * 若不一致,则会抛出:NoSuchBeanDefinitionException. 因为 Shiro 会来 IOC 容器中查找和 <filter-name> 名字对应的 filter bean。
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
        filterFactoryBean.setSecurityManager(securityManager);

        Map<String, String> map = new HashMap();
        //放行
        map.put("/shiro/home", "anon");
        map.put("/shiro/login", "anon");
        map.put("/shiro/403", "anon");
        //拦截
        map.put("/**", "authc");
        //身份认证失败
        filterFactoryBean.setLoginUrl("/shiro/home");
        //没有授权跳转的页面
        filterFactoryBean.setUnauthorizedUrl("/shiro/403");
        //自上而下的顺序
        filterFactoryBean.setFilterChainDefinitionMap(map);
        return filterFactoryBean;
    }

    /**
     * 配置域 , CustomerRealm是自定义的域
     * @return
     */
    @Bean
    public Realm realm() {
        CustomerRealm customerRealm = new CustomerRealm();
        customerRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return customerRealm;
    }

    /**
     * 认证匹配器配置
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //加密方式
        // hashedCredentialsMatcher.setHashAlgorithmName("SHA-512");
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        //加密次数
        hashedCredentialsMatcher.setHashIterations(2);
        //存储散列后的密码是否为16进制
        // hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
        return hashedCredentialsMatcher;
    }

    /**
     * 创建安全管理器
     * 只要我们注入了Real(1个或多个),这里会自动收集
     * @return
     */
    @Bean
    public DefaultWebSecurityManager securityManager(List<Realm> realms) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realms);
        return securityManager;
    }
}

(2)自定义域,重写授权和认证方法

import com.spring.shiro.entity.SysPermission;
import com.spring.shiro.entity.SysRole;
import com.spring.shiro.entity.SysUser;
import com.spring.shiro.service.SysUserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.util.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * @author linhongwei
 */
public class CustomerRealm extends AuthorizingRealm {

    /**
     * CustomerRealm 通过@bean加入了spring容器,所有能够自动注入
     */
    @Autowired
    private SysUserService sysUserService;

    /**
     * 授权
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String principal = (String) principalCollection.getPrimaryPrincipal();
        System.out.println("principal:" + principal);
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        //角色列表
        HashSet roleSet = new HashSet();
        //权限列表
        Set<String> permissionSet = new HashSet();
        //查询用户拥有的角色
        List<SysRole> roleList = sysUserService.getRoleByUserName(principal);
        if (!CollectionUtils.isEmpty(roleList)) {
            roleList.forEach(role -> {
                roleSet.add(role.getRoleName());
                List<SysPermission> permissionList = sysUserService.getPermissionByRoleName(role.getRoleName());
                if (!CollectionUtils.isEmpty(permissionList)) {
                    permissionList.forEach(permission -> {
                        permissionSet.add(permission.getPermissionTag());
                    });
                }
            });
        }
        //增加角色
        authorizationInfo.addRoles(roleSet);
        //增加权限
        authorizationInfo.addStringPermissions(permissionSet);
        return authorizationInfo;
    }

    /**
     * 认证
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String principal = (String) authenticationToken.getPrincipal();
        String credentials = new String((char[]) authenticationToken.getCredentials());
        System.out.println(principal + " " + credentials);
        //查询用户
        SysUser user = sysUserService.getUserByName(principal);
        if (user != null) {
            //这里注意第一个和二个参数都是被定义为Object类型的,尤其第一个参数表示是所有的认证信息
            //如果想使用标签:<shiro:principal property="username"/>
            //第一个参数必须设置一个对象类型,如果仅仅传递一个用户名user.getUsername(),页面会报错的.
            //property指定的属性就是第一个参数对象中的属性
            SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), this.getName());
            return authenticationInfo;
        }
        return null;
    }
}

(3)service、dao、实体

SysUserService.java

@Service
public class SysUserService {

    @Autowired
    private SysUserDao userDao;

    public SysUser getUserByName(String username) {
        return userDao.getUserByName(username);
    }

    public List<SysRole> getRoleByUserName(String username) {
        return userDao.getRoleByUserName(username);
    }

    public List<SysPermission> getPermissionByRoleName(String roleName) {
        return userDao.getPermissionByRoleName(roleName);
    }
}

SysUserDao.java

@Repository
public class SysUserDao {

    /**
     * 用户
     */
    private static Map<String, SysUser> userMap = new HashMap();

    /**
     * 用户-角色
     */
    private static Map<String, List<SysRole>> roleMap = new HashMap();

    /**
     * 角色-权限
     */
    private static Map<String, List<SysPermission>> rolePermissionMap = new HashMap<>();

    static {
        SysUser rootUser = new SysUser();
        rootUser.setId(1L);
        rootUser.setUsername("admin");

        //shiro的HashedCredentialsMatcher认证匹配器制定了属于自己的加解密逻辑
        //为了解密成功(即加解密对应才能解密成功),我们加密保存进数据库的数据也要用shiro自己的加密工具
        String salt = Md5Utils.generateSalt();
        rootUser.setPassword(Md5Utils.generatePwdEncrypt("admin", salt));
        rootUser.setSalt(salt);
        userMap.put("admin", rootUser);

        SysUser guestUser = new SysUser();
        guestUser.setId(2L);
        guestUser.setUsername("guest");
        guestUser.setPassword(Md5Utils.generatePwdEncrypt("guest", salt));
        guestUser.setSalt(salt);
        userMap.put("guest", guestUser);

        SysUser harveyUser = new SysUser();
        harveyUser.setId(3L);
        harveyUser.setUsername("harvey");
        harveyUser.setPassword(Md5Utils.generatePwdEncrypt("harvey", salt));
        harveyUser.setSalt(salt);
        userMap.put("harvey", harveyUser);
    }

    static {
        SysRole adminRole = new SysRole();
        adminRole.setId(1L);
        adminRole.setRoleName("admin");

        SysRole guestRole = new SysRole();
        guestRole.setId(2L);
        guestRole.setRoleName("guest");

        SysRole vipRole = new SysRole();
        vipRole.setId(3L);
        vipRole.setRoleName("vip");

        SysRole developerRole = new SysRole();
        developerRole.setId(4L);
        developerRole.setRoleName("developer");

        roleMap.put("admin", Arrays.asList(adminRole));
        roleMap.put("guest", Arrays.asList(guestRole));
        roleMap.put("harvey", Arrays.asList(vipRole));
    }

    static {
        SysPermission userAddPermission = new SysPermission();
        userAddPermission.setId(1L);
        userAddPermission.setPermissionDesc("添加用户");
        userAddPermission.setPermissionTag("user:add");

        SysPermission userUpdatePermission = new SysPermission();
        userUpdatePermission.setId(2L);
        userUpdatePermission.setPermissionDesc("修改用户");
        userUpdatePermission.setPermissionTag("user:update");

        SysPermission userDeletePermission = new SysPermission();
        userDeletePermission.setId(3L);
        userDeletePermission.setPermissionDesc("删除用户");
        userDeletePermission.setPermissionTag("user:del");

        SysPermission userViewPermission = new SysPermission();
        userViewPermission.setId(4L);
        userViewPermission.setPermissionDesc("查看用户");
        userViewPermission.setPermissionTag("user:view");
        //admin角色拥有的权限
        rolePermissionMap.put("admin", Arrays.asList(userAddPermission, userUpdatePermission, userDeletePermission, userViewPermission));
        //vip角色拥有的权限
        rolePermissionMap.put("vip", Arrays.asList(userAddPermission, userViewPermission));
        //guest角色拥有的权限
        rolePermissionMap.put("guest", Arrays.asList(userViewPermission));
        //developer角色拥有的权限
        rolePermissionMap.put("developer", Arrays.asList(userAddPermission, userUpdatePermission, userViewPermission));
    }

    /**
     * 获取用户信息
     *
     * @param username
     * @return
     */
    public SysUser getUserByName(String username) {
        return userMap.get(username);
    }

    /**
     * 获取角色列表
     *
     * @param username
     * @return
     */
    public List<SysRole> getRoleByUserName(String username) {
        return roleMap.get(username);
    }

    /**
     * 获取角色对应的权限列表
     *
     * @param roleName
     * @return
     */
    public List<SysPermission> getPermissionByRoleName(String roleName) {
        return rolePermissionMap.get(roleName);
    }
}

SysUser.java

@Setter
@Getter
@ToString
public class SysUser implements Serializable {
    private static final long serialVersionUID = 7824957368317925301L;
    /**
     * 用户id
     */
    private Long id;
    /**
     * 用户名称
     */
    private String username;
    /**
     * 用户密码
     */
    private String password;
    /**
     * 加密的盐
     */
    private String salt;
}

SysRole.java

@Setter
@Getter
@ToString
public class SysRole implements Serializable {

    /**
     * 角色id
     */
    private Long id;
    /**
     * 角色名称
     */
    private String roleName;

}

SysPermission.java

@Setter
@Getter
@ToString
public class SysPermission implements Serializable {
    private static final long serialVersionUID = -8488028606958098898L;
    /**
     * 权限ID
     */
    private Long id;
    /**
     * 权限描述
     */
    private String permissionDesc;
    /**
     * 权限标识
     */
    private String permissionTag;

}

(4)涉及到的其他类

ShiroController.java

@Controller
@RequestMapping("/shiro")
public class ShiroController {

    @Autowired
    private SysUserService userService;

    /**
     * 登录页
     *
     * @return
     */
    @GetMapping("home")
    public ModelAndView home() {
        ModelAndView homeView = new ModelAndView();
        homeView.setViewName("login");
        return homeView;
    }

    /**
     * 登录动作
     *
     * @return
     */
    @PostMapping("login")
    public String login(HttpServletRequest request) {
        //登录逻辑
        HttpSession session = request.getSession();

        //账号 and 密码
        String username = request.getParameter("username");
        String password = request.getParameter("password");

        //记住我
        String remember = request.getParameter("check");

        //得到 subject 对象
        Subject curUser = SecurityUtils.getSubject();

        UsernamePasswordToken upt = null;

        //是否认证过
        if (!curUser.isAuthenticated()) {
            try {
                if (remember != null) {
                    //记住我
                    upt.setRememberMe(true);
                }
                upt = new UsernamePasswordToken(username, password);
                //进行认证
                curUser.login(upt);
            } catch (UnknownAccountException e) {
                request.setAttribute("msg", e.getMessage());
                return "login";
            } catch (LockedAccountException e) {
                request.setAttribute("msg", e.getMessage());
                return "login";
            } catch (AuthenticationException e) {
                request.setAttribute("msg", "账号或密码错误,请检查后重试");
                return "login";
            } catch (Exception e) {
                request.setAttribute("msg", "未知错误,请联系管理员");
                return "login";
            }
        }

        //通过id去数据表查数据
        SysUser user = userService.getUserByName(username);
        //如果登录成功
        session.setAttribute("userVo", user);
        return "redirect:index";
    }

    /**
     * 首页(登录成功才能看到)
     *
     * @return
     */
    @GetMapping("index")
    public ModelAndView index() {
        return new ModelAndView("index");
    }

    /**
     * 403拒绝
     *
     * @return
     */
    @GetMapping("403")
    public ModelAndView access403() {
        return new ModelAndView("redirect:403");
    }

    /**
     * 退出登录
     *
     * @return
     */
    @PostMapping("logout")
    public ModelAndView logout() {
        //退出逻辑
        Subject subject = SecurityUtils.getSubject();
        if (subject.isAuthenticated()) {
            subject.logout(); // session 会销毁,在SessionListener监听session销毁,清理权限缓存
        }
        System.out.println("用户" + subject.getPrincipal() + "退出成功");
        return new ModelAndView("login");
    }
}

加密工具 ShiroUtils.java

import org.apache.commons.lang3.RandomStringUtils;
import org.apache.shiro.crypto.hash.SimpleHash;

public class ShiroUtils {
    
    /**
     * PWD_SALT_LENGTH: 密码加密盐值长度
     */
    public static final int PWD_SALT_LENGTH = 6;
    /**
     * PWD_ALGORITHM_NAME: 密码加密算法
     */
    public static final String PWD_ALGORITHM_NAME = "SHA-512";

    /**
     * PWD_ALGORITHM_NAME: 密码加密次数
     */
    public static final int PWD_HASH_ITERATIONS = 2;

    /**
     * 生成密码<br/>
     *
     * @param pwd
     * @param salt
     * @return
     */
    public static String generatePwdEncrypt(String pwd, String salt) {
        SimpleHash hash =
                new SimpleHash(PWD_ALGORITHM_NAME, pwd, salt, PWD_HASH_ITERATIONS);
        return hash.toString();
    }

    /**
     * 生成盐值<br/>
     *
     * @return
     */
    public static String generateSalt() {
        return RandomStringUtils.randomAlphabetic(PWD_SALT_LENGTH);
    }
}
    

(5)前端页面

login.jsp

<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<%
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>登录界面</title>
</head>
<body>
<center>
    <h1 style="color:red">登录</h1>
    <form id="indexform" name="indexForm" action="<%=basePath%>shiro/login" method="post">
        <table border="0">
            <tr>
                <td>账号:</td>
                <td><input type="text" name="username"></td>
            </tr>
            <tr>
                <td>密码:</td>
                <td><input type="password" name="password">
                </td>
            </tr>
        </table>
        <%--显示出错误信息--%>
        <div style="color: red;font-size: 13px;">${msg}</div>
        <br>
        <input type="submit" value="登录" style="color:#BC8F8F">
    </form>
</center>
</body>
</html>

index.jsp

<%@ page contentType="text/html;charset=UTF-8" %>
<%--引入shiro标签--%>
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<%
    String path = request.getContextPath();
    String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
<html>
<head>
    <title>首页</title>
</head>
<body>
<center>

    <!--验证当前用户是否拥有指定权限。  -->
    <p><a shiro:hasPermission="user:add" href="#">add用户</a><!-- 拥有权限 --></p>

    <!--与hasPermission标签逻辑相反,当前用户没有制定权限时,验证通过。-->
    <p shiro:lacksPermission="user:del"> 没有权限 </p>

    <!--验证当前用户是否拥有以下所有权限。-->
    <p shiro:hasAllPermissions="user:view, user:add"> 权限与判断 </p>

    <!--验证当前用户是否拥有以下任意一个权限。-->
    <p shiro:hasAnyPermissions="user:view, user:del"> 权限或判断 </p>

    <!--验证当前用户是否属于该角色。-->
    <a shiro:hasRole="admin" href="#">拥有该角色</a>

    <!--与hasRole标签逻辑相反,当用户不属于该角色时验证通过。-->
    <p shiro:lacksRole="developer"> 没有该角色 </p>

    <!--验证当前用户是否属于以下所有角色。-->
    <p shiro:hasAllRoles="developer, admin"> 角色与判断 </p>

    <!--验证当前用户是否属于以下任意一个角色。-->
    <p shiro:hasAnyRoles="admin, vip, developer"> 角色或判断 </p>

    <!--验证当前用户是否为“访客”,即未认证(包含未记住)的用户。-->
    <p shiro:guest="">访客 未认证</p>

    <!--认证通过或已记住的用户-->
    <p shiro:user> 认证通过或已记住的用户: <a href="javascript:void(0);" onclick="javascript:post('<%=basePath%>shiro/logout', {});">退出登录</a></p>

    <!--已认证通过的用户。不包含已记住的用户,这是与user标签的区别所在。-->
    <p shiro:authenticated><span shiro:principal=""></span></p>

    <!--输出当前用户信息,通常为登录帐号信息-->
    <p><shiro:principal property="username"/> 登录成功<br></p>

    <!--未认证通过用户,与authenticated标签相对应。-->
    <!--与guest标签的区别是,该标签包含已记住用户。-->
    <p shiro:notAuthenticated> 未认证通过用户 </p>


</center>
</body>
<script type="text/javascript">
    function post(url, params) {
        var temp = document.createElement("form");
        temp.action = url;
        temp.method = "post";
        temp.style.display = "none";
        for (var x in params) {
            var opt = document.createElement("textarea");
            opt.name = x;
            opt.value = params[x];
            temp.appendChild(opt);
        }
        document.body.appendChild(temp);
        temp.submit();
        return temp;
    }
</script>
</html>

403.jsp

<%@ page contentType="text/html;charset=UTF-8"%>
<html>
<head>
    <title>403拒绝访问</title>
</head>
<body>
<div class="layui-body">
    <!-- 内容主体区域 -->
    <div style="padding: 15px;">
        <h1>非常抱歉!您没有访问这个功能的权限!</h1>
    </div>
</div>
</body>
</html>

访问:http://localhost:8888/shiro/home, 然后输入用户名和密码,点击登录按钮。

shiro的工作原理

首先说到shiro,大家都必须了解几个知识点: Filter、、InitializingBean、FactoryBean。

① 在web服务启动时,首先会加载web.xml文件,对Filter、Servlety以及一些参数进行初始化,Filter会先执行init(FilterConfig config)方法进行初始化,其中的doFilter()方法会对其在web.xml文件中配置的路径进行过滤。在其过滤的过程中,可以根据自己的需求进行一些请求处理,整个web服务启动和访问过程中,init()方法只会执行一次,而doFilter()会执行多次,destroy()方法也是执行一次,在Filter实例销毁的时候执行。

② BeanPostProcessor这个接口类在Spring中会作为后置处理器的表示,如果一个类实现了这个接口类,那么在Spring容器的加载过程中,Spring会自动识别实现了PostBeanProcessor这个接口类的实现类,并且或将这个实现类注册为后置处理器。PostBeanProcessor这个接口类中有两个抽象方法 :postProcessBeforeInitialization(Object bean, String beanName) 方法会在Spring容器中Bean被实例化之前执行,而Object postProcessAfterInitialization(Object bean, String beanName)方法会在Spring容器中Bean被实例化之后执行,通常都是在postProcessBeforeInitialization(Object bean, String beanName)方法中针对Bean的实例化做一些初始化操作。

③ InitializingBean 在Spring中也是一个接口类,凡是实现了这个接口类,都需要实现它的接口方法afterPropertiesSet(),并且实现类作为一个单例的Bean被实例化后会先用实现的afterPropertiesSet()方法,因此也可以在通过实现 InitializingBean这个接口类来达到一些Bean被实例化之后的一些初始化操作,这个实现逻辑根据自己的需求而定。

④ FactoryBean 是Spring支持的工厂接口类,如果实现对FactoryBean 进行实现的话,那么在Spring容器加载的过程中,会针对该接口类的实现类进行判断,如果实现了该接口类,通常情况是通过实现方法getObject()方法来进行实例化,以及后续的一些操作。

1、Shiro的过滤器配置

假设在web.xml中的配置如下:

<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <init-param>
        <param-name>targetFilterLifecycle</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

进入DelegatingFilterProxy 这个过滤器类中,我们可以看到DelegatingFilterProxy类继承了GenericFilterBean。

GenericFilterBean 这个类是抽象类,并且该类实现了Filter接口类和 InitializingBean接口类,很明显,第一步就是直接找到实现的 init(FilterConfig config)方法,但是这个方法的实现是在GenericFilterBean中,在该方法中对web.xml文件中配置的Filter进行过滤,将各个Filter配置的参数存储到容器中每个Filter对应的FilterConfig中,方便后面获取Filter的相关信息,接着这个init()方法继续看,不难发现其中调用了initFilterBean()方法 ,然后此时调用的initFilterBean()是DelegatingFilterProxy实现的方法,再进入DelegatingFilterProxy中的initFilterBean()方法中。

根据initFilterBean()的实现中可以发现其参数targetBeanName只会被初始化一次,一旦不为null,就不会被重新赋值。 getFilterName()返回的就是当前加载的Filter过滤器的名称,也就是web.xml文件中配置的 ShiroFilter,并且会通过web环境对 delegate 进行初始化。

接着再看GenericFilterBean中针对 InitializingBean 的实现,会发现,在afterPropertiesSet()方法中调用的是DelegatingFilterProxy类中的initFilterBean()方法。

2、接下来从Shiro与Spring整合的配置来进行分析

LifeCycleBeanPostProcessor 这个类管理Shiro中各个Bean的生命周期。

LifeCycleBeanPostProcessor这个类间接实现了BeanPostProcessor这个接口类,因此Spring容器加载的时候也会自动把 LifeCycleBeanPostProcessor这个类注册为后置处理器,看看LifeCycleBeanPostProcessor中针对 BeanPostProcessor接口类的实现:

由上图可以知道在每个Bean实例化之前都会判断一下 是不是 Initializable接口类型的实例,如果是,就调用其实现的init()方法,通过源码发现:AuthorizingRealm 类 实现了Initializable接口类(我们自定义的域都需要继承AuthorizingRealm ),因此就能知道这里主要是针对自定义的Realm类进行过滤的,一旦满足,则会调用其init()方法:

init()方法的实现中主要是针对缓存的获取。

接着我们再看 DefaultWebSecurityManager 的配置:

看到这里就知道了为什么DefaultWebSecurityManager 的配置中引入MyRealm实例了,并且在一系列构造方法中都进行了各自的变量初始化。

接下来看 ShiroFilterFactoryBean的配置:

从上图可以看出我们应该直接查看ShiroFilterFactoryBean类中的getObject()方法和 postProcessBeforeInitialization(Object bean, String beanName)方法。

可以看出主要是通过调用createInstance()方法进行实例化的,但是在这个过程中调用了createFilterChainManager()方法,在createFilterChainManager()方法中进行了注册Shiro一些默认的Filter以及我们自己配置的Filter,并且对我们配置的路径访问权限也进行了全部解析。

3、到这里,相信大家对Shiro的运行流程有了一个基本的认识。最后我们来看看登录的一个认证授权过程,到底是怎么调用自定义的

LoginController.java中login()方法

从上图可以看出Security.getSubject()获取到的是一个Subject的对象,查看getSubject()方法,得知其来源 

可以看出创建Subject对象的时候传入了SecurityManager对象,而这个SecurityManager对象也就是在之前在SecurityUtils中初始化好的SecurityManager,以及Builder类中的成员变量subjectContext是通过 newSubjectContextInstance()方法进行创建并初始化的:

接着我们查看subject对象中的login()方法,会发现为什么进入的是 DelegatingSubject类中的login方法,继续查看源码:

这下就找到了进入login方法的入口:

继续查看SecurityManager类中的login()方法:

走到这里终于找到了我们自定义的存放的位置以及进行认证的地方:

登录认证通过后实际上也几乎完成了Shiro的认证,然后登录授权只是赋予菜单访问权限,这个就需要结合Shiro的标签使用。

shiro 基本API

这是shiro的基本api可以在需要的地方使用。shiro集成web通过过滤器和标签等包含了以下api和对应的组件。

1、构建SecurityManager 

Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:demo/shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);

2、用于认证数据的传输

Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();// 产生会话传输数据
session.setAttribute("传输数据项key", "传输数据值");
//UsernamePasswordToken用来将从Java应用程序通过某种方式获取到的用户名和密码绑定到一起
UsernamePasswordToken token = new UsernamePasswordToken("汤汤", "5201314");

3、登录

currentUser.login(token);

4、是否已经验证

currentUser.isAuthenticated();

5、登录当时人

currentUser.getPrincipal();

6、有没有权限

currentUser.hasRole("schwartz");

7、断言检查是否有该权限,有就继续没有抛出异常

currentUser.checkRole("user");

8、基于权限对象的实现 ,基于字符串的实现 

currentUser.isPermitted("lightsaber:weild");

9、断言权限角色授权实现,基于字符串

currentUser.checkPermission("account:open");

10、退出

currentUser.logout();

11、Shiro提供多个默认的过滤器,我们可以用这些过滤器来配置控制指定URL的权限,Shiro常见的过滤器如下:

配置缩写 过滤器名称 功能
身份认证相关的
anon AnonymousFilter 指定url可以匿名访问
authc FormAuthenticationFilter

基于表单的拦截器;如“/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址; failureKeyAttribute:登录失败后错误信息存储key(shiroLoginFailure)

authcBasic BasicHttpAuthenticationFilter Basic HTTP身份验证拦截器,主要属性: applicationName:弹出登录框显示的信息(application)
logout LogoutFilter 退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/)
user UserFilter 用户拦截器,用户已经身份验证/记住我登录的都可
授权相关的
roles RolesAuthorizationFilter 角色授权拦截器,验证用户是否拥有所有角色;主要属性: loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例“/admin/**=roles[admin]”
perms PermissionsAuthorizationFilter 权限授权拦截器,验证用户是否拥有所有权限;属性和roles一样;示例“/user/**=perms[“user:create”]”
port PortFilter 端口拦截器,主要属性:port(80):可以通过的端口;示例“/test= port[80]”,如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样
rest HttpMethodPermissionFilter

rest风格拦截器,自动根据请求方法构建权限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串;示例“/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete”权限字符串进行权限匹配(所有都得匹配,isPermittedAll)

ssl SslFilter SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口(443);其他和port拦截器一样
noSessionCreation NoSessionCreationAuthorizationFilter 需要指定

shiro标签

Shiro提供了JSP 的一套JSTL标签,用于做 JSP  页面做权限控制的。可以控制一些按钮和一些超链接,或者一些显示内容。

使用前必须先引入shiro标签:

<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<shiro:guest>
    游客访问 <a href = "login.jsp"></a>
</shiro:guest>
 
user 标签:用户已经通过认证\记住我 登录后显示响应的内容
<shiro:user>
    欢迎[<shiro:principal/>]登录 <a href = "logout">退出</a>
</shiro:user>
 
authenticated标签:用户身份验证通过,即 Subjec.login 登录成功 不是记住我登录的
<shiro:authenticted>
    用户[<shiro:principal/>] 已身份验证通过
</shiro:authenticted>
 
notAuthenticated标签:用户未进行身份验证,即没有调用Subject.login进行登录,包括"记住我"也属于未进行身份验证
<shiro:notAuthenticated>
    未身份验证(包括"记住我")
</shiro:notAuthenticated>
 
 
principal 标签:显示用户身份信息,默认调用
Subjec.getPrincipal()获取,即Primary Principal
<shiro:principal property = "username"/>
 
hasRole标签:如果当前Subject有角色将显示body体内的内容
<shiro:hashRole name = "admin">
    用户[<shiro:principal/>]拥有角色admin
</shiro:hashRole>
 
hasAnyRoles标签:如果Subject有任意一个角色(或的关系)将显示body体里的内容
<shiro:hasAnyRoles name = "admin,user">
    用户[<shiro:pricipal/>]拥有角色admin 或者 user
</shiro:hasAnyRoles>
 
lacksRole:如果当前 Subjec没有角色将显示body体内的内容
<shiro:lacksRole name = "admin">
    用户[<shiro:pricipal/>]没有角色admin
</shiro:lacksRole>
 
hashPermission:如果当前Subject有权限将显示body体内容
<shiro:hashPermission name = "user:create">
    用户[<shiro:pricipal/>] 拥有权限user:create
</shiro:hashPermission>
 
lacksPermission:如果当前Subject没有权限将显示body体内容
<shiro:lacksPermission name = "org:create">
    用户[<shiro:pricipal/>] 没有权限org:create
</shiro:lacksPermission>

shiro注解

@RequiresAuthenthentication:表示当前Subject已经通过login进行身份验证;即 Subjec.isAuthenticated()返回 true
 
@RequiresUser:表示当前Subject已经身份验证或者通过记住我登录的,
 
@RequiresGuest:表示当前Subject没有身份验证或者通过记住我登录过,即是游客身份
 
@RequiresRoles(value = {"admin","user"},logical = Logical.AND):表示当前Subject需要角色admin和user
 
@RequiresPermissions(value = {"user:delete","user:b"},logical = Logical.OR):表示当前Subject需要权限user:delete或者user:b

 

参考:

 

posted @ 2022-01-03 20:33  残城碎梦  阅读(213)  评论(0编辑  收藏  举报