简介

Shiro是Apache的一个开源安全框架,旨在简化身份验证和授权,主要用来处理身份认证,授权,企业会话管理和加密等,并且Shiro不依赖任何容器在JavaSE和JavaEE项目中都可以使用。 
相比较Spring家族的Spring Security,Shiro在保持强大功能的同时,还具有简单灵活轻量等特点,学习成本也要低很多,目前受到越来越多项目系统的应用,更多关于shiro的介绍可以自己百度,这里不偏题。

实战

以下为SpringBoot集成Shiro,实现基础登陆认证和权限管理的过程。 
一.引入依赖文件
<!-- shiro -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>

注意有需要的可以同时引入shiro-ehcache实现缓存,我这边因为后面会集成使用redis,所以就不引入了。引入shiro-ehcache也并不复杂,多加一个配置可自行百度,对于分布式部署来说,没有必要使用shiro-ehcache。

二.基础表 
这里介绍使用简单的用户+权限的表结构,需要三张表,用户表,角色表,以及关系表,建表sql如下
create table CMS_USER_INFO
(
   ID              integer(10) not null auto_increment comment '用户ID',
   USER_CODE            varchar(20) not null comment '用户编码',
   USER_NAME            varchar(64) not null comment '用户名称',
   USER_PWD             varchar(150) comment '用户密码',
   REMARK               varchar(60) comment '备注',
   CREATE_BY            varchar(20) comment '创建人',
   CREATE_DATE          datetime comment '创建时间',
   MODIFIED_BY          varchar(20) comment '修改人',
   MODIFIED_DATE        datetime comment '修改时间',
   SORTNO               integer(2) default 0 comment '排序',
   STATE                integer(2) comment '数据状态',
   primary key (ID)
);
create table CMS_ROLE_INFO
(
   ID                   integer(10) not null auto_increment comment '角色编号',
   ROLE_CODE            varchar(20) not null comment '角色编码',
   ROLE_NAME            varchar(60) not null comment '角色名称',
   CREATE_BY            varchar(20) comment '创建人',
   CREATE_DATE          datetime comment '创建时间',
   MODIFIED_BY          varchar(20) comment '修改人',
   MODIFIED_DATE        datetime comment '修改时间',
   SORTNO               integer(2) default 0 comment '排序',
   STATE                integer(2) comment '数据状态',
   primary key (ID)
);
create table CMS_USER_ROLE_R
(
   ID                   int not null auto_increment,
   USER_CODE            varchar(10),
   ROLE_CODE            varchar(10),
   CREATE_BY            varchar(20) comment '创建人',
   CREATE_DATE          datetime comment '创建时间',
   MODIFIED_BY          varchar(20) comment '修改人',
   MODIFIED_DATE        datetime comment '修改时间',
   SORTNO               integer(2) default 0 comment '排序',
   STATE                integer(2) comment '数据状态',
   primary key (ID)
);

测试数据如下

INSERT INTO `cms_user_info` VALUES (1, 'admin', '管理员', 'd0970714757783e6cf17b26fb8e2298f', '测试备注', 'admin', '2017-12-8 20:32:02', 'admin', '2017-12-8 20:32:09', 0, 1);
INSERT INTO `cms_user_info` VALUES (2, '17040406', '张三', 'd0970714757783e6cf17b26fb8e2298f', '测试备注', 'admin', '2017-12-8 20:32:02', 'admin', '2017-12-8 20:32:09', 1, 1);

INSERT INTO `cms_role_info` VALUES (1, 'admin', '系统管理员', 'admin', '2017-12-8 19:06:35', 'admin', '2017-12-8 19:06:39', 0, 1);
INSERT INTO `cms_role_info` VALUES (2, 'guest', '客人', 'admin', '2017-12-8 19:06:35', 'admin', '2017-12-8 19:06:35', 1, 1);

INSERT INTO `cms_user_role_r` VALUES (1, 'admin', 'admin', 'admin', '2017-12-8 20:19:34', 'admin', '2017-12-8 20:19:34', 0, 1);
INSERT INTO `cms_user_role_r` VALUES (2, '17040406', 'guest', 'admin', '2017-12-8 20:19:34', 'admin', '2017-12-8 20:19:34', 0, 1);
三.配置shiro 
SpringBoot中集成shiro时,因为省去了配置文件,所以需要编写配置类,使用@Configuration注解注入配置类。这里最简洁shiro的配置需要包含三个方法: 
1.注册Realm至Spring Bean,Realm是认证和授权的具体实现,shiro对于使用者来说需要实现的一个方法,它的简洁之处也在于只需要实现这个接口便可使用shiro。 
2.注册SecurityManager安全管理器,无特殊需求使用默认管理器便可,SecurityManager是shiro的主入口,该配置方法将用户自定义的Realm注入SecurityManager即可。 
3.配置Filter访问策略,这里请看代码,相信详细注解一看就明白。如下:

package com.pf.org.cms.common;

import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;

/**
 * @Auther: pf
 * @Date: 2017/12/12 19:34
 * @Description: shiro配置组件
 */
@Configuration
public class ShiroConfiguration {
    private static final Logger log = LoggerFactory.getLogger(ShiroConfiguration.class);

    /**
     * 注入Realm
     * @return MyRealm
     */
    @Bean(name = "myRealm")
    public MyRealm myAuthRealm() {
        MyRealm myRealm = new MyRealm();
        log.info("myRealm注册完成");
        return myRealm;
    }


    /**
     * 注入SecurityManager
     * @param myRealm
     * @return SecurityManager
     */
    @Bean(name = "securityManager")
    public SecurityManager securityManager(@Qualifier("myRealm")MyRealm myRealm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(myRealm);
        log.info("securityManager注册完成");
        return manager;
    }

    /**
     * 注入Filter
     * @param securityManager
     * @return ShiroFilterFactoryBean
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
        ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
        filterFactoryBean.setSecurityManager(securityManager);
        // 配置登录的url和登录成功的url
        filterFactoryBean.setLoginUrl("/auth/login");
        filterFactoryBean.setSuccessUrl("/home");
        // 配置未授权跳转页面
        filterFactoryBean.setUnauthorizedUrl("/errorPage/403");
        // 配置访问权限
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/css/**", "anon"); // 表示可以匿名访问
        filterChainDefinitionMap.put("/fonts/**", "anon");
        filterChainDefinitionMap.put("/imgs/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/auth/**", "anon");
        filterChainDefinitionMap.put("/errorPage/**", "anon");
        filterChainDefinitionMap.put("/demo/**", "anon");
        filterChainDefinitionMap.put("/swagger-*/**", "anon");
        filterChainDefinitionMap.put("/swagger-ui.html/**", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/v2/**", "anon");
        filterChainDefinitionMap.put("/admin/**", "roles[admin]");// 表示admin权限才可以访问,多个加引号用逗号相隔
        filterChainDefinitionMap.put("/*", "authc");// 表示需要认证才可以访问
        filterChainDefinitionMap.put("/**", "authc");
        filterChainDefinitionMap.put("/*.*", "authc");
        filterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        log.info("shiroFilter注册完成");
        return filterFactoryBean;
    }
}
注意shiro支持配置路径,页面标签,注解等等多种权限配置方式,和不同粒度的权限配置。 
1.配置方式:

默认过滤器(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没有参数表示必须存在用户,当登入操作时不做检查   

package com.pf.org.cms.common;

import com.pf.org.cms.entity.UserInfo;
import com.pf.org.cms.entity.UserRoleInfo;
import com.pf.org.cms.service.UserService;
import com.pf.org.cms.utils.MD5Util;
import org.apache.shiro.authc.*;
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.springframework.beans.factory.annotation.Autowired;

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

/**
 * @Auther: pf
 * @Date: 2017/12/12 19:29
 * @Description: 认证和授权具体实现
 */
public class MyRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;

    /**
     * 为当前subject授权
     * @param principalCollection
     * @return AuthorizationInfo
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        Map<String, Object> params = new HashMap<>();
        params.put("userCode", (String) super.getAvailablePrincipal(principalCollection));
        List<UserRoleInfo> userRoleInfos = userService.getUserRoleInfos(params);
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        if(!userRoleInfos.isEmpty()) {
            for(UserRoleInfo role : userRoleInfos) {
                info.addRole(role.getRoleCode());
            }
        }
        return info;
    }

    /**
     * 认证登陆subject身份
     * @param authenticationToken
     * @return AuthenticationInfo
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        Map<String, Object> params = new HashMap<>();
        params.put("userCode", (String)authenticationToken.getPrincipal());
        List<UserInfo> userInfos = userService.getUserInfos(params);
        if (userInfos.isEmpty()) {
            throw new UnknownAccountException();
        } else if(userInfos.size() > 1) {
            throw new DisabledAccountException();
        } else {
            UserInfo user = userInfos.get(0);
            // 校验密码
            return new SimpleAuthenticationInfo(authenticationToken.getPrincipal(), user.getUserPwd(), ByteSource.Util.bytes("2w@W"),  getName());
        }
    }

}

五.登陆和授权验证 
下面是登陆的controller:

package com.pf.org.cms.web;

import com.pf.org.cms.common.IConstants;
import com.pf.org.cms.entity.JsonBean;
import com.pf.org.cms.utils.ParamUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

/**
 * @Auther: pf
 * @Date: 2017/12/12 19:41
 * @Description:
 */
@Controller
@RequestMapping(value = "/auth")
public class AuthenticationController {
    private static final Logger log = LoggerFactory.getLogger(AuthenticationController.class);

    @RequestMapping(value = "/login")
    public String login() {
        return "/login";
    }

    @ResponseBody
    @RequestMapping(value = "/login_in", produces = "application/json;charset=UTF-8")
    public JsonBean loginIn(HttpServletRequest request) {
        JsonBean reJson = new JsonBean();
        Map paramMap = ParamUtils.handleServletParameter(request);
        String userCode = MapUtils.getString(paramMap, "userCode");
        String userPwd = MapUtils.getString(paramMap, "userPwd");
        // shiro认证
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(userCode, userPwd);
        try {
            subject.login(token);
        } catch (UnknownAccountException e) {
            reJson.setMessage("账户不存在");
            return reJson;
        } catch (DisabledAccountException e) {
            reJson.setMessage("账户存在问题");
            return reJson;
        } catch (AuthenticationException e) {
            reJson.setMessage("密码错误");
            return reJson;
        } catch (Exception e) {
            log.info("登陆异常", e);
            reJson.setMessage("登陆异常");
            return reJson;
        }
        reJson.setStatus(IConstants.RESULT_INT_SUCCESS);
        String res = subject.getPrincipals().toString();
        if (subject.hasRole("admin")) {
            res = res + "----------你拥有admin权限";
        }
        if (subject.hasRole("guest")) {
            res = res + "----------你拥有guest权限";
        }
        reJson.setData(res);
        reJson.setMessage("登陆成功");
        return reJson;
    }
}
页面:

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <#include "/common/common.ftl"/>
</head>
<body>
<div>
    <span>hello! please login!</span>
</div>
<div>
    <span>用户名:</span><input type="text" id="userCode" />
    <span>密码:</span><input type="text" id="userPwd" />
</div>
<div>
    <input type="button" οnclick="submitLogin()" value="登陆">
</div>
</body>

</html>

编写完成后启动SpringBoot工程,访问不允许匿名访问的路径,会跳转到登陆页面,登陆成功后可查看自己所拥有的权限,同样可以访问相关路径测试是否授权成功。具体代码可以访问github(https://github.com/15651037763/cms)下载。

写在最后,这里介绍的只是最简单方式的使用,以上github地址为本系列文章项目地址,本人能力有限博客略有延迟,更多shiro功能可以关注github或持续更新,在学习和研究的同时,博客会做到尽量总结全面不遗漏。
 

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐