Springboot整合Shiro安全框架
最近在学习Springboot,在这个过程中遇到了很多之前都没有技术知识,学习了一阵子,稍微总结一些。
---- Shiro框架
shiro框架,是一个相对比较简便的安全框架,它可以干净利落地处理身份验证、授权、企业会话管理和加密。
在此引入网上众多资料,归总shiro大致框架如下:
- Subject: 主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者
- Shiro SecurityManager: 管理所有Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞.
- Realm: 用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)
在主要开发上,我将Shiro的实际应用归结成两大块,其一是关于shiro的一些配置信息,这里称为ShiroConfig文件,由于Apache Shiro 核心通过 Filter 来实现,既然是使用 Filter 一般也就能猜到,是通过 URL 规则来进行过滤和权限校验,所以我们需要定义一系列关于 URL 的规则和访问权限,以及一些Shiro自身具备的一些Bean工具。其二是关于我们自己需要定义的Realm类,这里称为ShiroRealm文件,继承AuthorizingRealm 抽象类,重载 doGetAuthenticationInfo(),重写获取用户信息的方法,重写doGetAuthorizationInfo(),进行角色权限的配置。
---?ShiroConfig
package springbootshiro2.demo.configurer; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.cache.ehcache.EhCacheManager; import org.apache.shiro.codec.Base64; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.CookieRememberMeManager; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.mgt.WebSecurityManager; import org.apache.shiro.web.servlet.SimpleCookie; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.LinkedHashMap; import java.util.Map; /** * 〈一句话功能简述〉<br> * 〈〉 * * @author X450J * @create 2019/5/30 * @since 1.0.0 */ @Configuration public class ShiroConf { //注入shiro过滤器 @Bean("shiroFilterFactoryBean") public ShiroFilterFactoryBean shiroFiltr(WebSecurityManager securityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //设置管理器 shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/login"); // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面 shiroFilterFactoryBean.setSuccessUrl("/index");// 登录成功后要跳转的链接 shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");//设置无权限跳转页面 Map<String,String> chains = new LinkedHashMap<>(); chains.put("/logout","logout"); chains.put("/login", "anon");//anon表示可以匿名访问 chains.put("/login/qq", "anon");//anon表示可以匿名访问 chains.put("/authorize/qq", "anon");//anon表示可以匿名访问 //对PermissionAction.class 中的url进行权限控制 chains.put("/user", "roles[user]");//需要user角色才可以访问 chains.put("/user/per", "perms[user:query]");//需要user角色才可以访问 chains.put("/admin", "roles[admin]");//需要admin角色才可以访问 //chains.put("/**", "authc");//表示需要认证,才能访问 chains.put("/**", "user");//表示需要认证或记a住我都能访问 shiroFilterFactoryBean.setFilterChainDefinitionMap(chains); return shiroFilterFactoryBean; } //安全管理器 @Bean public WebSecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //设置realm securityManager.setRealm(shiroRealm()); securityManager.setCacheManager(ehCacheManager()); securityManager.setRememberMeManager(rememberMeManager()); return securityManager; } //会话管理器 @Bean public SessionManager sessionManager(){ DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionIdUrlRewritingEnabled(true); sessionManager.setGlobalSessionTimeout(1 * 60 * 60 * 1000); sessionManager.setDeleteInvalidSessions(true); sessionManager.setSessionIdCookie(rememberMeCookie()); return sessionManager; } //Realm,里面需要自己实现认证和授权业务 @Bean public ShiroRealm shiroRealm() { ShiroRealm shiroRealm = new ShiroRealm(); shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return shiroRealm; } //缓存管理 @Bean public EhCacheManager ehCacheManager(){ EhCacheManager ehCacheManager = new EhCacheManager(); ehCacheManager.setCacheManagerConfigFile("classpath:ehcache.xml"); return ehCacheManager; } //密码管理 @Bean public HashedCredentialsMatcher hashedCredentialsMatcher(){ HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); credentialsMatcher.setHashAlgorithmName("md5"); credentialsMatcher.setHashIterations(2); //加密两次 credentialsMatcher.setStoredCredentialsHexEncoded(true); //启用十六进制 return credentialsMatcher; } //cookie管理 @Bean public SimpleCookie rememberMeCookie() { SimpleCookie cookie = new SimpleCookie("rememberMe"); cookie.setHttpOnly(true); cookie.setMaxAge(1 * 60 * 60); return cookie; } //管理shiro的生命周期 @Bean(name = "lifecycleBeanPostProcessor") public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } //记住我 @Bean public CookieRememberMeManager rememberMeManager() { CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager(); cookieRememberMeManager.setCookie(rememberMeCookie()); //不是所有的base64编码都可以用,长度过大过小都不行 cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag==")); return cookieRememberMeManager; } //开启shiro注解权限控制 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor attributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); attributeSourceAdvisor.setSecurityManager(securityManager); return attributeSourcteAdvisor; } }
- LifecycleBeanPostProcessor:它管理Shiro的生命周期。
- ShiroFilterFactoryBean:采用了工厂设计模式,负责引入SecurityManager、登录的页面url设置、登录后要跳转的url设置、未授权的页面的url设置、拦截器链的设置。
- SecurityManager:Shiro的核心,认证器、缓存器、Realm数据源都在这里配置注入
- HashedCredentialsMatcher:Shiro自带密码器,这里用了MD5加密算法,并且还将其注入到自定义的ShiroRealm之中。
- CacheManager: 缓存管理器,这里引入了Ehcache框架来实现缓存,接着再一起注入安全管理器(SecurityManager)
- AuthorizationAttributeSourceAdvisor: 配置生效后,可以开启shiro的注解权限控制
- CookieRememberMeManager: 顾名思义,负责实现RememberMe功能的,使得浏览器请求页面时可以通过cookies判断用户而无需登录
- SessionManager: 关于session会话的一些管理
---?ShiroRealm
package springbootshiro2.demo.configurer; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.crypto.hash.SimpleHash; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import springbootshiro2.demo.model.User; /** * 〈一句话功能简述〉<br> * 〈自定义Realm〉 * * @author X450J * @create 2019/5/30 * @since 1.0.0 */ public class ShiroRealm extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //给当前角色授权 SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); User user = (User) principalCollection.getPrimaryPrincipal(); //在这里 实际开发应该从数据库进行获取 if (user.getUsername().equals("xslde")){ //设置该用户拥有user角色 authorizationInfo.addRole("user"); //设置该用户拥有query权限 authorizationInfo.addStringPermission("user:query"); } if(user.getUsername().equals("admin")){ //admin用户拥有admin、user角色 authorizationInfo.addRole("admin"); authorizationInfo.addRole("user"); //设置该用户拥有query权限 authorizationInfo.addStringPermission("user:query"); } return authorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //进行用户信息获取 String username = (String)token.getPrincipal(); //开发中,这里都是去数据库查询 if (!"xslde".equals(username)&&!"test".equals(username)&&!"xslde.com".equals(username)&&!"admin".equals(username)){ throw new UnknownAccountException("用户不存在!"); } User user = null; if ("xslde".equals(username)){ user = new User(); user.setUsername("xslde"); user.setPassword("0caf568dbf30f5c33a13c56b869259fc"); user.setSalt("abcd"); user.setAvailable(1); } if ("admin".equals(username)){ user = new User(); user.setUsername("admin"); user.setPassword("0caf568dbf30f5c33a13c56b869259fc"); user.setSalt("abcd"); user.setAvailable(1); } if ("test".equals(username)){ user = new User(); user.setUsername("test"); user.setPassword("0caf568dbf30f5c33a13c56b869259fc"); user.setSalt("abcd"); user.setAvailable(0); } if (user.getAvailable()!=1){ throw new LockedAccountException("账户已被锁定"); } return new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName()); } //生成一个加盐密码 public static void main(String[] args){ //加密类型 String hashAlgorithmName = "md5"; //迭代次数 Integer count = 2; String password = "123456"; String salt = "abcd"; String s = new SimpleHash(hashAlgorithmName,password,salt,count).toHex(); System.out.println(s); } }
小结两点:第一是理清数据之间关系,即一个用户可以拥有多个角色,而一个角色可以具备多种权限
第二是理解关于盐的事情,因为这里用的密码虽然已经通过MD5算法进行加密,但是假如别人是知道这个系统用的是这个加密的算法,那么很容易就通过对应的密码方式进 行破解,所以引入盐的概念,即生活中一道相同的菜在不同人的手里所下的盐的量都会有所区别,借助这个道理,因为我们上面已经在Realm中配置过加密工具了,所以这里不加盐值的话会默认使用"MD5"算法(上面配置加密工具时设置)进行密码加密,如果加了盐值,在算法加密之后会再进行盐值加密过程。
之后在controller层中,进行如下的配置
package springbootshiro2.demo.action; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.apache.shiro.authz.annotation.RequiresRoles; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import java.util.Map; /** * 〈一句话功能简述〉<br> * 〈〉 * * @author X450J * @create 2019/5/30 * @since 1.0.0 */ @Controller public class PermissionAction { @GetMapping("/unauthorized") public String unauthorized(){ return "unauthorized.html"; } //拥有user角色才可访问 @GetMapping("/user") public String user(Map<String,String> code){ code.put("msg","拥有user角色"); return "user.html"; } //拥有user角色才可访问和user:query权限才可访问 @GetMapping("/user/per") public String userPer(Map<String,String> mode){ mode.put("msg","拥有user角色和user:query权限"); return "user.html"; } //拥有admin角色才可访问 @GetMapping("/admin") public String admin(Map<String,String> mode){ mode.put("msg","拥有admin角色"); return "admin.html"; } //通过注解控制授权 @RequiresRoles({"admin"}) @GetMapping("/abc") public String abc(Map<String,String> mode){ mode.put("msg","拥有admin角色,并且是通过注解控制"); return "admin.html"; } //通过注解控制权限 @RequiresPermissions({"user:query"}) @GetMapping("/abcd") public String abcd(Map<String,String> mode){ mode.put("msg","拥有user:query权限,并且是通过注解控制"); return "user.html"; } }