700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > SpringBoot整合Shiro实现权限控制 验证码

SpringBoot整合Shiro实现权限控制 验证码

时间:2019-05-08 12:49:41

相关推荐

SpringBoot整合Shiro实现权限控制 验证码

本文介绍 SpringBoot 整合shiro,相对于 Spring Security 而言,shiro更加简单,没有那么复杂。

目前我的需求是一个博客系统,有用户和管理员两种角色。一个用户可能有多个角色,每个角色可能有多个权限,每个角色关联不同的菜单(也可以权限和菜单关联)。

本文主要介绍 Shiro 的使用,这里只介绍用户和角色,不需要权限也行。

一、数据库设计

三张表:user、role、user_role

--------------------------------Tablestructurefor`role`------------------------------DROPTABLEIFEXISTS`role`;CREATETABLE`role`(`id`int(11)NOTNULLAUTO_INCREMENT,`description`varchar(255)DEFAULTNULL,`role`varchar(255)DEFAULTNULL,PRIMARYKEY(`id`))ENGINE=MyISAMAUTO_INCREMENT=4DEFAULTCHARSET=utf8; --------------------------------Tablestructurefor`user`------------------------------DROPTABLEIFEXISTS`user`;CREATETABLE`user`(`id`int(10)NOTNULLAUTO_INCREMENT,`password`varchar(100)NOTNULL,`username`varchar(20)NOTNULLCOMMENT'用于登录的账号',`display_name`varchar(20)DEFAULTNULLCOMMENT'显示的用户名',`email`varchar(100)DEFAULTNULLCOMMENT'电子邮箱',`url`varchar(255)DEFAULTNULLCOMMENT'个人主页',`avatar`varchar(255)DEFAULTNULLCOMMENT'头像',`profile`varchar(255)DEFAULTNULLCOMMENT'简介',`create_time`datetimeDEFAULTNULL,`last_login_time`datetimeDEFAULTNULL,`status`tinyint(1)NOTNULLDEFAULT'1'COMMENT'正常1,禁止登录0,已删除-1',PRIMARYKEY(`id`),UNIQUEKEY`uq_user_username`(`username`)USINGBTREE,UNIQUEKEY`uq_user_displayname`(`display_name`)USINGBTREE,UNIQUEKEY`uq_user_email`(`email`)USINGBTREE)ENGINE=InnoDBAUTO_INCREMENT=5DEFAULTCHARSET=utf8mb4; --------------------------------Tablestructurefor`user_role`------------------------------DROPTABLEIFEXISTS`user_role`;CREATETABLE`user_role`(`role_id`int(11)NOTNULL,`user_id`int(11)NOTNULL,KEY`role_id`(`role_id`),KEY`user_id`(`user_id`))ENGINE=MyISAMDEFAULTCHARSET=utf8; SETFOREIGN_KEY_CHECKS=1;

二、SpringBoot 整合 Shiro

1、添加shiro依赖

<!--Shiro--><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.4.0</version></dependency>

2、MyShiroRealm.java

packagecom.liuyanzhao.blog.web.config; importcom.liuyanzhao.blog.api.model.Role;importcom.liuyanzhao.blog.api.model.User;importcom.liuyanzhao.blog.api.service.RoleService;importcom.liuyanzhao.blog.api.service.UserService;importcom.liuyanzhao.blog.api.util.Response;importcom.liuyanzhao.blog.web.enums.UserStatus;importorg.apache.shiro.authc.*;importorg.apache.shiro.authz.AuthorizationInfo;importorg.apache.shiro.authz.SimpleAuthorizationInfo;importorg.apache.shiro.realm.AuthorizingRealm;importorg.apache.shiro.subject.PrincipalCollection;importorg.springframework.beans.factory.annotation.Autowired; importjava.util.List; /***@author言曌*@date/9/1上午10:47*/ publicclassMyShiroRealmextendsAuthorizingRealm{@AutowiredprivateUserServiceuserService; @AutowiredprivateRoleServiceroleService; publicstaticfinalStringSALT="com.liuyanzhao"; @OverrideprotectedAuthorizationInfodoGetAuthorizationInfo(PrincipalCollectionprincipals){System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");SimpleAuthorizationInfoauthorizationInfo=newSimpleAuthorizationInfo();Useruser=(User)principals.getPrimaryPrincipal();List<Role>roleList=roleService.listRolesByUser(user);for(Rolerole:roleList){authorizationInfo.addRole(role.getRole());}returnauthorizationInfo;} @OverrideprotectedAuthenticationInfodoGetAuthenticationInfo(AuthenticationTokentoken)throwsAuthenticationException{System.out.println("MyShiroRealm.doGetAuthenticationInfo()");//获取用户的输入的账号.Stringusername=(String)token.getPrincipal();System.out.println(token.getCredentials());//通过username从数据库中查找User对象,如果找到,没找到.//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法Response<User>response=userService.getUserByUsername(username);if(!response.getSuccess()){returnnull;}Useruser=response.getData();if(UserStatus.LOCKED.getCode().equals(user.getStatus())){thrownewLockedAccountException(username+"账号被锁定,请联系管理员!");}SimpleAuthenticationInfoauthenticationInfo=newSimpleAuthenticationInfo(user,user.getPassword(),getName());returnauthenticationInfo;}}

这个是自定义验证账号密码和验证是否有权限。

其中UserService 和 RoleService 这里就不用给了,一个是根据用户名获得用户,一个是根据获得用户的权限列表。

3、ShiroConfig.java

packagecom.liuyanzhao.blog.web.config; importorg.apache.shiro.authc.credential.HashedCredentialsMatcher;importorg.apache.shiro.mgt.SecurityManager;importorg.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;importorg.apache.shiro.spring.web.ShiroFilterFactoryBean;importorg.apache.shiro.web.mgt.DefaultWebSecurityManager;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.web.servlet.handler.SimpleMappingExceptionResolver; importjava.util.LinkedHashMap;importjava.util.Map;importjava.util.Properties; /***@author言曌*@date/8/20上午6:19*/ @ConfigurationpublicclassShiroConfig{@BeanpublicShiroFilterFactoryBeanshirFilter(SecurityManagersecurityManager){System.out.println("ShiroConfiguration.shirFilter()");ShiroFilterFactoryBeanshiroFilterFactoryBean=newShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);//拦截器.Map<String,String>filterChainDefinitionMap=newLinkedHashMap<String,String>();//配置不会被拦截的链接顺序判断filterChainDefinitionMap.put("/css/**","anon");filterChainDefinitionMap.put("/js/**","anon");filterChainDefinitionMap.put("/img/**","anon");filterChainDefinitionMap.put("/components/**","anon");filterChainDefinitionMap.put("/favicon.ico","anon");//配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了filterChainDefinitionMap.put("/logout","logout");//<!--过滤链定义,从上向下顺序执行,一般将/**放在最为下边-->:这是一个坑呢,一不小心代码就不好使了;//<!--authc:所有url都必须认证通过才可以访问;anon:所有url都都可以匿名访问-->filterChainDefinitionMap.put("/admin/**","authc");filterChainDefinitionMap.put("/user/**","authc");filterChainDefinitionMap.put("/**","anon");shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); //如果不设置默认会自动寻找Web工程根目录下的"/login"页面shiroFilterFactoryBean.setLoginUrl("/login");//登录成功后要跳转的链接shiroFilterFactoryBean.setSuccessUrl("/");//未授权界面;shiroFilterFactoryBean.setUnauthorizedUrl("/403"); returnshiroFilterFactoryBean;} /***凭证匹配器*(由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了*)*@return*/@BeanpublicHashedCredentialsMatcherhashedCredentialsMatcher(){HashedCredentialsMatcherhashedCredentialsMatcher=newHashedCredentialsMatcher();//散列算法:这里使用MD5算法;hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列的次数,比如散列两次,相当于md5(md5(""));hashedCredentialsMatcher.setHashIterations(2);returnhashedCredentialsMatcher;} @BeanpublicMyShiroRealmmyShiroRealm(){MyShiroRealmmyShiroRealm=newMyShiroRealm();myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());returnmyShiroRealm;} @BeanpublicSecurityManagersecurityManager(){DefaultWebSecurityManagersecurityManager=newDefaultWebSecurityManager();//设置realmsecurityManager.setRealm(myShiroRealm());returnsecurityManager;} /***开启shiroaop注解支持.*使用代理方式;所以需要开启代码支持;*@paramsecurityManager*@return*/@BeanpublicAuthorizationAttributeSourceAdvisorauthorizationAttributeSourceAdvisor(SecurityManagersecurityManager){AuthorizationAttributeSourceAdvisorauthorizationAttributeSourceAdvisor=newAuthorizationAttributeSourceAdvisor();authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);returnauthorizationAttributeSourceAdvisor;} @Bean(name="simpleMappingExceptionResolver")publicSimpleMappingExceptionResolvercreateSimpleMappingExceptionResolver(){SimpleMappingExceptionResolverr=newSimpleMappingExceptionResolver();Propertiesmappings=newProperties();//数据库异常处理mappings.setProperty("DatabaseException","databaseError");mappings.setProperty("UnauthorizedException","403");r.setExceptionMappings(mappings);r.setDefaultErrorView("error");r.setExceptionAttribute("message");returnr;}}

这里补充一下链接器链,是按顺序匹配的。必须使用LinkedHashMap,因为HashMap遍历是无序的。

目前我是放行除 /admin/** 和 /user/** 之外所有的页面,通常情况下是放行匿名的页面,其他的一律需要授权验证,如

filterChainDefinitionMap.put("/**","authc");

4、LoginParam.java

packagecom.liuyanzhao.blog.web.param; importlombok.Data; importjava.io.Serializable; /***登录参数*@author言曌*@date/9/9上午11:42*/@DatapublicclassLoginParamimplementsSerializable{privatestaticfinallongserialVersionUID=166457193110647497L; privateStringusername; privateStringpassword; privateStringcaptchaCode; privatebooleanrememberMe; privateStringCSRFToken;}

5、LoginController.java

/***登录页面**@return*/@GetMapping("/login")publicStringloginPage(){return"login";} /***登录提交**@paramloginParam*@parammodel*@return*@throwsException*/@PostMapping("/login")publicStringlogin(LoginParamloginParam,Modelmodel)throwsException{//1、验证用户名和密码org.apache.shiro.subject.Subjectsubject=SecurityUtils.getSubject();UsernamePasswordTokenusernamePasswordToken=newUsernamePasswordToken(loginParam.getUsername(),loginParam.getPassword(),loginParam.isRememberMe());Stringmsg="";try{subject.login(usernamePasswordToken);return"redirect:/";}catch(UnknownAccountExceptione){log.info("UnknownAccountException-->账号不存在:");msg="账号不存在!";}catch(IncorrectCredentialsExceptione){log.info("IncorrectCredentialsException-->密码不正确:");msg="密码不正确!";}catch(LockedAccountExceptione){log.info("LockedAccountException-->账号被锁定");msg="账号被锁定!";}catch(Exceptione){log.info(e.getMessage());}model.addAttribute("msg",msg);return"login";}

6、login.html

<formname="loginform"id="loginform"action="/login"method="post"><p><labelfor="username">用户名或电子邮件地址<br/><inputtype="text"name="username"id="username"class="input"size="20"required/></label></p><p><labelfor="password">密码<br/><inputtype="password"name="password"id="password"class="input"size="20"required/></label></p><pth:if="${msg}"><labelfor="captchaCode">验证码<br/><inputtype="text"name="captchaCode"id="captchaCode"class="input"size="20"style="float:left;width:40%;"required/><imgsrc="/img/getKaptchaImage"alt=""style="float:left;padding-top:3px;"><span>换一张</span></label></p><inputtype="hidden"name="CSRFToken"th:value="${session.CSRFToken}"><divstyle="clear:both;"></div><pclass="forgetmenot"><labelfor="rememberme"><inputname="rememberMe"type="checkbox"id="rememberMe"checked="checked">记住我的登录信息</label></p><pclass="submit"><inputtype="submit"class="buttonbutton-primarybutton-large"value="登录"/></p><br></form>

这里主要关注 form 表单提交的 username 和 password 即可,其中 CSRF 防护忽略,rememberMe 下面要用到。

三、添加 kaptcha 验证码

1、添加验证码依赖

<!--验证码--><dependency><groupId>com.github.penggle</groupId><artifactId>kaptcha</artifactId><version>2.3.2</version></dependency>

2、验证码配置类

packagecom.liuyanzhao.blog.web.config; importcom.google.code.kaptcha.impl.DefaultKaptcha;importcom.google.code.kaptcha.util.Config;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration; importjava.util.Properties; /***验证码图片样式配置*@author言曌*@date/9/2上午10:23*/ @ConfigurationpublicclassKaptchaConfig{@Bean(name="captchaProducer")publicDefaultKaptchagetKaptchaBean(){DefaultKaptchadefaultKaptcha=newDefaultKaptcha();Propertiesproperties=newProperties();//验证码字符范围properties.setProperty("kaptcha.textproducer.char.string","23456789");//图片边框颜色properties.setProperty("kaptcha.border.color","245,248,249");//字体颜色properties.setProperty("kaptcha.textproducer.font.color","black");//文字间隔properties.setProperty("kaptcha.textproducer.char.space","1");//图片宽度properties.setProperty("kaptcha.image.width","100");//图片高度properties.setProperty("kaptcha.image.height","35");//字体大小properties.setProperty("kaptcha.textproducer.font.size","30");//session的key//properties.setProperty("kaptcha.session.key","code");//长度properties.setProperty("kaptcha.textproducer.char.length","4");//字体properties.setProperty("kaptcha.textproducer.font.names","宋体,楷体,微软雅黑");Configconfig=newConfig(properties);defaultKaptcha.setConfig(config);returndefaultKaptcha;}}

3、验证码控制器

packagecom.liuyanzhao.blog.mon; importcom.google.code.kaptcha.Constants;importcom.google.code.kaptcha.Producer;importlombok.extern.slf4j.Slf4j;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.GetMapping; importjavax.imageio.ImageIO;importjavax.servlet.ServletOutputStream;importjava.awt.image.BufferedImage; /***验证码控制器*@author言曌*@date/9/2上午10:41*/ @Controller@Slf4jpublicclassKaptchaControllerextendsBaseController{@AutowiredprivateProducercaptchaProducer; @GetMapping("/img/getKaptchaImage")publicvoidgetKaptchaImage()throwsException{response.setDateHeader("Expires",0);//SetstandardHTTP/1.1no-cacheheaders.response.setHeader("Cache-Control","no-store,no-cache,must-revalidate");//SetIEextendedHTTP/1.1no-cacheheaders(useaddHeader).response.addHeader("Cache-Control","post-check=0,pre-check=0");//SetstandardHTTP/1.0no-cacheheader.response.setHeader("Pragma","no-cache");//returnajpegresponse.setContentType("image/jpeg");//createthetextfortheimageStringcapText=captchaProducer.createText();//将验证码存到sessionsession.setAttribute(Constants.KAPTCHA_SESSION_KEY,capText);log.info(capText);//createtheimagewiththetextBufferedImagebi=captchaProducer.createImage(capText);ServletOutputStreamout=response.getOutputStream();//writethedataoutImageIO.write(bi,"jpg",out);try{out.flush();}finally{out.close();}}}

4、修改 LoginController.java

/***登录提交**@paramloginParam*@parammodel*@return*@throwsException*/@PostMapping("/login")publicStringlogin(LoginParamloginParam,Modelmodel)throwsException{//1、检验验证码if(loginParam.getCaptchaCode()!=null){StringinputCode=request.getParameter("captchaCode");StringcaptchaSession=(String)session.getAttribute(Constants.KAPTCHA_SESSION_KEY);if(!Objects.equals(inputCode,captchaSession)){log.info("验证码错误,用户输入:{},正确验证码:{}",inputCode,captchaSession);model.addAttribute("msg","验证码不正确!");CsrfTokenUtil.refreshToken(request);return"login";}} //2、验证用户名和密码org.apache.shiro.subject.Subjectsubject=SecurityUtils.getSubject();UsernamePasswordTokenusernamePasswordToken=newUsernamePasswordToken(loginParam.getUsername(),loginParam.getPassword());Stringmsg="";try{subject.login(usernamePasswordToken);return"redirect:/";}catch(UnknownAccountExceptione){log.info("UnknownAccountException-->账号不存在:");msg="账号不存在!";}catch(IncorrectCredentialsExceptione){log.info("IncorrectCredentialsException-->密码不正确:");msg="密码不正确!";}catch(LockedAccountExceptione){log.info("LockedAccountException-->账号被锁定");msg="账号被锁定!";}catch(Exceptione){log.info(e.getMessage());}model.addAttribute("msg",msg);CsrfTokenUtil.refreshToken(request);return"login";}

四、配置记住我

1、修改 ShiroConfig.java

/***cookie对象;*rememberMeCookie()方法是设置Cookie的生成模版,比如cookie的name,cookie的有效时间等等。*@return*/@BeanpublicSimpleCookierememberMeCookie(){//System.out.println("ShiroConfiguration.rememberMeCookie()");//这个参数是cookie的名称,对应前端的checkbox的name=rememberMeSimpleCookiesimpleCookie=newSimpleCookie("rememberMe");//<!--记住我cookie生效时间30天,单位秒;-->simpleCookie.setMaxAge(259200);returnsimpleCookie;} /***cookie管理对象;*rememberMeManager()方法是生成rememberMe管理器,而且要将这个rememberMe管理器设置到securityManager中*@return*/@BeanpublicCookieRememberMeManagerrememberMeManager(){//System.out.println("ShiroConfiguration.rememberMeManager()");CookieRememberMeManagercookieRememberMeManager=newCookieRememberMeManager();cookieRememberMeManager.setCookie(rememberMeCookie());//rememberMecookie加密的密钥建议每个项目都不一样默认AES算法密钥长度(128256512位)cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));returncookieRememberMeManager;} @BeanpublicSecurityManagersecurityManager(){DefaultWebSecurityManagersecurityManager=newDefaultWebSecurityManager();//设置realmsecurityManager.setRealm(myShiroRealm());//用户授权/认证信息Cache,采用EhC//注入记住我管理器securityManager.setRememberMeManager(rememberMeManager());returnsecurityManager;}

2、修改 LoginController.java

主要是修改

UsernamePasswordTokenusernamePasswordToken=newUsernamePasswordToken(loginParam.getUsername(),loginParam.getPassword(),loginParam.isRememberMe());

最终如下

/***登录提交**@paramloginParam*@parammodel*@return*@throwsException*/@PostMapping("/login")publicStringlogin(LoginParamloginParam,Modelmodel)throwsException{//1、检验验证码if(loginParam.getCaptchaCode()!=null){StringinputCode=request.getParameter("captchaCode");StringcaptchaSession=(String)session.getAttribute(Constants.KAPTCHA_SESSION_KEY);if(!Objects.equals(inputCode,captchaSession)){log.info("验证码错误,用户输入:{},正确验证码:{}",inputCode,captchaSession);model.addAttribute("msg","验证码不正确!");CsrfTokenUtil.refreshToken(request);return"login";}} //2、验证用户名和密码org.apache.shiro.subject.Subjectsubject=SecurityUtils.getSubject();UsernamePasswordTokenusernamePasswordToken=newUsernamePasswordToken(loginParam.getUsername(),loginParam.getPassword(),loginParam.isRememberMe());Stringmsg="";try{subject.login(usernamePasswordToken);return"redirect:/";}catch(UnknownAccountExceptione){log.info("UnknownAccountException-->账号不存在:");msg="账号不存在!";}catch(IncorrectCredentialsExceptione){log.info("IncorrectCredentialsException-->密码不正确:");msg="密码不正确!";}catch(LockedAccountExceptione){log.info("LockedAccountException-->账号被锁定");msg="账号被锁定!";}catch(Exceptione){log.info(e.getMessage());}model.addAttribute("msg",msg);CsrfTokenUtil.refreshToken(request);return"login";}

3、login.html 添加记住我的复选框

name为之前填的rememberMe

4、修改 ShiroConfig.java

上面的配置后,当登录后,会创建rememberMe的 cookie,退出浏览器,cookie依然存在。

但是我们访问需要登录(authc)的页面会被拦截到登录页面

解决办法是修改 authc 为 user

我们要修改

filterChainDefinitionMap.put("/admin/**","authc");filterChainDefinitionMap.put("/user/**","authc");

filterChainDefinitionMap.put("/admin/**","user");filterChainDefinitionMap.put("/user/**","user");

然后再次尝试,发现可以访问。

五、效果图如下

首次登录无需验证码,登录错误需要验证码

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。