权限框架Shiro快速整合前后端分离项目
开端好的，起因是我把最近做好的项目给一部分人进行了测试，发现大部分朋友都提出了同一个问题，你的系统权限管理是如何实现的。我只能尴尬的说一句，不好意思这部分还没开发。然而我也知道，其实对于一个项目来说，权限可以说是最主要的一部分。后端除了对权限进行处理，其实也就是提供一些业务逻辑对 CRUD 进行组合拼装。所以我打算接下来学习权限控制方面的知识并整合到我的项目中去，顺便把我的学习笔记分享给大家。而对于权限控制的框架呢，听的最多的还是 Shiro 还有 Spring Security。Spring 的安全框架单独用在 Spring 项目中是无可挑剔的，不管是功能上还是维护方面。但是考虑到 Shiro 是一个全能性的框架，可以用在各种场合，甚至非 Web 项目中，由于它的会话独立于容器，后期学习分布式和微服务的时候也比较方便使用。最重要的是 Spring 官网用的也是 Shiro 的框架。所以还是打算学习 Shiro。由于这部分内容比较多，我也发现了前后端项目部署那篇文章接近 1w 字导致阅读的时候不是很舒服，所以我打算把这个内容分成几个部分：初识 Shiro、配置重点、整合技巧来讲述。这篇文章是关于如何利用我们之前学习的知识快速进行 Shiro 与前后端分离项目的整合。虽然说是快速，我还是会一步步都告诉大家，并没有想象中的那么快。整合那么之前结果两篇长文的讲解，大家应该已经初步了解了一些关于 Shiro 的知识，接下来我们开始快速整合 Shiro 和 SSM ，我只讲最关键的步骤，至于更多额外功能的拓展留给大家自行发掘。下文的内容可能略微与我前面写的两篇文章有所不同，不过不影响食用。毕竟一边做一边写，总会发现之前的一些问题并找到更好的解决方法。使用 Maven 管理 jar 包Maven 是一个后端的包管理工具，可以使用写 XML 的形式快速从仓库寻找 jar 包。使用阿里云仓库众所周知，如果你从 github clone 一个项目，如果没有梯子下载 jar 包可能就要花几个小时的时间。还好阿里爸爸给我们提供了镜像仓库，几分钟就能解决 jar 包的问题。关于引用仓库可以在 maven 配置中修改：<mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>不过我还是推荐大家在自己的项目中加入仓库，灵活性更高：<repositories> <repository> <id>alimaven</id> <name>aliyun maven</name> <url>https://maven.aliyun.com/repository/public</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories>这里我就直接用 public 仓库，这个仓库整合了两个小仓库，东西更丰富些。releases 是线上版本，snapshots 是开发版本，都填 true 问题不大。搜索并导入 jar 包大家可以在上边这个网址内搜索自己要找的 jar 包，中心 central 仓库够大了，组织的话就是 Shiro 的网址，按结构写不要反了，然后输入文件名，点击搜索到的蓝色文件名后把 Maven 依赖这些代码复制进项目里即可。像下面的一样，我们引入最新版本的 Shiro 相关的 jar 包。具体使用哪些包的话请大家去官网上查找资料，比如说官网里提示说 core 包需要 slf4j 的支持，再比如说你用了谷歌的 IOC 容器，那么需要使用 guice 包等等。 <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-all</artifactId> <version>1.5.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.5.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.5.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.5.3</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-web-starter</artifactId> <version>1.5.3</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.9</version> </dependency>日志包和 Spring Boot 包都不是必要的，主要还是 shiro-all 和 shiro-core 这两个核心包。权限五表的建立请转到下文，翻到最后，把权限五表的 SQL 代码运行一下，完成用户、角色。权限、用户角色、角色权限五表的建立。我在这里贴出我的五张表，与下文中的略微不同，为了方便整合作出了一些调整。MySQL建表DROP TABLE IF EXISTS `sys_user`; CREATE TABLE `sys_user` ( `id` INT AUTO_INCREMENT NOT NULL, `usrName` VARCHAR ( 255 ) DEFAULT '' NOT NULL, `usrPassword` VARCHAR ( 255 ) DEFAULT '' NOT NULL, `usrNick` VARCHAR ( 255 ) DEFAULT '' NOT NULL, `usrPhone` VARCHAR ( 255 ) DEFAULT '' NOT NULL, `usrEmail` VARCHAR ( 255 ) DEFAULT '' NOT NULL, `remark` VARCHAR ( 255 ) DEFAULT '' NOT NULL, `lastLogin` BIGINT DEFAULT 0 NOT NULL, `utcCreate` BIGINT DEFAULT 0 NOT NULL, `utcModify` BIGINT DEFAULT 0 NOT NULL, `modifyBy` VARCHAR ( 255 ) DEFAULT '' NOT NULL, `valid` BIT (1) DEFAULT 1 NOT NULL, PRIMARY KEY ( `id` ) ) ENGINE=INNODB DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `sys_role`; CREATE TABLE `sys_role` ( `id` int(64) NOT NULL AUTO_INCREMENT, `description` varchar(32) DEFAULT NULL, `role` varchar(32) DEFAULT NULL, `remark` varchar( 255 ) DEFAULT NULL, `utcCreate` BIGINT DEFAULT 0, `utcModify` BIGINT DEFAULT 0, `modifyBy` VARCHAR ( 255 ) DEFAULT NULL, `valid` BIT (1) DEFAULT 1, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `sys_permission`; CREATE TABLE `sys_permission` ( `id` int(64) NOT NULL AUTO_INCREMENT, `name` varchar(64) DEFAULT NULL, `permission` varchar(64) DEFAULT NULL, `url` varchar(64) DEFAULT NULL, `remark` varchar( 255 ) DEFAULT NULL, `utcCreate` BIGINT DEFAULT 0, `utcModify` BIGINT DEFAULT 0, `modifyBy` VARCHAR ( 255 ) DEFAULT NULL, `valid` BIT (1) DEFAULT 1, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `sys_user_role`; CREATE TABLE `sys_user_role` ( `role` VARCHAR(255) NOT NULL, `user_name` VARCHAR(255) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `sys_role_permission`; CREATE TABLE `sys_role_permission` ( `role` VARCHAR(255) NOT NULL COMMENT '角色名', `permission` VARCHAR(255) NOT NULL COMMENT '权限名' ) ENGINE=InnoDB DEFAULT CHARSET=utf8;主要的不同在于两张关联表中我都是假定某个字段不能修改，然后把它当成关联键进行操作。如果直接用用户、角色、权限表的 id 主键的话确实更方便更合理，但是我还没正式学过 SQL ，只会简单的增删改查。前面讲到的正统方法涉及 SQL 中 LEFT JOIN 等操作，比较复杂，各位要是学过 SQL 推荐用主键关联。后端五表一条龙后端对于这五张表的操作主要在 POJO 层、Mapper 层、Mybatis 层、Service 层和 Controller 层。就和你的项目系统中任何一个实体类一样，方方面面都需要涉及。那么我相信大家既然看到这里，已经会写接口会写 Mybatis，所以怎么写就不讲了，贴一些重点的内容。POJOPOJO 层的实体类属性需要严格按照之前 MySQL 中的情况来，具体见下表：就举一个 Permission 的权限实体作参考，使用了 Lombok。@Getter @Setter @ToString(callSuper = true) @EqualsAndHashCode(callSuper = true) @NoArgsConstructor @AllArgsConstructor public class Permission extends BaseEntity { private static final long serialVersionUID = 8482551052982019660L; private Integer id; private String name; private String permission; private String url; }创建时间、修改时间等公共属性都放在 BaseEntity 中。就只要这么写一个实体类就完成了，Lombok 注解实现非常简单。同样对用户和角色作如上操作即可。Mapper、Mybatis这块关联性大一些，就放在一起写了。主要就是在 Mapper 中写接口，在 XML 的配置文件里写对应方法的 Mybatis SQL 语句。增删改查不多说，由于我们的 Shiro 在认证时需要查询用户、查询用户角色和查询角色权限这三个数据库操作，因此我只写这三个，其他自行完成。@Repository public interface UserMapper { User selectByName(@Param("usrName") String usrName); } @Repository public interface RoleMapper { Set<String> findRoleByUserName(@Param("usrName") String usrName); } @Repository public interface PermissionMapper { Set<String> findPermissionByRole(@Param("role") String role); }注意一下根据官方给出的案例，后边两个操作需要返回 Set 集合，我没试过 List 可不可行，既然例子是 Set 咱们也用 Set。接下来写 XML（我写在一起了，实际需要分三个 XML 写）： SELECT * FROM sys_user WHERE usrName = #{usrName} SELECT role FROM sys_user_role WHERE user_name = #{usrName} SELECT permission FROM sys_role_permission WHERE role = #{role} 需要注意的是，如果在 Spring 配置中没有为 Mybatis 对应的 POJO 配置 type-aliases，需要写全包名。而像 String 这种基本包装类简写会报错但不影响程序运行，这是 IDEA 的一个小 bug。千万别忘了写增删改查等等其他操作，我们接着往下。ServiceService 需要对数据库的原子操作开始组合，由于涉及接口和实现类，我这里只举一个例子：public interface PermissionService { Set<String> findPermissionByRole(String paramRole); } @Service @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class, isolation = Isolation.READ_COMMITTED) public class PermissionServiceImpl implements PermissionService { @Resource private PermissionMapper permissionMapper; @Override public Set<String> findPermissionByRole(String paramRole) { return permissionMapper.findPermissionByRole(paramRole); } }注解主要是告诉 Spring 这是一个 Service ，交给它管理，然后设置一下事物传播属性、报错回滚类型、事物隔离级别等。其他基本业务需求也请自行组装。Controller终于到了最后一层，这一层就和前面的不太一样了。角色和权限的 Service 接口只在 Shiro 配置的时候用到，所以不需要写其他的。除非你的系统中涉及权限和角色的增删改查和分配才需要写，当然一般来说都是有这个需求的。我依旧是只举一个例子：@CrossOrigin @RestController() @RequestMapping("/permission/") public class PermissionController extends BaseController { @Resource(name = "permissionServiceImpl") private PermissionServiceImpl permissionServiceImpl; @GetMapping(value = "/findPermissionByRole") @ResponseBody public Response<Set<String>> findPermissionByRole(@RequestParam String role) { Set<String> results = permissionServiceImpl.findPermissionByRole(role); if (results.size() != 0) { return getSuccessResult(results); } return getFailResult(404, "MEssage not found!"); }这里的跨域注解只适用于项目初期，结合了 Vue 特别是整合了 Shiro 之后，跨域问题已经不是这个注解能够解决的了，我会在后面讲到。对于三个 Controller 的功能请自行完成。至此，对于我们来讲项目基本所需的新内容已经完成，接下来就是要进行 Shiro 的配置来实现权限的自动管理。配置Shiro开始配置 Shiro，我要先提跨域问题。在项目初期，跨域是不存在的，因为只有后端的程序在跑，直接用 Postman 测试不会出问题。在我们整合了 Vue 之后，开始出现跨域问题。由于本地测试时 Vue 其实是跑在 Node.js 的，前后端是两个不同的端口号。但是这是的跨域问题也不是特别难解决，设置一个全局跨域处理即可。说的简单其实我第一次碰到的时候捉摸了好几个周才解决。但是当我们整合 Shiro，就会多出好多问题。讲个容易理解的问题，在访问网页的时候并不是一次连通的，又是会遇到多次“握手”，浏览器先发送一个 OPTIONS 类型的请求（PreFlight）给后端，这种叫做预检请求，以检测实际请求是否可以被服务器所接收。那么问题就来了，既然我们设置了权限管理，用户在登录系统前肯定是不被允许有其他权限的，那么这个所谓的预检请求，就会被 Shiro 拦截。在 Shiro 中我们只能对指定接口指定 URL 的内容进行拦截或放行，但是对于预检请求就无能为力。这只是一个小例子，实际开发中还会遇到其他各种问题。所以我们先从跨域处理开始，以应对之后遇到的问题。创建工具类解决跨域对跨域忍无可忍，终于集合各路奇技，写了一个工具类解决一切跨域问题。public class CorsUtil { private static final String OPTIONS_FOR_REQUEST = "OPTIONS"; public static void setResponseHeader(HttpServletResponse response, HttpServletRequest request) { // 设置编码格式 response.setContentType("text/html;charset=UTF-8"); // 允许哪些Origin发起跨域请求,nginx下正常 response.setHeader( "Access-Control-Allow-Origin", request.getHeader("Origin")); // 允许请求的方法 response.setHeader( "Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE" ); // 多少秒内，不需要再发送预检验请求，可以缓存该结果 response.setHeader( "Access-Control-Max-Age", "86400" ); // 表明它允许跨域请求包含xxx头 response.setHeader( "Access-Control-Allow-Headers", "Origin, No-Cache, X-Requested-With," + "If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type," + "X-E4M-With,userId,token, Accept, Authorization" ); // 允许浏览器携带用户身份信息（cookie） response.setHeader( "Access-Control-Allow-Credentials", "true" ); response.setHeader("XDomainRequestAllowed","1"); // 允许OPTIONS请求直接通过 if (OPTIONS_FOR_REQUEST.equals((request).getMethod().toUpperCase())) { response.setStatus(HttpServletResponse.SC_OK); } } }可以看到此类中，编码设置了；对源头地址直接采用 get 的方法不需要每次都重新修改了；请求方法接受 POST、GET、OPTIONS 和 DELETE；请求中也允许了各种类型奇奇怪怪的请求头，应有尽有；最后还设置了允许携带 Cookie 信息，允许同步 Session 信息等。方法的参数是 HttpServlet 中的 Response 和 Request，对 Response 进行处理后返回。基本上以后遇到什么需要跨域的处理，直接在需要的地方调用这个方法即可，比我之前百度到的什么配置全局拦截器好用多了。创建登录拦截器既然写好的跨域根据类，接下来我们重写一下 Shiro 的跨域过滤器。@Component @WebFilter(urlPatterns = "/*",filterName = "shiroCrossFilter") public class ShiroLoginFilter implements Filter { private FilterConfig config = null; @Override public void init(FilterConfig config) throws ServletException { this.config = config; } @Override public void destroy() { this.config = null; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) servletResponse; HttpServletRequest request = (HttpServletRequest) servletRequest; CorsUtil.setResponseHeader(response, request); filterChain.doFilter( servletRequest, response ); } }最主要的是 @WebFilter注解，名字千万不能错，要完全一致。然后重写一下三个方法，在 doFilter( ) 方法中使用我们刚刚写的跨域工具类处理。创建全局异常处理器针对登陆账户和操作权限时因某种因素操作失败，Shiro 会直接在控制台抛出异常，我们设置一个全局处理器找到这些异常并返回一些容易辨认的中文提示，也方便前端进行提示。@RestControllerAdvice public class GlobalExceptionHandle { @ExceptionHandler(ShiroException.class) public String doHandleShiroException(ShiroException se, Model model) { se.printStackTrace(); if(se instanceof UnknownAccountException) { return "该账户不存在"; } else if (se instanceof LockedAccountException) { return "该账户已锁定"; } else if (se instanceof IncorrectCredentialsException) { return "密码错误请重试"; } else if (se instanceof AuthorizationException) { return "没有相应权限"; } else { return "操作失败请重试"; } } }创建自定义会话管理器public class CustomSessionManager extends DefaultWebSessionManager { private static final Logger logger = LoggerFactory.getLogger(CustomSessionManager.class); private static final String AUTHORIZATION = "Authorization"; private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request"; public CustomSessionManager() { super(); setGlobalSessionTimeout(DEFAULT_GLOBAL_SESSION_TIMEOUT * 48); } @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response) { String sessionId = WebUtils.toHttp(request).getHeader(AUTHORIZATION); if (!StringUtils.isEmpty(sessionId)) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); return sessionId; } else { return super.getSessionId(request, response); } } }这部分照抄就可以了，没什么好讲的。创建授权拦截器对于访问某个资源，会出现两种情况，一种就是你有权限可以访问，一种就是你没有权限被 Shiro 拦截，在这里也会涉及跨域问题，所以我们继承 UserFilter （根据你的默认网页拦截规则选择继承的类），重写通过和拦截两个方法，首先给大家 Shiro 默认拦截器类对应的名称，再给代码。名称类authcFormAuthenticationFilterauthcBasicBasicHttpAuthenticationFilterlogoutLogoutFilteruserUserFilteranonAnonymousFilterrolesRolesAuthorizationFilterpermsPermissionsAuthorizationFilterportPortFilterrestHttpMethodPermissionFiltersslSslFilternoSessionCreationNoSessionCreationFilter每个拦截器的作用我在前文都讲过，不赘述了。public class CorsAuthenticationFilter extends UserFilter { private static final String OPTIONS_FOR_REQUEST = "OPTIONS"; public CorsAuthenticationFilter() { super(); } @Override public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { if (OPTIONS_FOR_REQUEST.equals(((HttpServletRequest) request).getMethod().toUpperCase())) { return true; } return super.isAccessAllowed(request, response, mappedValue); } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { HttpServletResponse res = (HttpServletResponse)response; HttpServletRequest req = (HttpServletRequest)request; CorsUtil.setResponseHeader(res, req); res.setStatus(HttpServletResponse.SC_OK); PrintWriter writer = res.getWriter(); Map<String, Object> map= new HashMap<>(16); map.put("success", false); map.put("code", 702); map.put("message", "用户未登录"); map.put("timestamp", DateUtil.currentSecond()); writer.write(JSON.toJSONString(map)); writer.close(); return false; } }当用户认证成功，只需要判断请求是否是 OPTIONS 类型，如果是直接放行。当用户认证失败，用户就不具有权限访问，需要调用我们的跨域工具进行处理，并且告诉控制台用户未登录，返回 false 进行拦截。创建安全实体数据源接下来的两块是最重要的，在之前的文章中也多次讲到过，这里再次演示一遍。public class MyShiroRealm extends AuthorizingRealm { private static final Logger LOGGER = LoggerFactory.getLogger(MyShiroRealm.class); @Resource(name = "roleServiceImpl") private RoleService roleService; @Resource(name = "permissionServiceImpl") private PermissionService permissionService; @Resource(name = "userServiceImpl") private UserService userService; @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String userName = token.getUsername(); User user = userService.selectByName(userName); if (user == null) { throw new UnknownAccountException("Message not found"); } else if (!user.getValid()) { throw new LockedAccountException("Account locked"); } return new SimpleAuthenticationInfo( user.getUsrName(), user.getUsrPassword(), ByteSource.Util.bytes(userName), getName() ); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); String userName = (String)principalCollection.getPrimaryPrincipal(); if (userName == null) { LOGGER.error("授权失败，用户信息为空!"); return null; } try { Set<String> roles = roleService.findRoleByUserName(userName); simpleAuthorizationInfo.addRoles(roles); for (String role : roles) { Set<String> permissions = permissionService.findPermissionByRole(role); simpleAuthorizationInfo.addStringPermissions(permissions); } return simpleAuthorizationInfo; } catch (Exception e) { LOGGER.error("授权失败，系统内部错误!"); } return simpleAuthorizationInfo; } }需要注意的是按照原则，应当注入接口，使用@Resource注解和实现类的名字进行注入。用户验证使用 UserService 查询数据库中的用户信息并验证；用户授权使用 PermissionService 和 RoleService，首先从数据库读取用户角色交给 Shiro 管理。对于每个用户所对应的角色都从数据库再读取角色对应权限，由于 Set 集合有去重性，会得到唯一的 permission 集合，将其交给 Shiro 管理。具体的就不多说啦，见前两篇文章吧。创建配置之前写了这么多杂七杂八的，其实都是打地基，接下来要把他组合在 Shiro 配置中让其生效。@Configuration public class ShiroConfiguration { private final Log logger = LogFactory.getLog(ShiroConfiguration.class); @Bean public MyShiroRealm myShiroRealm(){ // 生成之前配置的数据源 return new MyShiroRealm(); } @Bean public CacheManager cacheManager() { // 生成缓存管理器 return new MemoryConstrainedCacheManager(); } @Bean public SessionManager sessionManager() { // 生成之前配置的会话管理器 return new CustomSessionManager(); } @Bean public CorsAuthenticationFilter corsAuthenticationFilter() { // 生成之前配置的授权拦截器 return new CorsAuthenticationFilter(); } @Bean public DefaultWebSecurityManager securityManager(){ // 生成默认安全管理器后将上文中配置全部加入其中 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); securityManager.setCacheManager(cacheManager()); securityManager.setRememberMeManager(rememberMeManager()); securityManager.setSessionManager(sessionManager()); return securityManager; } // 这部分的含义见之前的文章 @Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/user/login"); Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/favicon.ico","anon"); filterChainDefinitionMap.put("/user/login","anon"); filterChainDefinitionMap.put("/**", "corsAuthenticationFilter"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); Map<String, Filter> filterMap = new LinkedHashMap<>(); filterMap.put("corsAuthenticationFilter", corsAuthenticationFilter()); shiroFilterFactoryBean.setFilters(filterMap); return shiroFilterFactoryBean; } /** * SpringShiroFilter首先注册到spring容器 * 然后被包装成FilterRegistrationBean * 最后通过FilterRegistrationBean注册到servlet容器 */ @Bean public FilterRegistrationBean delegatingFilterProxy(){ FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); DelegatingFilterProxy proxy = new DelegatingFilterProxy(); proxy.setTargetFilterLifecycle(true); proxy.setTargetBeanName("shiroFilter"); filterRegistrationBean.setFilter(proxy); return filterRegistrationBean; } @Bean public SimpleCookie rememberMeCookie(){ // 配置rememberMe Cookie SimpleCookie simpleCookie = new SimpleCookie("rememberMe"); simpleCookie.setMaxAge(24 * 60 * 60); simpleCookie.setHttpOnly(false); return simpleCookie; } @Bean public CookieRememberMeManager rememberMeManager(){ // 将Cookie交给管理器并加密 CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager(); cookieRememberMeManager.setCookie(rememberMeCookie()); cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag==")); return cookieRememberMeManager; } // 后面这仨方法直接照抄即可，配置生命周期和代理，否则Shiro无法生效 @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager()); return authorizationAttributeSourceAdvisor; } @Bean @ConditionalOnMissingBean @DependsOn({"lifecycleBeanPostProcessor"}) public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } } 至此配置就算是全部完成了，接下来只需要在 Controller 层设置所需权限。注解标记权限@RestController() @CrossOrigin @RequestMapping("/user/") public class UserController extends BaseController { @Resource(name = "userServiceImpl") private UserService userService; @RequiresRoles("admin") @RequiresPermissions("user:selectAll") @GetMapping(value = "/selectAll") @ResponseBody public Response<List<User>> selectAll() { List<User> result = userService.selectAll(); if (result.size() != 0) { return getSuccessResult(result); } return getFailResult(404, "Message not found!"); } }比如我的用户 Controller 中的查询方法，同时使用@RequiresRoles和@RequiresPermissions表示用户不仅需要 admin 角色，而且还需要 user:selectAll 权限才能够使用此 api 接口，缺一不可。换句话来说，如果我给其他角色也分配了这个权限，他们依旧不能使用此方法，admin 用户被删除该权限后也将无法使用此方法。除此之外我再贴一个登陆方法和一个退出登录方法，其他都和查询方法类似。@PostMapping(value = "/login") @ResponseBody public Response<User> userLogin(@RequestBody User sysUser) { User result; Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(sysUser.getUsrName(), sysUser.getUsrPassword()); token.setRememberMe(true); try { subject.login(token); result = userService.userLogin(sysUser.getUsrName(), sysUser.getUsrPassword()); session.setAttribute("user", subject); } catch (UnknownAccountException e) { return getFailResult(404, "Message not found"); } catch (IncorrectCredentialsException e) { return getFailResult(412, "Incorrect message"); } catch (LockedAccountException e) { return getFailResult(401, "Account locked"); } catch (Exception e) { e.printStackTrace(); return getFailResult(408, "Unknown error"); } return getSuccessResult(result); } @GetMapping(value = "/logout") @ResponseBody public Response<String> userLogout() { CorsUtil.setResponseHeader(response, request); Subject subject = SecurityUtils.getSubject(); try { subject.logout(); } catch (SessionException e) { e.printStackTrace(); return getFailResult(408, e.toString()); } return getSuccessResult("logout"); }这部分之前也讲过，这里不说了，由于整合 Shiro 是在涉及了大量的内容，如果全部解释一遍今天这篇又得拆成很多篇来讲，我之所以分三篇写就是为了让这一篇的内容分散到其他两篇中去，有需要的同学可以回去巩固一下基础知识。最终测试差不多可以收工了，我们最后再使用 Postman 测试一下接口看看是什么效果。直接调查询所有用户的接口，发现提示“用户未登录”。接下来调用登录接口并传数据库中已经有的用户参数，发现登录成功。在登录之后重新调用查询接口，发现现在已经拥有看权限，可以点开 Cookie 查看，也是我们在后端设置过的 rememberMe，Headers 中也有我们设置的跨域处理内容。那么至此，Shiro 整合 SSM 项目就大功告成了，你可以在后端拓展一些关于权限方面的业务，在前端也做一些权限控制的内容，使项目更加完善。总结这篇文章解释性的话并不多，主要都是代码。从头到尾，贴出了配置 Shiro 并整合到项目中的过程。准确的来说，如果想要权限的功能完完全全的配置成功，这些代码都是必不可少的。因为其中还涉及跨域问题的处理等在实际操作中会遇到的问题，许多问题都是我在操作中遇到并解决的，其他一些博客中则并没有讲到。希望这篇文章对你有所帮助，尤其是也在开发前后端分离项目并且遇到很多问题的同学。接下来我打算写一篇前后端项目整合 Swagger 的文章，毕竟 api 接口对于前后端分离的项目可以说是两个人沟通的桥梁，非常容易在这一块出问题，学会使用 Swagger 不仅能减少问题的产生，也能优化我们的后台管理系统，丰富其内容。如果有什么建议，欢迎评论交流。
开端好的，起因是我把最近做好的项目给一部分人进行了测试，发现大部分朋友都提出了同一个问题，你的系统权限管理是如何实现的。我只能尴尬的说一句，不好意思这部分还没开发。然而我也知道，其实对于一个项目来说，权限可以说是最主要的一部分。后端除了对权限进行处理，其实也就是提供一些业务逻辑对 CRUD 进行组合拼装。所以我打算接下来学习权限控制方面的知识并整合到我的项目中去，顺便把我的学习笔记分享给大家。而对于权限控制的框架呢，听的最多的还是 Shiro 还有 Spring Security。Spring 的安全框架单独用在 Spring 项目中是无可挑剔的，不管是功能上还是维护方面。但是考虑到 Shiro 是一个全能性的框架，可以用在各种场合，甚至非 Web 项目中，由于它的会话独立于容器，后期学习分布式和微服务的时候也比较方便使用。最重要的是 Spring 官网用的也是 Shiro 的框架。所以还是打算学习 Shiro。由于这部分内容比较多，我也发现了前后端项目部署那篇文章接近 1w 字导致阅读的时候不是很舒服，所以我打算把这个内容分成几个部分：初识 Shiro、配置重点、整合技巧来讲述。这篇文章是关于 Shiro 该如何配置的，面向 Spring Boot，讲一些比较基础的内容。深入Shiro之前我们了解了一下 Shiro 的基本功能和架构，现在我们来看看它的工作流程。工作流程拦截Shiro 特别重要的一块就是拦截，它通过过滤器拦截所有请求，根据配置决定哪些可以认证通过，哪些不用被拦截，还有哪些没有通过认证强制跳转。我们看一段配置代码，如何通过过滤器决定谁去谁留。@Bean(name = "shiroFilter") public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/user/login"); Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/user/login","anon"); filterChainDefinitionMap.put("/**", "corsAuthenticationFilter"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); Map<String, Filter> filterMap = new LinkedHashMap<>(); filterMap.put("corsAuthenticationFilter", corsAuthenticationFilter()); shiroFilterFactoryBean.setFilters(filterMap); return shiroFilterFactoryBean; }代码非常简单，首先使用 @Bean 注释将这个方法交给 Spring 来管理，名字必须是 shiroFilter，这样才能和默认配置中的名字匹配，否则 Spring 会找不到这个过滤器。在这个方法中首先 new 一个过滤器工厂对象，将获取到的 securityManager 注入。之后设置一个登陆 url，告诉 Shiro，这个页面/接口是我的登陆页面，用户角色权限的获取也都在这里。接着就是刚刚图中的 filterChainDefinition，我们定义一个 map 来存储不被拦截的页面。每一个键值对中 key 存储 url，value 存储拦截器的名字，拦截器主要是以下几个：拦截器名称拦截器功能anno不需要授权和登录，可以匿名访问authc需要登录授权才能访问logout退出成功之后可以自定义重定向user需要登录授权或者登录后开启了记住我（remember me）就能访问ssl通过https协议才能通过roles角色拦截restrest风格拦截port端口拦截perms权限拦截noSessionCreation不创建会话连接器authcBasic基本http身份验证拦截比较常用的四个拦截器我使用加粗表示出来了，anno 一般是最先使用的，为什么说最先使用，因为拦截器拦截的顺序是按照你 map 中加入的键值对的顺序来的。按照第一次匹配优先原则，如果一开始就被拦截，那么之后如果符合条件也不会放行。有一些公开资源，尤其是登录接口，肯定是要设置 anno 匿名的。假如说连入口的登录功能都拦截，那用户还这么使用这个系统的其他功能呢？authc 是指需要登录或授权才能访问，这个值的 key 一般都是 "/*" ，而且写在最后。表示除了上述设置的拦截规则，对于其他所有 url 全部拦截并且需要授权才能访问。而我这里没有这么写，而是自定义了一个 corsAuthenticationFilter。从后面的代码也可以看到我又定义了一个 filterMap* ，使用我自定义的一个过滤方式拦截其他所有请求。本来我也是采用 authc 的方式，但是由于前后端跨域的需要，必须要加上跨域请求头的一些参数，所以直接另写了一个过滤器类，并且在其中处理了验证通过和验证失败的处理方式。具体可见下一篇，SSM 整合 Shiro，这里不多赘述。认证那么拦截之后我们怎么样判断用户是什么角色，拥有哪些权限呢？这块其实我在上一篇中有所讲到，已经把源码拉出来解析过了。有需要的朋友可以再去回顾一下，我这里只讲它整个认证的流程。获取当前的 Subject，调用 SecurityUtils.getSubject( )测试当前的用户是否已经被认证，即是否已登陆，调用 Subject 的 isAuthenticated( )若没有被认证，则把用户名和密码封装为 UsernamePasswordToken 对象执行登陆：调用 Subject 的 login(AuthenticationToken) 方法自定义 Realm 方法，从数据库中获取相应的记录，返回给 Shiro由 Shiro 完成对密码的比对回顾第一篇我们讲到的，第一步是从数据库中找到所有的用户。第三步是通过我们前端获取到的用户名和密码生成 token。第四步是利用生成的 token 参数传给 login 方法进行登录。登录时对用户角色和权限的验证采用了自定义 Reaml 中重写的两个方法 doGetAuthenticationInfo( ) 和 doGetAuthorizationInfo( )，还记得吧，挺容易搞混的。机制在 Shiro 中有许多我们曾经经常使用到的内容，比如缓存，还有一些我们不太了解的东西，比如盐值加密。我这里简要讲一讲这些机制。MD5盐值加密盐值加密是我以前没有听说过的知识，所以在我第一次整合 Shiro 的时候也没有加入这一部分的内容。众所周知，用户名和密码是被保存在数据库中。可是一旦数据库发生了泄露，用户名和密码就都遭到了泄露。攻击者可以轻松的获取用户名和密码，进行操作。更大的危害是，由于现在需要注册的网站、app越来越多。用户名和密码很多时候都是相同的。一旦某处发生了泄露，则后果会慢慢的扩散。这些危害大家可以查询下近些年发生的一些安全事故，如Sony数据库泄露、网易数据库泄露、CSDN数据库泄露等，因此我们要对密码进行加密。为何取名为盐，此盐非彼颜。盐是许许多多微小的颗粒组成的，他就像一个随机数，你猜不到有多少颗。当你给密码加了盐，再结合 MD5，就不那么容易被破解了。盐值加密主要就是如何生成盐，我们一般使用唯一的数据来产生。比如说我的数据库中用户名是不可重复的，那么用户名就是唯一的（昵称不唯一），所以可以使用用户名生成盐。ByteSource.Util.bytes(userName)ByteSource 是 Shiro 提供的一个工具类，可以转换进制。其中还有一个静态子类 Util，里面的 bytes( ) 方法返回了一个 SimpleByteSource 对象，而这个对象又使用 CodecSupport 中的 toBytes( ) 方法对接收到的不同参数采取不同的加密方式。以下为源码：protected byte[] toBytes(Object o) { if (o == null) { String msg = "Argument for byte conversion cannot be null."; throw new IllegalArgumentException(msg); } else if (o instanceof byte[]) { return (byte[])((byte[])o); } else if (o instanceof ByteSource) { return ((ByteSource)o).getBytes(); } else if (o instanceof char[]) { return toBytes((char[])((char[])o)); } else if (o instanceof String) { return toBytes((String)o); } else if (o instanceof File) { return this.toBytes((File)o); } else { return o instanceof InputStream ? this.toBytes((InputStream)o) : this.objectToBytes(o); } }一般而言我们生成了盐值之后，会使用 MD5 将原密码与盐值拼接在一起，然后再次进行加密，之后的加密次数是可以设置的，理论上加密的次数越多越安全。因为我没搞过安全，不太了解这一方面。我觉得咱们要是做个项目练练手的话没有必要加这个内容。如果需要，加密两次也足够了。因为我的项目数据表中没有写盐值，所以没有尝试过存在数据库中，只单独测试了几个例子，也就不展开讲了。多Realm验证通常来讲一个项目可能会用到多个数据库，比如说用户的验证需要 MySQL 和 Oracle 两个数据库中的数据，那这里就涉及到了多 Realm 验证和验证策略的问题。对于每一个 Realm 都要配置到 ModularRealmAuthenticator 类中。还记得我们在认证和上一篇的 Shiro 基本功能中讲到了两个 doGetAuthenticationInfo( ) 和 doGetAuthorizationInfo( ) 方法吗。这两个方法就不多说了，我们接着要复写 ModularRealmAuthenticator 类，主要是要重写两个 isPermitted( ) 方法和一个 hasRole( ) 方法。假设我们已经拥有了一个 AdminRealm 和一个 UserReal，其中分别重写了两个 doGet 方法。那么我们就要在 ModularRealm 中通过匹配判断应该实现哪一个验证器的策略。public class CustomizedModularRealmAuthorizer extends ModularRealmAuthorizer { @Override public boolean isPermitted(PrincipalCollection principals, String permission) { assertRealmsConfigured(); Set<String> realmNames = principals.getRealmNames(); //获取realm的名字 String realmName = realmNames.iterator().next(); for (Realm realm : getRealms()) { if (!(realm instanceof Authorizer)) continue; //匹配名字 if(realmName.equals("admin"))) { if (realm instanceof AdminRealm) { return ((AdminRealm) realm).isPermitted(principals, permission); } } if(realmName.equals("user")) { if (realm instanceof UserRealm) { return ((UserRealm) realm).isPermitted(principals, permission); } } } return false; } @Override public boolean isPermitted(PrincipalCollection principals, Permission permission) { // 省略，和上一个方法一样 } @Override public boolean hasRole(PrincipalCollection principals, String roleIdentifier) { // 省略，和上一个方法一样 } }接着在 ShiroConfiguration 中的 securityManager( ) 中加入你的多 Realm，就像这样：@Bean public DefaultWebSecurityManager securityManager(UserRealm customRealm, AdminRealm adminRealm, DefaultWebSessionManager sessionManager) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //======================设置多个Realm====================== List<Realm> realms = new ArrayList<>(); realms.add(customRealm); realms.add(adminRealm); securityManager.setRealms(realms); securityManager.setSessionManager(sessionManager); securityManager.setCacheManager(new RedisCacheManager()); //====================多realm授权核心代码=================== CustomizedModularRealmAuthorizer authorizer = new CustomizedModularRealmAuthorizer(); authorizer.setRealms(realms); securityManager.setAuthorizer(authorizer); return securityManager; }注意咯，不仅要把 Realm 放进 SecurityManager 中，还要将其放入 CustomizedModularRealmAuthorizer 中。这样相当于开启了多 Realm 模式，程序会在运行到 doAuthenticate( ) 方法时判断有多个 Realm，然后返回多 Realm 认证策略。// 判断Realm个数 protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { this.assertRealmsConfigured(); Collection<Realm> realms = this.getRealms(); return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken); } // 施行多Realm策略 protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) { if (!realm.supports(token)) { String msg = "Realm [" + realm + "] does not support authentication token [" + token + "]. Please ensure that the appropriate Realm implementation is configured correctly or that the realm accepts AuthenticationTokens of this type."; throw new UnsupportedTokenException(msg); } else { AuthenticationInfo info = realm.getAuthenticationInfo(token); if (info == null) { String msg = "Realm [" + realm + "] was unable to find account data for the submitted AuthenticationToken [" + token + "]."; throw new UnknownAccountException(msg); } else { return info; } } }认证策略ModularRealmAuthorizer 有三种认证策略：名称策略FirstSuccessfulStrategy只要有一个Ream验证成功即可，返回第一个验证成功的认证信息而忽略其他AtLeastOneSuccessfulStrategy只要有一个Ream验证成功即可，返回所有认证成功的验证信息AllSuccessfulStrategy所有Realm都认证成功才算成功，且返回所有认证成功的验证信息之前没有讲这个，因为 ModularRealmAuthorizer 默认实现第二种认证策略，所以只要有一个认证成功即可。这块的配置我只能查到使用 ini 或 xml 的形式，百度根本没有 Spring Boot 的配置方式，如果有小伙伴会直接在 ShiroConfig 类中修改的话请评论告诉我谢谢。下面是我找到的源码，不过还是不太能懂他具体怎么使用这个配置的。public class AllSuccessfulStrategyTest { private AllSuccessfulStrategy strategy; @Before public void setUp() { strategy = new AllSuccessfulStrategy(); } @Test public void beforeAllAttempts() { AuthenticationInfo info = strategy.beforeAllAttempts(null, null); assertNotNull(info); } @Test public void beforeAttemptSupportingToken() { new SimpleAccountRealm(); } @Test(expected = UnsupportedTokenException.class) public void beforeAttemptRealmDoesntSupportToken() { Realm notSupportingRealm = new AuthorizingRealm() { public boolean supports(AuthenticationToken token) { return false; } protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { return null; } protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) { return null; } }; strategy.beforeAttempt(notSupportingRealm, null, null); } }权限注解Shiro 中权限的配置有多种方式，其中编程式不太使用，因为需要写很多代码。老式写法一般都是 xml，我的话采用注解来完成。 <property name="filterChainDefinitions"> <value> /login = anno /logout = logout /user = roles[user] /admin = permissions[admin:check] </value> </property>// 注解形式 @RequiresRoles("user") @RequiresPermissions("admin:check") @GetMapping(value = "/selectAll") @ResponseBody public Response<List<User>> selectAll() { List<User> result = userService.selectAll(); if (result.size() != 0) { return getSuccessResult(result); } return getFailResult(404, "Message not found!"); }显然注解更加方便，xml 的话能够统一配置。特别注意一下，如果一个接口同时注解了 role 和 permission，必须要同时满足才可以访问。除此之外还有 @RequireUser和 @RequireAuthentication 和 @RequireGuest 三种注解，分别表示需要登录或者记住我、必须登录、没有验证或记住我登录这三个条件，前两个相当于之前认证中讲到的 user、 authc。标签这部分内容是 jsp 中的标签，可以根据 shiro 的授权为不同角色显示不同信息或者输出权限信息等等，由于是涉及 jsp 的，我这里不展开讲了，今后的趋势是前后端分离，这些老旧的技术咱不讨论。虽然说这个机制确实挺方便的，但是熟悉前端的同学分分钟就能自己写 js 实现这些功能。会话管理在 Shiro 中有和 Java Web 想类似的会话管理。Shiro 中的会话是不需要基于容器的，而且可以在 Service 层跑，这是两个比较方便的特性。记住我你是否有印象，当访问一下网站登录时，下方会有一个记住我的选项框，如果勾选上之后并登录，第二次打开（一段时间之内）就不需要进行登录了。但是访问一些敏感网页，比如订单，还是需要重新登录以免他人借用你的电脑。事实上由于浏览器提供的密码保存功能，重新登录确实没有必要。所以我们需要记住一点，我们登录了一个系统，那我们要么就是通过身份验证登录的，要么就是通过记住我登录的，两者只取其一。对于记住我的设置也非常简单，只需要在用户的 Subject 采用 login 方法前设置即可，上代码。@PostMapping(value = "/login") @ResponseBody public Response<User> userLogin(@RequestBody User sysUser) { User result; Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(sysUser.getUsrName(), sysUser.getUsrPassword()); token.setRememberMe(true); try { subject.login(token); result = userService.userLogin(sysUser.getUsrName(), sysUser.getUsrPassword()); } catch (UnknownAccountException e) { return getFailResult(404, "Message not found"); } catch (IncorrectCredentialsException e) { return getFailResult(412, "Incorrect message"); } catch (LockedAccountException e) { return getFailResult(401, "Account locked"); } catch (Exception e) { e.printStackTrace(); return getFailResult(408, "Unknown error"); } return getSuccessResult(result); }注意一下我是直接设置为 true，如果小伙伴们希望通过用户在登录时勾选记住我才实现记住我的功能，需要在接口接受参数然后设置。除此之外还要在 ShiroConfiguration 中设置一下一些参数：@Bean public SimpleCookie rememberMeCookie(){ // 取名为rememberMe SimpleCookie simpleCookie = new SimpleCookie("rememberMe"); simpleCookie.setMaxAge(36000); simpleCookie.setHttpOnly(false); return simpleCookie; } @Bean public CookieRememberMeManager rememberMeManager(){ CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager(); // 管理Cookie cookieRememberMeManager.setCookie(rememberMeCookie()); cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag==")); return cookieRememberMeManager; } @Bean public DefaultWebSecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); securityManager.setCacheManager(cacheManager()); // 设置记住我 securityManager.setRememberMeManager(rememberMeManager()); securityManager.setSessionManager(sessionManager()); return securityManager; }设置 Cookie 的寿命为 36000 秒（10小时），设置 HTTPOnly 为 false，这一步很重要，否则前端无法读取 Cookie 数据。然后将其将入 rememberMeManager 里，顺带加个密。最后的最后将其注入 SecurityManager 中就完成了配置。总结经过前一篇的 Shiro 入门，大家已经初步了解了 Shiro 这个框架的基本功能和架构。在这篇进阶版教程中，更加详细的讲了拦截器和认证规则，以及许多我们项目中需要手动进行配置的内容，比如密码加密、权限注解、记住我功能等等。在下一篇 Shiro 系列的文章中，我会利用之前讲过的知识，教大家如何快速整合 Shiro、SSM 和 Vue 项目。如果在阅读的过程中大家觉得我有哪些地方描述的不正确，或者你能够解决文章中我提到的问题，欢迎评论交流！全部原创，真情实感，更新不易，需要动力 ：）
开端好的，起因是我把最近做好的项目给一部分人进行了测试，发现大部分朋友都提出了同一个问题，你的系统权限管理是如何实现的。我只能尴尬的说一句，不好意思这部分还没开发。然而我也知道，其实对于一个项目来说，权限可以说是最主要的一部分。后端除了对权限进行处理，其实也就是提供一些业务逻辑对 CRUD 进行组合拼装。所以我打算接下来学习权限控制方面的知识并整合到我的项目中去，顺便把我的学习笔记分享给大家。而对于权限控制的框架呢，听的最多的还是 Shiro 还有 Spring Security。Spring 的安全框架单独用在 Spring 项目中是无可挑剔的，不管是功能上还是维护方面。但是考虑到 Shiro 是一个全能性的框架，可以用在各种场合，甚至非 Web 项目中，由于它的会话独立于容器，后期学习分布式和微服务的时候也比较方便使用。最重要的是 Spring 官网用的也是 Shiro 的框架。所以还是打算学习 Shiro。由于这部分内容比较多，我也发现了前后端项目部署那篇文章接近 1w 字导致阅读的时候不是很舒服，所以我打算把这个内容分成几个部分：初识 Shiro、配置重点、整合技巧来讲述。这篇文章是关于初识 Shiro 的，主要是 Shiro 的一些基本特性和架构，以及官网上的小例子。 # Shiro 简介Apache 软件基金会开发的 Java 安全（权限）框架。适用于 Java SE 和 Java EE 的环境。主要功能有：认证、授权、加密、会话管理、集成 Web、缓存等。[官网下载]: shiro.apache.org Shiro 具体功能功能翻译解释Authentication身份验证/登录验证用户登录时的身份Authorization权限验证验证用户拥有哪些权限Session Manager会话管理一次登录即一次会话Cryptography加密保护数据安全，密码加密存储Web SupportWeb支持支持方便的Web集成Caching缓存提高查找效率Run As身份替换假装成其他用户(若被允许)Remember Me二次记忆二次登录无需验证初次见面看不懂没有关系的，现在心中有个初步的印象，等到全部学习完整合完你就恍然大悟了。我就简单的讲两个：Authentication首先前两个功能比较容易混淆，到时候写程序千万看清别把方法名写错。Authentication 是身份的验证，我们对于权限的结构划分是基于 RBAC 模型来的（具体可以见我的另一篇博文），所以会有用户、角色、权限三层数据表。而这个功能就是判断用户的角色，是管理员、运维、开发还是测试等等。我们来看一段 Realm 中的方法 doGetAuthenticationInfo：@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; String userName = token.getUsername(); User user = userService.selectByName(userName); return new SimpleAuthenticationInfo( user.getUsrName(), user.getUsrPassword(), getName() ); }这段方法中首先获取用户身份 token 参数，然后获取用户名。使用我自定义的 Mapper 中的 selectByName( ) 在用户数据表中查询数据，最后返回一个 Simple 身份验证信息，参数为用户名、密码和 CachingRealm 的名字。当然正统的方式是把用户对象直接传给他，但是我测试的时候发现传用户名和密码出错的可能性更低，具体可以去看看 SimpleAuthenticationInfo 类的源码。Authorization第二个是用来验证角色所对应的权限。比如说管理员用户拥有增删改查所有权限，游客只拥有查找的权限等等。我们还是通过一段 Java 代码来理解这个功能。@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); String userName = (String)principalCollection.getPrimaryPrincipal(); if (userName == null) { LOGGER.error("授权失败，用户信息为空!"); return null; } try { Set<String> roles = roleService.findRoleByUserName(userName); simpleAuthorizationInfo.addRoles(roles); for (String role : roles) { Set<String> permissions = permissionService.findPermissionByRole(role); simpleAuthorizationInfo.addStringPermissions(permissions); } return simpleAuthorizationInfo; } catch (Exception e) { LOGGER.error("授权失败，系统内部错误!"); } return simpleAuthorizationInfo; }首先 new 一个简单权限验证信息对象（SimpleAuthorizationInfo），使用 getPrimaryPrincipal( ) 方法从函数参数中获取用户名。当用户名存在时，调用自定义的 Mapper 中的 findRoleByUserName( ) 在 sys-user-role 表中找到该用户所对应的角色，这个角色可以是一对多的（我的项目里是一对一的）。之后在简单权限验证信息对象中把角色添加进去。对于每一个角色都赋予了多个权限，所以接下来使用 foreach 循环再次调用 findPermissionByRole( ) 在 sys-role-permission 表中找到每一个角色对应的权限，然后同样加入到简单权限验证信息对象中。至此，获取系统内部角色和权限并把这些信息告诉 Shiro 的步骤就完成了。这个功能中的代码是比较重要的，涉及了用户的数据表和 Mybatis 的操作，因人而异。其它其他功能的话，Session Manager 和 Remember Me 还是比较常用的，但是配置比较简单，照葫芦画瓢就行，不多赘述。Shiro 具体架构架构翻译解释Subject用户即与应用交互的用户Security Manager安全管理管理所有用户(Shiro的心脏)Authenticator用户验证自定义验证Authorizer授权器控制用户不同权限Realm安全实体数据源通过JDBC等实现Cache Manager缓存控制器加速访问Cryptography密码模块密码的加密与解密架构与某些功能相类似，通过这些模块才能实现特定的功能。我也挑两个讲一下：Subject这里的 Subject 不是课程，我第一次看到也以为是课程，这个类和我项目中的 Subject 类竟然一样，import 的时候差点搞错。我们看一段登录接口的代码：@PostMapping(value = "/login") @ResponseBody public Response<User> userLogin(@RequestBody User sysUser) { User result; Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken( sysUser.getUsrName(), sysUser.getUsrPassword() ); token.setRememberMe(true); try { subject.login(token); result = userService.userLogin(sysUser.getUsrName(), sysUser.getUsrPassword()); session.setAttribute("user", subject); } catch (UnknownAccountException e) { return getFailResult(404, "Message not found"); } catch (IncorrectCredentialsException e) { return getFailResult(412, "Incorrect message"); } catch (LockedAccountException e) { return getFailResult(401, "Account locked"); } catch (Exception e) { e.printStackTrace(); return getFailResult(408, "Unknown error"); } return getSuccessResult(result); }这里我们定义了一个 Subject 类的对象，然后使用 SecurityUtils.getSubject( ) 获取当前登录用户的信息。可以 conmand + 单击这个类看一下源码，原来 SecurityUtils 中放了一个 SecurityManager 对象，也就是第二部分的架构，这部分下面再讲。然后我们通过前端获取到的用户名和密码 new 一个 UsernamePasswordToken 类的对象，生成一个 token。最后使用这个唯一的 token 进行 login，对于用户不存在、密码错误、账户锁定、未知错误四种异常进行捕获并反馈。Security Manager刚刚讲到 SecurityUtils 中放了一个 SecurityManager 对象，而 SecurityManager 对象非常的简单，就只包含登录、退出、创建用户三个方法。public interface SecurityManager extends Authenticator, Authorizer, SessionManager { Subject login(Subject var1, AuthenticationToken var2) throws AuthenticationException; void logout(Subject var1); Subject createSubject(SubjectContext var1); }这个架构是 Shiro 的心脏，可以用来管理所有用户。在第三个方法中提供了用户信息上下文的建立与保存，session 和 principals 的建立等等，都是非常重要的内容。Authorizer这个架构其实是先前 Authentication 和 Authorization 的结合，通过用户-角色-权限来管理用户所拥有的不同权限。Realm安全实体数据源又是一个非常重要的内容，我们之前的 doGetAuthenticationInfo( ) 和 doGetAuthorizationInfo( ) 两个自定义获取数据库中的角色和权限信息的方法就是写在这里的，主要使用 JDBC 来操作，用户的授权首先都是要经过这一块关卡的。其它其它功能的话，也不能说不重要，没什么必要讲。就好比密码模块，我暂时还没有用到，涉及到盐值加密（salt）、MD5 码校验等等，比较复杂。其实是我不会所以不讲。还有缓存的话也只要模式化的设定一个缓存即可，如果是在公司一般都有企业内部的缓存方式。官网示例我们从官网把最新的源代码 down 下来或者 clone，研究一下他最简单的实现原理。我们一共需要四部分的文件才能启动一个简单的 helloword 程序。可以在源代码中的 samples 下的 quickstart 中将这四个文件单独拷贝到你的项目中启动。jar 包关于 jar 包如何导入请看 Shiro 整合 SSM 一文。log4j.properties这是一个日志该如何生成和怎样生成的配置文件。# 级别从低到高分别为 DEBUG INFO WARN ERROR FATAL，INFO 代表重要信息 log4j.rootLogger=INFO, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n # General Apache libraries WARN 代表警告信息 log4j.logger.org.apache=WARN # Spring log4j.logger.org.springframework=WARN # Default Shiro logging log4j.logger.org.apache.shiro=INFO # Disable verbose logging log4j.logger.org.apache.shiro.util.ThreadContext=WARN log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN我们可以看到，对于不同的文件产生的输出。shiro.ini这是一个 Shiro 的配置文件，里面设置了用户权限密码等基本信息，只用于测试，一般项目开发不怎么用。[users] # 以下示例配置了5位用户，等号后分别是密码和用户对应的角色，比如管理员、访客等等 # user 'root' with password 'secret' and the 'admin' role root = secret, admin # user 'guest' with the password 'guest' and the 'guest' role guest = guest, guest # user 'presidentskroob' with password '12345' ("That's the same combination on # my luggage!!!" ;)), and role 'president' presidentskroob = 12345, president # user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz' darkhelmet = ludicrousspeed, darklord, schwartz # user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz' lonestarr = vespa, goodguy, schwartz [roles] # 以下示例配置了3种觉得，等号后是他们所拥有的权限，*代表全部，：代表内含权限，而第三种的三个参数分别代表用户角色、用户权限、允许操作的实例号 # 'admin' role has all permissions, indicated by the wildcard '*' admin = * # The 'schwartz' role can do anything (*) with any lightsaber: schwartz = lightsaber:* # The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with # license plate 'eagle5' (instance specific id) goodguy = winnebago:drive:eagle5Quickstart.java类似于 Spring 的 Application，这是一个启动类。在启动类中我们也可以看到 IDEA 报了个错，说 Shiro 的工厂类已经过时，不推荐使用 ini 的形式配置。看一下主函数中一些重要的代码吧：// 通过工厂导入ini配置 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); // 创建单例 SecurityUtils.setSecurityManager(securityManager); // 获取当前的角色 Subject currentUser = SecurityUtils.getSubject(); // 测试session Session session = currentUser.getSession(); session.setAttribute("someKey", "aValue"); // 验证用户是否登录 if (!currentUser.isAuthenticated()) { // 封装用户名密码 UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); token.setRememberMe(true); // 登录成功 try { currentUser.login(token); } // 用户名不存在异常 catch (UnknownAccountException uae) { log.info("There is no user with username of " + token.getPrincipal()); } // 用户名存在，密码错误异常 catch (IncorrectCredentialsException ice) { log.info("Password for account " + token.getPrincipal() + " was incorrect!"); } // 用户锁定异常 catch (LockedAccountException lae) { log.info("The account for username " + token.getPrincipal() + " is locked. " + "Please contact your administrator to unlock it."); } // 其他异常... catch more exceptions here (maybe custom ones specific to your application? catch (AuthenticationException ae) { //unexpected condition? error? } } // 测试是否有该角色 test a role: if (currentUser.hasRole("schwartz")) { log.info("May the Schwartz be with you!"); } else { log.info("Hello, mere mortal."); } // 测试是否有该权限 test a typed permission (not instance-level) if (currentUser.isPermitted("lightsaber:wield")) { log.info("You may use a lightsaber ring. Use it wisely."); } else { log.info("Sorry, lightsaber rings are for schwartz masters only."); }好啦，大家可以自行到官网下载实例本地测试，看源码是一个能够最快理解程序的方式。总结这篇博文从我的项目需要整合 Shiro 开始讲起，主要是写了我在初学 Shiro 时的所感所悟，以及对一些源码的解析。因为初学，可能有些地方理解没到位或者有偏差，请谅解。欢迎评论指正！求求大家别白嫖了，看过打个卡留个言吧！！！全部原创，真情实感，更新不易，需要动力 ：）