更新时间:2020/9/8 10:15,更新到了springboot整合 更新时间:2020/9/3 16:14,更新到了入门的第三个案例
本文主要对shiro的学习总结,shiro的功能跟security的功能是类似的,据网上的说法,shiro相对于security较轻量级一点,但security功能较多。本文会持续更新,不断地扩充
本文仅为记录学习轨迹,如有侵权,联系删除
Apache Shiro 是 Java 的一个安全框架。目前,使用 Apache Shiro 的人越来越多,因为它相当简单,对比 Spring Security,可能没有 Spring Security 做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。
这是shiro的官网给出的一张核心架构图,包括认证、授权和加密等
模块说明Authentication身份认证 / 登录,验证用户是不是拥有相应的身份;Authorization授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;Session Management会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;Session Dao对会话数据进行curd操作,操作Session Management中数据Cryptography加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;Caching Manager缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;Pluggable Realms可以自定义realm,获取用户认证和授权的相应数据,以完成认证和授权密码存储在数据库中,一般不会明文存储,大多都是经过加密的,而MD5算法就是其中的一种加密的方式,MD5除了可以进行加密之外,还可以用来做签名,也就是校验,相同的内容经过MD5加密后,生成的结果都是一样的,所以它还可以用来判断多份数据是否一致,数据有没有经过别人纂改等,另外MD5加密是不可逆的,生成的结果始终是一个16进制的32位长度的字符串。 MD5加密还可以加上一个盐值,并且可以指定hash散列次数,这样的话就更安全,盐值就是随机生成一个字符串,加在要加密的密码上面,载进行加密,这样会跟安全,当然,这个盐值在校验的时候要加上,并且这个盐值不能暴露,同时还可以进行hash散列,这样就更安全了。
这里使用maven项目,需要引入shiro的依赖
<!--shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.6.0</version> </dependency>主要代码
public class MD5 { public static void main(String[] args) { //默认的MD5加密,默认hash散列一次 Md5Hash md5Hash1 = new Md5Hash("123"); String s1 = md5Hash1.toHex(); System.out.println("默认的MD5加密 => "+s1); //MD5 + salt Md5Hash md5Hash2 = new Md5Hash("123", "q1v*(%");//默认salt加在123前面 String s2 = md5Hash2.toHex(); System.out.println("MD5+salt => "+s2); //MD5 + salt + hash散列 Md5Hash md5Hash3 = new Md5Hash("123","q1v*(%",1024); String s3 = md5Hash3.toHex(); System.out.println("MD5 + salt + hash => "+s3); } }shiro支持单机应用和web应用,并且提供有相应的坐标依赖
新建一个maven项目,总体结构如图
并且引入相应的坐标依赖
<!--shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.6.0</version> </dependency>配置用户名和密码,这里用shiro.ini配置文件进行配置,实际开发要使用查看数据库
[users] zs=123 ls=456 ww=789实现的代码
public class Shiro01 { public static void main(String[] args) { //1.创建安全管理器对象 DefaultSecurityManager securityManager = new DefaultSecurityManager(); //2.给安全管理器设置realm securityManager.setRealm(new IniRealm("classpath:shiro.ini")); //3.securityManager给全局安全工具类设置安全管理器 SecurityUtils.setSecurityManager(securityManager); //4.关键对象 subject 主体 Subject subject = SecurityUtils.getSubject(); //5.创建令牌 UsernamePasswordToken token = new UsernamePasswordToken(); token.setUsername("z1");//用户名 token.setPassword("123".toCharArray());//密码 //6.用户认证 try { System.out.println("认证前状态:"+subject.isAuthenticated()); subject.login(token); System.out.println("认证后状态:"+subject.isAuthenticated()); } catch (UnknownAccountException e){ System.out.println("认证失败:用户不存在"); e.printStackTrace(); }catch (IncorrectCredentialsException e){ System.out.println("认证失败:密码错误"); e.printStackTrace(); }catch (AuthenticationException e) { e.printStackTrace(); } } }验证通过 验证未通过,验证不存在的用户
这边采用三种加密方式,MD5、MD5+Salt和MD5+Salt+Hash,首先需要创建一个自定义realm,用于用户的验证与授权
public class CustomerRealm extends AuthorizingRealm { //授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } //认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //获取身份信息 String principal = (String) token.getPrincipal(); //根据用户名查询数据库 if("zs".equals(principal)){ /**没有加盐salt的MD5:202cb962ac59075b964b07152d234b70**/ //return new SimpleAuthenticationInfo(principal,"202cb962ac59075b964b07152d234b70",this.getName()); /**md5+salt:243230d5403e2690008308353bd46f7e**/ // return new SimpleAuthenticationInfo( // principal,//数据库查询到的用户名 // "243230d5403e2690008308353bd46f7e",//数据库MD5+salt之后的密码 // ByteSource.Util.bytes("q1v*(%"),//注册时的随机salt // this.getName()//realm的名字 // ); /**md5+salt+hash:99c93a793708b521548aca28a226fd5b**/ return new SimpleAuthenticationInfo( principal,//数据库查询到的用户名 "99c93a793708b521548aca28a226fd5b",//数据库MD5+salt之后的密码 ByteSource.Util.bytes("q1v*(%"),//注册时的随机salt this.getName()//realm的名字 ); } return null; } }编写测试类
public class shiro02 { public static void main(String[] args) { //1.创建安全管理器对象 DefaultSecurityManager securityManager = new DefaultSecurityManager(); //2.给安全管理器设置realm CustomerRealm customerRealm = new CustomerRealm(); //注入自定义realm securityManager.setRealm(customerRealm); //设置realm使用hash凭证匹配器 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); credentialsMatcher.setHashAlgorithmName("md5");//使用的加密算法 credentialsMatcher.setHashIterations(1024);//散列次数 customerRealm.setCredentialsMatcher(credentialsMatcher); //3.securityManager给全局安全工具类设置安全管理器 SecurityUtils.setSecurityManager(securityManager); //4.关键对象 subject 主体 Subject subject = SecurityUtils.getSubject(); //5.创建令牌 UsernamePasswordToken token = new UsernamePasswordToken(); token.setUsername("zs");//用户名 token.setPassword("123".toCharArray());//密码 //6.用户认证 try { System.out.println("认证前状态:"+subject.isAuthenticated()); subject.login(token); System.out.println("认证后状态:"+subject.isAuthenticated()); System.out.println("登录成功"); } catch (UnknownAccountException e){ System.out.println("认证失败:用户不存在"); e.printStackTrace(); }catch (IncorrectCredentialsException e){ System.out.println("认证失败:密码错误"); e.printStackTrace(); }catch (AuthenticationException e) { e.printStackTrace(); } } }shiro02 代码用了MD5+Salt+Hash,如果只需要用到MD5,则可以将salt和hash部分注释掉
用户的授权和认证都是在自定义的realm类中实现的,这里接着上面的(2)的案例,进行权限的授权,首先是自定义的realm类
public class CustomerRealm extends AuthorizingRealm { //授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String primaryPrincipal = (String) principals.getPrimaryPrincipal(); System.out.println("身份信息:"+primaryPrincipal); //根据身份信息、用户名、获取当前用户的角色信息,以及权限信息 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); //将数据库中查询到的角色信息赋值给权限对象 simpleAuthorizationInfo.addRole("admin"); simpleAuthorizationInfo.addRole("user"); //将数据库中查询的权限信息赋值给权限对象 simpleAuthorizationInfo.addStringPermission("user:*:01"); simpleAuthorizationInfo.addStringPermission("product:create:*"); return simpleAuthorizationInfo; } //认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //获取身份信息 String principal = (String) token.getPrincipal(); //根据用户名查询数据库 if("zs".equals(principal)){ /**没有加盐salt的MD5:202cb962ac59075b964b07152d234b70**/ //return new SimpleAuthenticationInfo(principal,"202cb962ac59075b964b07152d234b70",this.getName()); /**md5+salt:243230d5403e2690008308353bd46f7e**/ // return new SimpleAuthenticationInfo( // principal,//数据库查询到的用户名 // "243230d5403e2690008308353bd46f7e",//数据库MD5+salt之后的密码 // ByteSource.Util.bytes("q1v*(%"),//注册时的随机salt // this.getName()//realm的名字 // ); /**md5+salt+hash:99c93a793708b521548aca28a226fd5b**/ return new SimpleAuthenticationInfo( principal,//数据库查询到的用户名 "99c93a793708b521548aca28a226fd5b",//数据库MD5+salt之后的密码 ByteSource.Util.bytes("q1v*(%"),//注册时的随机salt this.getName()//realm的名字 ); } return null; } }测试用户授权
public class shiro02 { public static void main(String[] args) { //1.创建安全管理器对象 DefaultSecurityManager securityManager = new DefaultSecurityManager(); //2.给安全管理器设置realm CustomerRealm customerRealm = new CustomerRealm(); //注入自定义realm securityManager.setRealm(customerRealm); //设置realm使用hash凭证匹配器 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); credentialsMatcher.setHashAlgorithmName("md5");//使用的加密算法 credentialsMatcher.setHashIterations(1024);//散列次数 customerRealm.setCredentialsMatcher(credentialsMatcher); //3.securityManager给全局安全工具类设置安全管理器 SecurityUtils.setSecurityManager(securityManager); //4.关键对象 subject 主体 Subject subject = SecurityUtils.getSubject(); //5.创建令牌 UsernamePasswordToken token = new UsernamePasswordToken(); token.setUsername("zs");//用户名 token.setPassword("123".toCharArray());//密码 //6.用户认证 try { System.out.println("认证前状态:"+subject.isAuthenticated()); subject.login(token); System.out.println("认证后状态:"+subject.isAuthenticated()); System.out.println("登录成功"); } catch (UnknownAccountException e){ System.out.println("认证失败:用户不存在"); e.printStackTrace(); }catch (IncorrectCredentialsException e){ System.out.println("认证失败:密码错误"); e.printStackTrace(); }catch (AuthenticationException e) { e.printStackTrace(); } //用户授权 if(subject.isAuthenticated()){ //基于角色权限控制 System.out.println(subject.hasRole("admin")); //基于多角色权限控制,只有同时满足里面所有权限后才能访问 System.out.println(subject.hasAllRoles(Arrays.asList("admin", "user"))); //是否具有其中的一个角色 boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "super", "user")); for (boolean a : booleans) { System.out.println(a); } //基于权限字符串的访问控制 资源标识符:操作:资源类型 System.out.println("权限:"+subject.isPermitted("user:update:01")); System.out.println("权限:"+subject.isPermitted("product:create:02")); //同时具有哪些权限 System.out.println(subject.isPermittedAll("user:*:01", "product:create:01")); } } }首先引入依赖
<!--shiro--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.2</version> </dependency>配置类ShiroConfig ,实现了拦截,所有的请求进来需要认证,里面需要写3个方法,ShiroFilterFactoryBean,DefaultWebSecurityManager和自定义Realm类,自定义Realm类在外面定义,然后在这个配置类里面new出来即可,DefaultWebSecurityManager需要使用自定义Realm类,ShiroFilterFactoryBean需要使用DefaultWebSecurityManager,一环套一环;然后里面有DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor,这两个用于支持开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
/** * @ClassName : ShiroConfig * @Description : Shiro配置类 * @Author : CJH * @Date: 2020-09-02 09:35 */ @Configuration public class ShiroConfig { /** * ShiroFilterFactoryBean * * @param securityManager * @return */ @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); //关联安全管理器(DefaultWebSecurityManager) bean.setSecurityManager(securityManager); /** * 添加shiro的内置过滤器 * anon:无需认证就可以访问 * anthc:必须认证了才能访问 * user:必须拥有记住我功能才能用 * perms:拥有对某个资源的权限才能访问 * role:拥有对某个角色权限才能访问 */ Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/login", "anon"); filterMap.put("/register", "anon"); filterMap.put("/login_page", "anon"); filterMap.put("/auth/logout", "logout");//注销登录操作 //拦截所有的这一行必须放在最后 filterMap.put("/**", "authc"); bean.setFilterChainDefinitionMap(filterMap);//添加过滤器 bean.setLoginUrl("/login_page");//未登录时的跳转路径 return bean; } /** * DefaultWebSecurityManager * * @param customerRealm * @return */ @Bean(name = "securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("customerRealm") CustomerRealm customerRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //关联UserRealm securityManager.setRealm(customerRealm); return securityManager; } /** * 创建自定义realm类,自定义UserRealm * * @return */ @Bean(name = "customerRealm") public CustomerRealm customerRealm() { //修改凭证校验匹配器 HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("MD5");//md5算法 hashedCredentialsMatcher.setHashIterations(1024);//散列次数 //应用凭证校验匹配器 CustomerRealm customerRealm = new CustomerRealm(); customerRealm.setCredentialsMatcher(hashedCredentialsMatcher); return customerRealm; } /** * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions) * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能 * * @return */ @Bean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } }自定义Realm类,该类用于实现用户的授权和认证
/** * @ClassName : UserRealm * @Description : shiro自定义的UserRealm类 * @Author : CJH * @Date: 2020-09-02 09:45 */ @Slf4j public class CustomerRealm extends AuthorizingRealm { @Autowired private UserServer userServer; @Autowired private RoleServer roleServer; @Autowired private PermsServer permsServer; //授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { log.info("授权"); String username = (String) principalCollection.getPrimaryPrincipal(); //添加用户权限 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); List<Role> roles = roleServer.queryRoleByUserName(username);//根据用户名称查询对应用户权限 //依次添加权限 for (Role role:roles){ //角色信息添加 simpleAuthorizationInfo.addRole(role.getName()); List<Perms> perms = permsServer.queryPermsByRoleId(role.getId()); //权限信息添加 for(Perms perm:perms){ simpleAuthorizationInfo.addStringPermission(perm.getName()); } } return simpleAuthorizationInfo; } //认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { log.info("认证"); /**用户和密码认证**/ //用户名认证 UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;//拿到token List<User> users = userServer.queryUserByName(userToken.getUsername()); Assert.notEmpty(users,"用户不存在"); //密码认证,shiro自动处理 return new SimpleAuthenticationInfo( users.get(0).getName(), users.get(0).getPassword(), ByteSource.Util.bytes(users.get(0).getSalt()), this.getName()); } }编写测试控制器进行控制
@RestController @RequestMapping("/test") @RequiresRoles(value = {"test","admin"},logical = Logical.OR) public class TestController { @RequestMapping("/add") public String add(){ return "this is test_add"; } @RequestMapping("/update") public String update(){ return "this is test_update"; } @RequestMapping("/delete") public String delete(){ return "this is test_delete"; } @RequestMapping("/query") public String query(){ return "this is test_query"; } } /** * @ClassName : UserController * @Description : 用户控制器 * @Author : CJH * @Date: 2020-09-02 20:41 */ @RestController @RequestMapping("/user") @RequiresRoles(value = {"user","admin"},logical = Logical.OR)//同时满足value里面所有的权限才能访问 public class UserController { @RequestMapping("/add") @RequiresPermissions("user:add") public String add(){ return "this is user_add"; } @RequestMapping("/update") @RequiresPermissions("user:update") public String update(){ return "this is user_update"; } @RequestMapping("/delete") @RequiresPermissions("user:delete") public String delete(){ return "this is user_delete"; } @RequestMapping("/query") @RequiresPermissions("user:query") public String query(){ return "this is user_query"; } }两个控制器用@RequiresRoles注解进行角色授权,@RequiresPermissions进行资源授权,只有对应的资源才有权限访问。
授权的方式有基于角色授权认证和基于资源授权认证,以上面的UserController为例,@RequiresRoles只有具有user角色的人才能访问,在该类里面,@RequiresPermissions只有具有对应的资源权限才能访问对应的接口,即接口级别的权限控制
配置了上面的认证与授权之后发现,每次发起请求的时候,系统都会去数据库查询认证和授权信息,这样会影响效率,解决方案之一就是加个缓存,这样用户只有第一次发请求的时候需要查询数据,之后的验证信息都直接去查缓存
添加pom依赖
<!--shiro的ehcache缓存--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.6.0</version> </dependency>在配置类ShiroConfig里面的自定义reaml方法中增加缓存管理,下面给出整个完整的配置类
/** * @ClassName : ShiroConfig * @Description : Shiro配置类 * @Author : CJH * @Date: 2020-09-02 09:35 */ @Configuration public class ShiroConfig { /** * ShiroFilterFactoryBean * @param securityManager * @return */ @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){ ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); //关联安全管理器(DefaultWebSecurityManager) bean.setSecurityManager(securityManager); /** * 添加shiro的内置过滤器 * anon:无需认证就可以访问 * anthc:必须认证了才能访问 * user:必须拥有记住我功能才能用 * perms:拥有对某个资源的权限才能访问 * role:拥有对某个角色权限才能访问 */ Map<String,String> filterMap = new LinkedHashMap<>(); filterMap.put("/login","anon"); filterMap.put("/register","anon"); filterMap.put("/login_page","anon"); filterMap.put("/**","authc"); bean.setFilterChainDefinitionMap(filterMap);//添加过滤器 bean.setLoginUrl("/login_page");//未登录时的跳转路径 return bean; } /** * DefaultWebSecurityManager * @param customerRealm * @return */ @Bean(name = "securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("customerRealm") CustomerRealm customerRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //关联UserRealm securityManager.setRealm(customerRealm); return securityManager; } /** * 创建自定义realm类,自定义UserRealm * @return */ @Bean(name = "customerRealm") public CustomerRealm customerRealm(){ //修改凭证校验匹配器 HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("MD5");//md5算法 hashedCredentialsMatcher.setHashIterations(1024);//散列次数 CustomerRealm customerRealm = new CustomerRealm(); //应用凭证校验匹配器 customerRealm.setCredentialsMatcher(hashedCredentialsMatcher); //开启缓存 customerRealm.setCacheManager(new EhCacheManager()); customerRealm.setCachingEnabled(true); customerRealm.setAuthenticationCachingEnabled(true);//开启认证缓存 customerRealm.setAuthenticationCacheName("authenticationCache");//给认证缓存起个名字 customerRealm.setAuthorizationCachingEnabled(true);//开启授权缓存 customerRealm.setAuthorizationCacheName("authorizationCache");//给授权缓存起个名字 return customerRealm; } /** * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions) * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能 * @return */ @Bean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } }