后端进阶 每一步成长都想与你分享

Shiro身份认证与授权

2017-05-27
张乘辉

对于一个好的框架,那必然有易于使用和理解的API,我们从外部看shiro,即从应用程序角度来看shiro是如何来完成工作的:

shiro工作流程

从上图可看到,程序与shiro直接的交互对象是Subject,它是shiro对外交互的核心API:

  • Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
  • SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;
  • Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源也就是说shiro本身并不提供维护用户的权限,而是让开发者自己来注入。

实现身份认证与授权

准备环境

  • 加入maven依赖:
 <!-- shiro+redis缓存插件 -->
 <dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-spring</artifactId>
   <version>1.2.2</version>
 </dependency>
 <dependency>
   <groupId>org.crazycake</groupId>
   <artifactId>shiro-redis</artifactId>
   <version>2.4.2.1-RELEASE</version>
 </dependency>
  • 创建shiro配置类:
@Configuration
public class ShiroConfig {

    @Autowired
    AuthorizationService authorizationService;

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

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

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

    @Bean
    public ShiroFilterFactoryBean shirFilter(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new MShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/webmanage/login");
        // 登录成功后要跳转的连接
        shiroFilterFactoryBean.setSuccessUrl("/webmanage/index");
        shiroFilterFactoryBean.setUnauthorizedUrl("/error");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(authorizationService.loadShiroFilterChainDefinitionMap());
        return shiroFilterFactoryBean;
    }

    @Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();


        // 设置realm.
        securityManager.setRealm(authRealm());
        // 自定义缓存实现 使用redis
        securityManager.setCacheManager(cacheManager());
        // 自定义session管理 使用redis
        securityManager.setSessionManager(SessionManager());
        return securityManager;
    }

    /**
     * 这里需要设置成与PasswordEncrypter类相同的加密规则
     *
     * 在doGetAuthenticationInfo认证登陆返回SimpleAuthenticationInfo时会使用hashedCredentialsMatcher
     * 把用户填入密码加密后生成散列码与数据库对应的散列码进行对比
     *
     * HashedCredentialsMatcher会自动根据AuthenticationInfo的类型是否是SaltedAuthenticationInfo来获取credentialsSalt盐
     *
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");// 散列算法, 与注册时使用的散列算法相同
        hashedCredentialsMatcher.setHashIterations(2);// 散列次数, 与注册时使用的散列册数相同
        hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);// 生成16进制, 与注册时的生成格式相同
        return hashedCredentialsMatcher;
    }

    /**
     * 身份认证realm
     *
     * @return
     */
    @Bean
    public AuthRealm authRealm() {
        AuthRealm authRealm = new AuthRealm();
        // 设置加密算法
        authRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return authRealm;
    }


    /**
     * 配置shiro redisManager
     *
     * @return
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        redisManager.setExpire(1800);// 配置过期时间
        // redisManager.setTimeout(timeout);
        redisManager.setPassword(password);
        return redisManager;
    }

    /**
     * cacheManager 缓存 redis实现
     *
     * @return
     */
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }

    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis
     */
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }

    /**
     * shiro session的管理
     */
    public DefaultWebSessionManager SessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        sessionManager.setGlobalSessionTimeout(1800000L);
        sessionManager.setDeleteInvalidSessions(true);
        sessionManager.setSessionValidationSchedulerEnabled(true);
        return sessionManager;
    }

}

该配置类创建shiro的核心过滤器shiroFilterFactoryBean,为它设置安全管理器,并为安全管理器配置了用户自定义Realm,缓存机制,session会话以及加密机制hashedCredentialsMatcher等。

  • 登陆与登出代码:
@RequestMapping(value = "/login", method = RequestMethod.POST)
    public JSONObject login(@RequestBody ManageAdmin manageAdmin) {
        String username = manageAdmin.getUsername();
        String password = manageAdmin.getPassword();
        Subject subject = SecurityUtils.getSubject();
        MUsernamepasswordToken usernamepasswordToken = new MUsernamepasswordToken(username, password);
        usernamepasswordToken.setLoginType(MUsernamepasswordToken.LOGIN_TYPE.webside.toString());
        String errmsg = "";
        try {
            logger.info("对用户[" + username + "]进行登录验证..验证开始");
            subject.login(usernamepasswordToken);
        } catch (UnknownAccountException uae) {
            logger.info("对用户[" + username + "]进行登录验证..验证未通过,未知账户");
            errmsg = "对用户[" + username + "]进行登录验证..验证未通过,未知账户";
        } catch (IncorrectCredentialsException ice) {
            logger.info("对用户[" + username + "]进行登录验证..验证未通过,错误的凭证");
            errmsg = "对用户[" + username + "]进行登录验证..验证未通过,错误的凭证";
        } catch (LockedAccountException lae) {
            logger.info("对用户[" + username + "]进行登录验证..验证未通过,账户已锁定");
            errmsg = "对用户[" + username + "]进行登录验证..验证未通过,账户已锁定";
        } catch (ExcessiveAttemptsException eae) {
            logger.info("对用户[" + username + "]进行登录验证..验证未通过,错误次数过多");
            errmsg = "对用户[" + username + "]进行登录验证..验证未通过,错误次数过多";
        } catch (AuthenticationException ae) {
            logger.info("对用户[" + username + "]进行登录验证..验证未通过,堆栈轨迹如下");
            errmsg = "对用户[" + username + "]进行登录验证..验证未通过";
            ae.printStackTrace();
        }
        if (subject.isAuthenticated()) {
            errmsg = "用户[" + username + "]进行登录验证..验证通过";
            ManageAdmin manageAdminLogin = authorizationService.getManageAdminByUsername(username);
            subject.getSession().setAttribute("admin", JsonUtil.Object2JsonStr(manageAdminLogin));
            return new RestResultBuilder(0, errmsg).build();
        } else {
            usernamepasswordToken.clear();
            return new RestResultBuilder(304, errmsg).build();
        }
    }
@RequestMapping(value = "/logout", method = RequestMethod.GET)
    public JSONObject logout() {
        Subject subject = SecurityUtils.getSubject();
        if (!subject.isAuthenticated()) {
            return new RestResultBuilder(WebGatewayErrcodeConstant.logoutFail, WebGatewayErrcodeConstant.logoutFailStr).build();
        } else {
            subject.logout(); // session 会销毁,在SessionListener监听session销毁,清理权限缓存
            return new RestResultBuilder(0).build();
        }
    }

从上面代码得出:

通过shiro工具类SubjectUtils的getSubject()获取Subject类,通过Subject类的login()实现用户的登陆,如果身份认证失败就会抛出对应的异常。

身份认证

身份认证流程

  • Subject类的login()会自动委托给SecurityManager.login();
  • SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator进行身份验证;
  • Authenticator是真正认证身份的认证者,Shiro API中核心的身份认证入口点,此处可以自定义插入自己的实现;
  • 如果有多个Realm域,Authenticator还会委托给AuthenticationStrategy进行多身份认证;
  • Authenticator会把响应的token传入自定义的Realm域,并从Realm域获取身份验证信息。

编写身份验证Realm代码:

public class AuthRealm extends AuthorizingRealm {

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

    @Autowired
    private AuthorizationService authorizationService;

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        MUsernamepasswordToken token = (MUsernamepasswordToken) authenticationToken;
        String username = token.getUsername();
        logger.info("验证当前Subject时获取到token类型为:" + token.getLoginType());

        ManageAdmin manageAdmin = authorizationService.getManageAdminByUsername(username);
        if (manageAdmin == null) {
            throw new UnknownAccountException();
        }

        return new SimpleAuthenticationInfo(manageAdmin.getUsername(), manageAdmin.getPassword(),
                ByteSource.Util.bytes(manageAdmin.getSalt()), getName());
    }
}

SimpleAuthenticationInfo会默认将数据库查出来的密码与用户输入的密码对比,如果相同则通过,否则抛出异常。

授权

授权,也叫访问控制,即在应用中控制谁能访问哪些资源。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)。

主体:指使用Subject登陆的用户,只有经过shiro授权才能登陆和访问相关资源。

资源:在系统中能够访问的东西,用户也只能经过授权后才能访问。

权限:指允许在系统中进行CRUD某个资源的权力,比如访问某个页面,查看某个文档,修改那些设置,删除某些数据等操作。

角色:可以理解为权限的集合,比如超级管理员可以拥有系统的全部权限,普通用户只拥有系统的一部分权限,而如果用户拥有多个角色,则用户的权限为多个角色拥有的权限的集合。

  • 自定义Realm:
public class AuthRealm extends AuthorizingRealm {

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

    @Autowired
    private AuthorizationService authorizationService;

    /**
     * 权限认证,为当前登录的Subject授予角色和权限
     * <p>
     * 方法的调用时机为需授权资源被访问时
     * 若每次访问需授权资源时都会执行方法中的逻辑,这表明并未启用AuthorizationCache
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获取当前登录输入的用户名,等价于(String) principalCollection.fromRealm(getName()).iterator().next();
        String username = (String) super.getAvailablePrincipal(principalCollection);

        //权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission)
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        ManageAdmin manageAdmin = authorizationService.getManageAdminByUsername(username);

        // 添加管理员角色列表
        info.addRoles(manageAdmin.getRoles());
        // 添加管理员权限列表
        info.addStringPermissions(manageAdmin.getPermissions());

        return info;
    }

}
  • 为系统配置权限:
	 /**
     * 加载shiroFilter权限控制规则(从数据库读取然后配置)
     */
    public Map<String, String> loadShiroFilterChainDefinitionMap() {

        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

        filterChainDefinitionMap.put("/webmanage/index", "authc");// 首页
        filterChainDefinitionMap.put("/webmanage/login", "anon");// 登陆页面

        /** 商户模块 */
        filterChainDefinitionMap.put("/shop/api/shop/edit", "roles[super_admin]");// 商户修改
        filterChainDefinitionMap.put("/shop/api/shop/shopStatus", "roles[super_admin]");// 商户状态修改
        filterChainDefinitionMap.put("/shop/api/shop/add", "roles[super_admin, common_admin]");// 添加商户
        filterChainDefinitionMap.put("/api/manage/manageAdmin", "roles[super_admin]");// 添加管理员
        /** 设备模块 */
        filterChainDefinitionMap.put("/device/api/device/add", "roles[super_admin, common_admin]");// 添加设备
        filterChainDefinitionMap.put("/device/api/device/edit", "roles[super_admin]");// 修改设备
        filterChainDefinitionMap.put("/device/api/device/bind", "roles[super_admin]");// 绑定设备
        filterChainDefinitionMap.put("/device/api/device/unbind", "roles[super_admin]");// 绑定设备
        filterChainDefinitionMap.put("/device/api/device/disabledDevice/**", "roles[super_admin]");// 启用禁用设备
        /** 会员模块 */
        filterChainDefinitionMap.put("/member/member/disabledMember/**", "roles[super_admin]");// 会员启用禁用

        filterChainDefinitionMap.put("/**", "anon");// 默认

        return filterChainDefinitionMap;
    }
 }

默认过滤器(10个)

anon – org.apache.shiro.web.filter.authc.AnonymousFilter authc – org.apache.shiro.web.filter.authc.FormAuthenticationFilter authcBasic – org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter perms – org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter port – org.apache.shiro.web.filter.authz.PortFilter rest – org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter roles – org.apache.shiro.web.filter.authz.RolesAuthorizationFilter ssl – org.apache.shiro.web.filter.authz.SslFilter user – org.apache.shiro.web.filter.authc.UserFilter logout – org.apache.shiro.web.filter.authc.LogoutFilter

anon:例子/admins/** =anon 没有参数,表示可以匿名使用。 authc:例如/admins/user/** =authc表示需要认证(登录)才能使用,没有参数

roles:例子/admins/user/** =roles[admin],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles[“admin,guest”],每个参数通过才算通过,相当于hasAllRoles()方法。

perms:例子/admins/user/** =perms[user:add:],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms[“user:add:,user:modify:*”],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。

rest:例子/admins/user/** =rest[user],根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。

port:例子/admins/user/** =port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。 authcBasic:例如/admins/user/** =authcBasic没有参数表示httpBasic认证 ssl:例子/admins/user/** =ssl没有参数,表示安全的url请求,协议为https user:例如/admins/user/**=user没有参数表示必须存在用户,当登入操作时不做检查

这个方法已经在shiro配置类中注入shiro核心过滤器时已经调用。

授权流程

第三步骤,当身份验证通过且调用了Realm类的AuthorizationInfo方法之后,该用户就拥有了指定的权限,Authorizer会判断Realm的角色/权限是否和传入的匹配。

总结

通过SubjectUtils获取subject实例,并调用login()方法开始shiro身份验证,login()方法会委托给SecurityManager安全管理器的login()方法,之后SecurityManager会调用自定义Realm类的doGetAuthenticationInfo()方法进行身份验证,当身份验证不通过则抛出异常,当验证逻辑通过后,会把用户名和密码放到SimpleAuthenticationInfo中,Shiro会自动根据用户输入的密码和查询到的密码进行匹配,如果密码匹配,执行doGetAuthorizationInfo()进行相应的权限验证。doGetAuthorizationInfo()方法的处理逻辑也比较简单,根据用户名获取到他所拥有的角色以及权限,然后赋值到SimpleAuthorizationInfo对象中即可,Shiro就会按照我们配置的XX角色对应XX权限来进行判断。接下来为系统资源配置指定权限树。这样,身份验证与授权流程基本完成。

参考原文地址:

http://jinnianshilongnian.iteye.com/blog/2019547

http://jinnianshilongnian.iteye.com/blog/2020017


更多精彩文章请关注作者维护的公众号「后端进阶」,这是一个专注后端相关技术的公众号。 关注公众号并回复「后端」免费领取后端相关电子书籍。 欢迎分享,转载请保留出处。

微信公众号「后端进阶」

Content