spring boot 集成 shiro 自定义密码验证 自定义freemarker标签根据权限渲染不同页面(推荐
项目里一直用的是spring-security,不得不说,spring-security真是东西太多了,学习难度太大(可能我比较菜),这篇博客来总结一下折腾shiro的成果,分享给大家,强烈推荐shiro,真心简单:)
引入依赖
org.apache.shiro shiro-spring 1.4.0
用户,角色,权限
就是经典的RBAC权限系统,下面简单给一下实体类字段
AdminUser.java
publicclassAdminUserimplementsSerializable{ privatestaticfinallongserialVersionUID=8264158018518861440L; privateIntegerid; privateStringusername; privateStringpassword; privateIntegerroleId; //gettersetter... }
Role.java
publicclassRoleimplementsSerializable{ privatestaticfinallongserialVersionUID=7824693669858106664L; privateIntegerid; privateStringname; //gettersetter... }
Permission.java
publicclassPermissionimplementsSerializable{ privatestaticfinallongserialVersionUID=-2694960432845360318L; privateIntegerid; privateStringname; privateStringvalue; //权限的父节点的id privateIntegerpid; //gettersetter... }
自定义Realm
这货就是查询用户的信息然后放在shiro的个人用户对象的缓存里,shiro自己有一个session的对象(不是servlet里的session)作用就是后面用户发起请求的时候拿来判断有没有权限
另一个作用是查询一下用户的信息,将用户名,密码组装成一个AuthenticationInfo用于后面密码校验的
具体代码如下
MyShiroRealm.java
@Component publicclassMyShiroRealmextendsAuthorizingRealm{ privateLoggerlog=LoggerFactory.getLogger(MyShiroRealm.class); @Autowired privateAdminUserServiceadminUserService; @Autowired privateRoleServiceroleService; @Autowired privatePermissionServicepermissionService; //用户权限配置 @Override protectedAuthorizationInfodoGetAuthorizationInfo(PrincipalCollectionprincipals){ //访问@RequirePermission注解的url时触发 SimpleAuthorizationInfosimpleAuthorizationInfo=newSimpleAuthorizationInfo(); AdminUseradminUser=adminUserService.selectByUsername(principals.toString()); //获得用户的角色,及权限进行绑定 Rolerole=roleService.selectById(adminUser.getRoleId()); //其实这里也可以不要权限那个类了,直接用角色这个类来做鉴权, //不过角色包含很多的权限,已经算是大家约定的了,所以下面还是查询权限然后放在AuthorizationInfo里 simpleAuthorizationInfo.addRole(role.getName()); //查询权限 Listpermissions=permissionService.selectByRoleId(adminUser.getRoleId()); //将权限具体值取出来组装成一个权限String的集合 List permissionValues=permissions.stream().map(Permission::getValue).collect(Collectors.toList()); //将权限的String集合添加进AuthorizationInfo里,后面请求鉴权有用 simpleAuthorizationInfo.addStringPermissions(permissionValues); returnsimpleAuthorizationInfo; } //组装用户信息 @Override protectedAuthenticationInfodoGetAuthenticationInfo(AuthenticationTokentoken)throwsAuthenticationException{ Stringusername=(String)token.getPrincipal(); log.info("用户:{}正在登录...",username); AdminUseradminUser=adminUserService.selectByUsername(username); //如果用户不存在,则抛出未知用户的异常 if(adminUser==null)thrownewUnknownAccountException(); returnnewSimpleAuthenticationInfo(username,adminUser.getPassword(),getName()); } }
实现密码校验
shiro内置了几个密码校验的类,有Md5CredentialsMatcherSha1CredentialsMatcher,不过从1.1版本开始,都开始使用HashedCredentialsMatcher这个类了,通过配置加密规则来校验
它们都实现了一个接口CredentialsMatcher我这里也实现这个接口,实现一个自己的密码校验
说明一下,我这里用的加密方式是Spring-Security里的BCryptPasswordEncoder作的加密,之所以用它,是因为同一个密码被这货加密后,密文都不一样,下面是具体代码
publicclassMyCredentialsMatcherimplementsCredentialsMatcher{ @Override publicbooleandoCredentialsMatch(AuthenticationTokentoken,AuthenticationInfoinfo){ //大坑!!!!!!!!!!!!!!!!!!! //明明token跟info两个对象的里的Credentials类型都是Object,断点看到的类型都是char[] //但是!!!!!token里转成String要先强转成char[] //而info里取Credentials就可以直接使用String.valueOf()转成String //醉了。。 StringrawPassword=String.valueOf((char[])token.getCredentials()); StringencodedPassword=String.valueOf(info.getCredentials()); returnnewBCryptPasswordEncoder().matches(rawPassword,encodedPassword); } }
配置shiro
因为项目是spring-boot开发的,shiro就用java代码配置,不用xml配置,具体配置如下
@Configuration publicclassShiroConfig{ privateLoggerlog=LoggerFactory.getLogger(ShiroConfig.class); @Autowired privateMyShiroRealmmyShiroRealm; @Bean publicShiroFilterFactoryBeanshiroFilter(SecurityManagersecurityManager){ log.info("开始配置shiroFilter..."); ShiroFilterFactoryBeanshiroFilterFactoryBean=newShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); //拦截器. Mapmap=newHashMap<>(); //配置不会被拦截的链接顺序判断相关静态资源 map.put("/static/**","anon"); //配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了 map.put("/admin/logout","logout"); //:这是一个坑呢,一不小心代码就不好使了; // map.put("/admin/**","authc"); //如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面 shiroFilterFactoryBean.setLoginUrl("/adminlogin"); //登录成功后要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/admin/index"); //未授权界面; shiroFilterFactoryBean.setUnauthorizedUrl("/error"); shiroFilterFactoryBean.setFilterChainDefinitionMap(map); returnshiroFilterFactoryBean; } //配置加密方式 //配置了一下,这货就是验证不过,,改成手动验证算了,以后换加密方式也方便 @Bean publicMyCredentialsMatchermyCredentialsMatcher(){ returnnewMyCredentialsMatcher(); } //安全管理器配置 @Bean publicSecurityManagersecurityManager(){ DefaultWebSecurityManagersecurityManager=newDefaultWebSecurityManager(); myShiroRealm.setCredentialsMatcher(myCredentialsMatcher()); securityManager.setRealm(myShiroRealm); returnsecurityManager; } }
登录
都配置好了,就可以发起登录请求做测试了,一个简单的表单即可,写在Controller里就行
@PostMapping("/adminlogin") publicStringadminLogin(Stringusername,Stringpassword, @RequestParam(defaultValue="0")BooleanrememberMe, RedirectAttributesredirectAttributes){ try{ //添加用户认证信息 Subjectsubject=SecurityUtils.getSubject(); if(!subject.isAuthenticated()){ UsernamePasswordTokentoken=newUsernamePasswordToken(username,password,rememberMe); //进行验证,这里可以捕获异常,然后返回对应信息 subject.login(token); } }catch(AuthenticationExceptione){ //e.printStackTrace(); log.error(e.getMessage()); redirectAttributes.addFlashAttribute("error","用户名或密码错误"); redirectAttributes.addFlashAttribute("username",username); returnredirect("/adminlogin"); } returnredirect("/admin/index"); }
从上面代码可以看出,记住我功能也直接都实现好了,只需要在组装UsernamePasswordToken的时候,将记住我字段传进去就可以了,值是true,false,如果是true,登录成功后,shiro会在本地写一个cookie
调用subject.login(token);方法后,它会去鉴权,期间会产生各种各样的异常,有以下几种,可以通过捕捉不同的异常然后提示页面不同的错误信息,相当的方便呀,有木有
- AccountException帐户异常
- ConcurrentAccessException这个好像是并发异常
- CredentialsException密码校验异常
- DisabledAccountException帐户被禁异常
- ExcessiveAttemptsException尝试登录次数过多异常
- ExpiredCredentialsException认证信息过期异常
- IncorrectCredentialsException密码不正确异常
- LockedAccountException帐户被锁定异常
- UnknownAccountException未知帐户异常
- UnsupportedTokenExceptionlogin(AuthenticationToken)这个方法只能接收AuthenticationToken类的对象,如果传的是其它的类,就抛这个异常
上面这么多异常,shiro在处理登录的逻辑时,会自动的发出一些异常,当然你也可以手动去处理登录流程,然后根据不同的问题抛出不同的异常,手动处理的地方就在自己写的MyShiroRealm里的doGetAuthenticationInfo()方法里,我在上面代码里只处理了一个帐户不存在时抛出了一个UnknownAccountException的异常,其实还可以加更多其它的异常,这个要看个人系统的需求来定了
到这里已经可以正常的实现登录了,下面来说一些其它相关的功能的实现
自定freemarker标签
开发项目肯定要用到页面模板,我这里用的是freemarker,一个用户登录后,页面可能要根据用户的不同权限渲染不同的菜单,github上有个开源的库,也是可以用的,不过我觉得那个太麻烦了,就自己实现了一个,几行代码就能搞定
ShiroTag.java
@Component publicclassShiroTag{ //判断当前用户是否已经登录认证过 publicbooleanisAuthenticated(){ returnSecurityUtils.getSubject().isAuthenticated(); } //获取当前用户的用户名 publicStringgetPrincipal(){ return(String)SecurityUtils.getSubject().getPrincipal(); } //判断用户是否有xx角色 publicbooleanhasRole(Stringname){ returnSecurityUtils.getSubject().hasRole(name); } //判断用户是否有xx权限 publicbooleanhasPermission(Stringname){ return!StringUtils.isEmpty(name)&&SecurityUtils.getSubject().isPermitted(name); } }
将这个类注册到freemarker的全局变量里
FreemarkerConfig.java
@Configuration publicclassFreemarkerConfig{ privateLoggerlog=LoggerFactory.getLogger(FreeMarkerConfig.class); @Autowired privateShiroTagshiroTag; @PostConstruct publicvoidsetSharedVariable()throwsTemplateModelException{ //注入全局配置到freemarker log.info("开始配置freemarker全局变量..."); //shiro鉴权 configuration.setSharedVariable("sec",shiroTag); log.info("freemarker自定义标签配置完成!"); } }
有了这些配置后,就可以在页面里使用了,具体用法如下
<#ifsec.hasPermission("topic:list")>
加上这个后,在渲染页面的时候,就会根据当前用户是否有查看话题列表的权限,然后来渲染这个菜单
注解权限
有了上面freemarker标签判断是否有权限来渲染页面,这样做只能防君子,不能防小人,如果一个人知道后台的某个访问链接,但这个链接它是没有权限访问的,那他只要手动输入这个链接就还是可以访问的,所以这里还要在Controller层加一套防御,具体配置如下
在ShiroConfig里加上两个Bean
//加入注解的使用,不加入这个注解不生效 @Bean publicAuthorizationAttributeSourceAdvisorauthorizationAttributeSourceAdvisor(DefaultWebSecurityManagersecurityManager){ AuthorizationAttributeSourceAdvisorauthorizationAttributeSourceAdvisor=newAuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); returnauthorizationAttributeSourceAdvisor; } @Bean @ConditionalOnMissingBean publicDefaultAdvisorAutoProxyCreatordefaultAdvisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreatordefaultAAP=newDefaultAdvisorAutoProxyCreator(); defaultAAP.setProxyTargetClass(true); returndefaultAAP; }
有了这两个Bean就可以用shiro的注解鉴权了,用法如下@RequiresPermissions("topic:list")
@Controller @RequestMapping("/admin/topic") publicclassTopicAdminControllerextendsBaseAdminController{ @RequiresPermissions("topic:list") @GetMapping("/list") publicStringlist(){ //TODO return"admin/topic/list"; } }
shiro除了@RequiresPermissions注解外,还有其它几个鉴权的注解
- @RequiresPermissions
- @RequiresRoles
- @RequiresUser
- @RequiresGuest
- @RequiresAuthentication
一般@RequiresPermissions就够用了
总结
spring-boot集成shiro到这就结束了,是不是网上能找到的教程里最全的!相比spring-security要简单太多了,强烈推荐
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。