注:本文只做流程分享,重点解读,源码已上传github,源码获取方式(左下角阅读原文)
Springboot+Shiro
版本介绍
Springboot: 2.0.5.RELEASE
Java: JDK1.8
Shiro: 1.4.1
MySQL:8.0
本文简介
该例子中使用Springboot集成Shiro的方式展示Shiro 授权,主要涉及技术点,tkmapper、Druid、maven、mysql、lombok
初始化mysql 数据库:init.sql见resources/db/init.sql
引入maven依赖:见pom.xml
修改generator.xml配置文件(涉及数据库连接地址信息以及对应数据表修改)
点击maven插件(mybatis-generator)生成entity,dao,mapper
生成的代码结构如下
到此权限代码的基础就已经搭建好了,使用的用户-角色-权限模型也是经典的权限设计模型
配置application.xml
server: port: 9006 #数据源配置 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource #Druid连接池 url: jdbc:mysql://localhost:3306/springboot-demo?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=true&nullCatalogMeansCurrent=true username: root #数据库用户名 password: root #数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver #mysql驱动 thymeleaf: prefix: classpath:/templates/ suffix: .html mybatis: mapper-locations: classpath:/mapper/*.xml configuration: map-underscore-to-camel-case: true #打印sql log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper: identity: MYSQL # 配置主键自动增长(使用MYSQL原生方式) logging: level: com.tz.springbootshiro: info # 分页插件 pagehelper: reasonable: true page-size-zero: true params: pageNum=start;pageSize=limit support-methods-arguments: true
配置shiroconfig类,配置我们需要的组件
package com.tz.springbootshiro.config; import com.oracle.tools.packager.Log; import com.tz.springbootshiro.realm.MyRealm; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.authc.pam.AuthenticationStrategy; import org.apache.shiro.authc.pam.FirstSuccessfulStrategy; 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.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.apache.shiro.mgt.SecurityManager; import java.util.LinkedHashMap; import java.util.Map; /** * @author tz * @Classname ShiroConfig * @Description shiro 配置 * @Date 2019-11-10 09:23 */ @SpringBootConfiguration public class ShiroConfig { /** * 凭证匹配器 * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了 * 所以我们需要修改下doGetAuthenticationInfo中的代码; * ) * * @return */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); //散列算法:这里使用MD5算法; hashedCredentialsMatcher.setHashAlgorithmName("md5"); //散列的次数,比如散列两次,相当于 md5(md5("")); hashedCredentialsMatcher.setHashIterations(2); // 是否存储为16进制 hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true); return hashedCredentialsMatcher; } /** * Shiro默认提供了三种 AuthenticationStrategy 实现: * AtLeastOneSuccessfulStrategy :其中一个通过则成功。 * FirstSuccessfulStrategy :其中一个通过则成功,但只返回第一个通过的Realm提供的验证信息。 * AllSuccessfulStrategy :凡是配置到应用中的Realm都必须全部通过。 * authenticationStrategy * * @return */ @Bean(name = "authenticationStrategy") public AuthenticationStrategy authenticationStrategy() { Log.info("----------->>>>>>>>>>>>ShiroConfiguration.authenticationStrategy()"); return new FirstSuccessfulStrategy(); } /** * 安全管理器,并把realm注入 * @return */ @Bean public DefaultWebSecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myRealm()); return securityManager; } /** * 开启shiro aop注解支持. * 使用代理方式;所以需要开启代码支持; * @RequiresPermissions("topic:list") * @RequiresPermissions * @RequiresRoles * @RequiresUser * @RequiresGuest * @RequiresAuthentication * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } @Bean @ConditionalOnMissingBean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setProxyTargetClass(true); return defaultAdvisorAutoProxyCreator; } @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setSuccessUrl("/"); shiroFilterFactoryBean.setUnauthorizedUrl("/unauth"); //注意此处使用的是LinkedHashMap,是有顺序的,shiro会按从上到下的顺序匹配验证,匹配了就不再继续验证 //所以上面的url要苛刻,宽松的url要放在下面,尤其是"/**"要放到最下面,如果放前面的话其后的验证规则就没作用了。 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/static/**/**", "anon"); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/captcha.jpg", "anon"); filterChainDefinitionMap.put("/favicon.ico", "anon"); filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * 自定义realm */ @Bean public MyRealm myRealm(){ MyRealm userRealm = new MyRealm(); userRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return userRealm; } }
自定义Realm 授权验证使用代码
package com.tz.springbootshiro.realm; import com.tz.springbootshiro.bean.SysUser; import com.tz.springbootshiro.service.SysPermissionService; import com.tz.springbootshiro.service.SysUserService; import lombok.extern.slf4j.Slf4j; 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.realm.Realm; import org.apache.shiro.realm.text.TextConfigurationRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import java.util.List; /** * @author tz * @Classname MyRealm * @Description * @Date 2019-11-09 11:44 */ @Slf4j public class MyRealm extends AuthorizingRealm { @Autowired private SysUserService sysUserService; @Autowired private SysPermissionService sysPermissionService; /** * 授权 * * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SysUser sysUser = (SysUser) principals.getPrimaryPrincipal(); List<String> sysPermissions = sysPermissionService.selectPermissionByUserId(sysUser.getUserId()); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addStringPermissions(sysPermissions); log.info("------------>>>>>>>>>>>>>>>>>>>>>>>>>>>>doGetAuthorizationInfo"); return info; } /** * 认证 * * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; SysUser sysUser = sysUserService.findByUserName(token.getUsername()); if (sysUser == null) { return null; } log.info("------------>>>>>>>>>>>>>>>>>>>>>>>>>>>>doGetAuthenticationInfo"); return new SimpleAuthenticationInfo(sysUser, sysUser.getPassword().toCharArray(), ByteSource.Util.bytes(sysUser.getSalt()), getName()); } /** * 测试realm * * @return */ @Bean public Realm realm() { TextConfigurationRealm realm = new TextConfigurationRealm(); realm.setUserDefinitions("joe.coder=password,user\n" + "jill.coder=password,admin"); realm.setRoleDefinitions("admin=read,write\n" + "user=read"); realm.setCachingEnabled(true); return realm; } }
- 到此基础配置就结束了,对应的数据库实体类相关关系类可在github查找,下面掩饰结果,首先分析拦截url
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setSuccessUrl("/"); shiroFilterFactoryBean.setUnauthorizedUrl("/unauth"); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/static/**/**", "anon"); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/captcha.jpg", "anon"); filterChainDefinitionMap.put("/favicon.ico", "anon"); filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
上方代码,我们在shiroconfig配置中指定的,登录url为/login,此处为get请求,找到controller中我们的/login的地址进行跳转,此处需要注意的是我们的controller不能返回json体,即不能加入RequestBody注解,登录成功的url为/,此处没有该url所以代码不会跳转,我们实际项目中可以把此改为我们登录成功之后跳转的url,setUnauthorizedUrl配置我们未授权页面的url,然后下面配置我门不需要拦截的url,最后加上/**的拦截验证,url依次往下,注意顺序,这个拦截全部一定要放在最后,不然其他配置就不生效,下面放上我controller的代码
package com.tz.springbootshiro.controller; import com.tz.springbootshiro.common.CodeMsg; import com.tz.springbootshiro.common.ResultBean; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.subject.Subject; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; /** * @author tz * @Classname LoginController * @Description 登录 * @Date 2019-11-10 09:52 */ @Controller public class LoginController { /** * shiro 跳转登录页面使用 * @return */ @RequestMapping(value = "/login", method = RequestMethod.GET) public String defaultLogin() { return "/login/login"; } /** * 登录页面表单提交使用 * @param username * @param password * @return */ @ResponseBody @PostMapping(value = "/login") public ResultBean login(@RequestParam("userName") String username, @RequestParam("password") String password) { // 从SecurityUtils里边创建一个 subject Subject subject = SecurityUtils.getSubject(); // 在认证提交前准备 token(令牌) UsernamePasswordToken token = new UsernamePasswordToken(username, password); // 执行认证登陆 try { subject.login(token); } catch (UnknownAccountException uae) { return new ResultBean(CodeMsg.ACCOUNT_NOT_FOUND); } catch (IncorrectCredentialsException ice) { return new ResultBean(CodeMsg.PASSWORD_ERROR); } catch (LockedAccountException lae) { return new ResultBean(CodeMsg.ACCOUNT_LOCK); } catch (ExcessiveAttemptsException eae) { return new ResultBean(CodeMsg.ERROR_NUM_MORE); } catch (AuthenticationException ae) { return new ResultBean(CodeMsg.USERNAME_PASSWORD_ERROR); } if (subject.isAuthenticated()) { return new ResultBean(CodeMsg.LOGIN_SUCCESS); } else { token.clear(); return new ResultBean(CodeMsg.LOGIN_ERROR); } } }
ok,教程到此结束,纯干货,源码----->阅读原文