本文介绍 SpringBoot 整合 shiro,相对于 Spring Security 而言,shiro 更加简单,没有那么复杂。
目前我的需求是一个博客系统,有用户和管理员两种角色。一个用户可能有多个角色,每个角色可能有多个权限,每个角色关联不同的菜单(也可以权限和菜单关联)。
本文主要介绍 Shiro 的使用,这里只介绍用户和角色,不需要权限也行。
一、数据库设计
三张表:user、role、user_role
- -- ----------------------------
- -- Table structure for `role`
- -- ----------------------------
- DROP TABLE IF EXISTS `role`;
- CREATE TABLE `role` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `description` varchar(255) DEFAULT NULL,
- `role` varchar(255) DEFAULT NULL,
- PRIMARY KEY (`id`)
- ) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
- -- ----------------------------
- -- Table structure for `user`
- -- ----------------------------
- DROP TABLE IF EXISTS `user`;
- CREATE TABLE `user` (
- `id` int(10) NOT NULL AUTO_INCREMENT,
- `password` varchar(100) NOT NULL,
- `username` varchar(20) NOT NULL COMMENT '用于登录的账号',
- `display_name` varchar(20) DEFAULT NULL COMMENT '显示的用户名',
- `email` varchar(100) DEFAULT NULL COMMENT '电子邮箱',
- `url` varchar(255) DEFAULT NULL COMMENT '个人主页',
- `avatar` varchar(255) DEFAULT NULL COMMENT '头像',
- `profile` varchar(255) DEFAULT NULL COMMENT '简介',
- `create_time` datetime DEFAULT NULL,
- `last_login_time` datetime DEFAULT NULL,
- `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '正常1,禁止登录0, 已删除-1',
- PRIMARY KEY (`id`),
- UNIQUE KEY `uq_user_username` (`username`) USING BTREE,
- UNIQUE KEY `uq_user_displayname` (`display_name`) USING BTREE,
- UNIQUE KEY `uq_user_email` (`email`) USING BTREE
- ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
- -- ----------------------------
- -- Table structure for `user_role`
- -- ----------------------------
- DROP TABLE IF EXISTS `user_role`;
- CREATE TABLE `user_role` (
- `role_id` int(11) NOT NULL,
- `user_id` int(11) NOT NULL,
- KEY `role_id` (`role_id`),
- KEY `user_id` (`user_id`)
- ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
- SET FOREIGN_KEY_CHECKS = 1;
二、SpringBoot 整合 Shiro
1、添加 shiro 依赖
2、MyShiroRealm.java
- package com.liuyanzhao.blog.web.config;
- import com.liuyanzhao.blog.api.model.Role;
- import com.liuyanzhao.blog.api.model.User;
- import com.liuyanzhao.blog.api.service.RoleService;
- import com.liuyanzhao.blog.api.service.UserService;
- import com.liuyanzhao.blog.api.util.Response;
- import com.liuyanzhao.blog.web.enums.UserStatus;
- 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.subject.PrincipalCollection;
- import org.springframework.beans.factory.annotation.Autowired;
- import java.util.List;
- /**
- * @author 言曌
- * @date 2018/9/1 上午10:47
- */
- public class MyShiroRealm extends AuthorizingRealm {
- @Autowired
- private UserService userService;
- @Autowired
- private RoleService roleService;
- public static final String SALT = "com.liuyanzhao";
- @Override
- protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
- System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
- SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
- User user = (User) principals.getPrimaryPrincipal();
- List<Role> roleList = roleService.listRolesByUser(user);
- for (Role role : roleList) {
- authorizationInfo.addRole(role.getRole());
- }
- return authorizationInfo;
- }
- @Override
- protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
- throws AuthenticationException {
- System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
- //获取用户的输入的账号.
- String username = (String) token.getPrincipal();
- System.out.println(token.getCredentials());
- //通过username从数据库中查找 User对象,如果找到,没找到.
- //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
- Response<User> response = userService.getUserByUsername(username);
- if (!response.getSuccess()) {
- return null;
- }
- User user = response.getData();
- if (UserStatus.LOCKED.getCode().equals(user.getStatus())) {
- throw new LockedAccountException(username + "账号被锁定,请联系管理员!");
- }
- SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
- user,
- user.getPassword(),
- getName()
- );
- return authenticationInfo;
- }
- }
这个是自定义验证账号密码和验证是否有权限。
其中 UserService 和 RoleService 这里就不用给了,一个是根据用户名获得用户,一个是根据获得用户的权限列表。
3、ShiroConfig.java
- package com.liuyanzhao.blog.web.config;
- import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
- import org.apache.shiro.mgt.SecurityManager;
- 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.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
- import java.util.LinkedHashMap;
- import java.util.Map;
- import java.util.Properties;
- /**
- * @author 言曌
- * @date 2018/8/20 上午6:19
- */
- @Configuration
- public class ShiroConfig {
- @Bean
- public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
- System.out.println("ShiroConfiguration.shirFilter()");
- ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
- shiroFilterFactoryBean.setSecurityManager(securityManager);
- //拦截器.
- Map<String,String> filterChainDefinitionMap = new LinkedHashMap<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");
- return shiroFilterFactoryBean;
- }
- /**
- * 凭证匹配器
- * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
- * )
- * @return
- */
- @Bean
- public HashedCredentialsMatcher hashedCredentialsMatcher(){
- HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
- //散列算法:这里使用MD5算法;
- hashedCredentialsMatcher.setHashAlgorithmName("md5");
- //散列的次数,比如散列两次,相当于 md5(md5(""));
- hashedCredentialsMatcher.setHashIterations(2);
- return hashedCredentialsMatcher;
- }
- @Bean
- public MyShiroRealm myShiroRealm(){
- MyShiroRealm myShiroRealm = new MyShiroRealm();
- myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
- return myShiroRealm;
- }
- @Bean
- public SecurityManager securityManager(){
- DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
- //设置realm
- securityManager.setRealm(myShiroRealm());
- return securityManager;
- }
- /**
- * 开启shiro aop注解支持.
- * 使用代理方式;所以需要开启代码支持;
- * @param securityManager
- * @return
- */
- @Bean
- public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
- AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
- authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
- return authorizationAttributeSourceAdvisor;
- }
- @Bean(name="simpleMappingExceptionResolver")
- public SimpleMappingExceptionResolver
- createSimpleMappingExceptionResolver() {
- SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
- Properties mappings = new Properties();
- //数据库异常处理
- mappings.setProperty("DatabaseException", "databaseError");
- mappings.setProperty("UnauthorizedException","403");
- r.setExceptionMappings(mappings);
- r.setDefaultErrorView("error");
- r.setExceptionAttribute("message");
- return r;
- }
- }
这里补充一下链接器链,是按顺序匹配的。必须使用LinkedHashMap,因为HashMap遍历是无序的。
目前我是放行除 /admin/** 和 /user/** 之外所有的页面,通常情况下是放行匿名的页面,其他的一律需要授权验证,如
filterChainDefinitionMap.put("/**", "authc");
4、LoginParam.java
- package com.liuyanzhao.blog.web.param;
- import lombok.Data;
- import java.io.Serializable;
- /**
- * 登录参数
- * @author 言曌
- * @date 2018/9/9 上午11:42
- */
- @Data
- public class LoginParam implements Serializable{
- private static final long serialVersionUID = 166457193110647497L;
- private String username;
- private String password;
- private String captchaCode;
- private boolean rememberMe;
- private String CSRFToken;
- }
5、LoginController.java
- /**
- * 登录页面
- *
- * @return
- */
- @GetMapping("/login")
- public String loginPage() {
- return "login";
- }
- /**
- * 登录提交
- *
- * @param loginParam
- * @param model
- * @return
- * @throws Exception
- */
- @PostMapping("/login")
- public String login(LoginParam loginParam,
- Model model) throws Exception {
- //1、验证用户名和密码
- org.apache.shiro.subject.Subject subject = SecurityUtils.getSubject();
- UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(loginParam.getUsername(), loginParam.getPassword(), loginParam.isRememberMe());
- String msg = "";
- try {
- subject.login(usernamePasswordToken);
- return "redirect:/";
- } catch (UnknownAccountException e) {
- log.info("UnknownAccountException -- > 账号不存在:");
- msg = "账号不存在!";
- } catch (IncorrectCredentialsException e) {
- log.info("IncorrectCredentialsException -- > 密码不正确:");
- msg = "密码不正确!";
- } catch (LockedAccountException e) {
- log.info("LockedAccountException -- > 账号被锁定");
- msg = "账号被锁定!";
- } catch (Exception e) {
- log.info(e.getMessage());
- }
- model.addAttribute("msg", msg);
- return "login";
- }
6、login.html
- <form name="loginform" id="loginform" action="/login" method="post">
- <p>
- <label for="username">用户名或电子邮件地址<br/>
- <input type="text" name="username" id="username" class="input" size="20" required/>
- </label>
- </p>
- <p>
- <label for="password">密码<br/>
- <input type="password" name="password" id="password" class="input" size="20" required/>
- </label>
- </p>
- <p th:if="${msg}">
- <label for="captchaCode">验证码<br/>
- <input type="text" name="captchaCode" id="captchaCode" class="input" size="20"
- style="float:left; 40%; " required/>
- <img src="/img/getKaptchaImage" alt="" style="float:left;padding-top: 3px;">
- <span>换一张</span>
- </label>
- </p>
- <input type="hidden" name="CSRFToken" th:value="${session.CSRFToken}">
- <div style="clear: both;"></div>
- <p class="forgetmenot">
- <label for="rememberme">
- <input name="rememberMe" type="checkbox" id="rememberMe"
- checked="checked"> 记住我的登录信息</label>
- </p>
- <p class="submit">
- <input type="submit" class="button button-primary button-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、验证码配置类
- package com.liuyanzhao.blog.web.config;
- import com.google.code.kaptcha.impl.DefaultKaptcha;
- import com.google.code.kaptcha.util.Config;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import java.util.Properties;
- /**
- * 验证码图片样式配置
- * @author 言曌
- * @date 2018/9/2 上午10:23
- */
- @Configuration
- public class KaptchaConfig {
- @Bean(name="captchaProducer")
- public DefaultKaptcha getKaptchaBean(){
- DefaultKaptcha defaultKaptcha=new DefaultKaptcha();
- Properties properties=new Properties();
- //验证码字符范围
- 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", "宋体,楷体,微软雅黑");
- Config config=new Config(properties);
- defaultKaptcha.setConfig(config);
- return defaultKaptcha;
- }
- }
3、验证码控制器
- package com.liuyanzhao.blog.web.controller.common;
- import com.google.code.kaptcha.Constants;
- import com.google.code.kaptcha.Producer;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.GetMapping;
- import javax.imageio.ImageIO;
- import javax.servlet.ServletOutputStream;
- import java.awt.image.BufferedImage;
- /**
- * 验证码控制器
- * @author 言曌
- * @date 2018/9/2 上午10:41
- */
- @Controller
- @Slf4j
- public class KaptchaController extends BaseController {
- @Autowired
- private Producer captchaProducer;
- @GetMapping("/img/getKaptchaImage")
- public void getKaptchaImage() throws Exception {
- response.setDateHeader("Expires", 0);
- // Set standard HTTP/1.1 no-cache headers.
- response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
- // Set IE extended HTTP/1.1 no-cache headers (use addHeader).
- response.addHeader("Cache-Control", "post-check=0, pre-check=0");
- // Set standard HTTP/1.0 no-cache header.
- response.setHeader("Pragma", "no-cache");
- // return a jpeg
- response.setContentType("image/jpeg");
- // create the text for the image
- String capText = captchaProducer.createText();
- //将验证码存到session
- session.setAttribute(Constants.KAPTCHA_SESSION_KEY, capText);
- log.info(capText);
- // create the image with the text
- BufferedImage bi = captchaProducer.createImage(capText);
- ServletOutputStream out = response.getOutputStream();
- // write the data out
- ImageIO.write(bi, "jpg", out);
- try {
- out.flush();
- } finally {
- out.close();
- }
- }
- }
4、修改 LoginController.java
- /**
- * 登录提交
- *
- * @param loginParam
- * @param model
- * @return
- * @throws Exception
- */
- @PostMapping("/login")
- public String login(LoginParam loginParam,
- Model model) throws Exception {
- //1、检验验证码
- if (loginParam.getCaptchaCode() != null) {
- String inputCode = request.getParameter("captchaCode");
- String captchaSession = (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.Subject subject = SecurityUtils.getSubject();
- UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(loginParam.getUsername(), loginParam.getPassword());
- String msg = "";
- try {
- subject.login(usernamePasswordToken);
- return "redirect:/";
- } catch (UnknownAccountException e) {
- log.info("UnknownAccountException -- > 账号不存在:");
- msg = "账号不存在!";
- } catch (IncorrectCredentialsException e) {
- log.info("IncorrectCredentialsException -- > 密码不正确:");
- msg = "密码不正确!";
- } catch (LockedAccountException e) {
- log.info("LockedAccountException -- > 账号被锁定");
- msg = "账号被锁定!";
- } catch (Exception e) {
- log.info(e.getMessage());
- }
- model.addAttribute("msg", msg);
- CsrfTokenUtil.refreshToken(request);
- return "login";
- }
四、配置记住我
1、修改 ShiroConfig.java
- /**
- * cookie对象;
- * rememberMeCookie()方法是设置Cookie的生成模版,比如cookie的name,cookie的有效时间等等。
- * @return
- */
- @Bean
- public SimpleCookie rememberMeCookie(){
- //System.out.println("ShiroConfiguration.rememberMeCookie()");
- //这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
- SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
- //<!-- 记住我cookie生效时间30天 ,单位秒;-->
- simpleCookie.setMaxAge(259200);
- return simpleCookie;
- }
- /**
- * cookie管理对象;
- * rememberMeManager()方法是生成rememberMe管理器,而且要将这个rememberMe管理器设置到securityManager中
- * @return
- */
- @Bean
- public CookieRememberMeManager rememberMeManager(){
- //System.out.println("ShiroConfiguration.rememberMeManager()");
- CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
- cookieRememberMeManager.setCookie(rememberMeCookie());
- //rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
- cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
- return cookieRememberMeManager;
- }
- @Bean
- public SecurityManager securityManager(){
- DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
- //设置realm
- securityManager.setRealm(myShiroRealm());
- //用户授权/认证信息Cache, 采用EhC//注入记住我管理器
- securityManager.setRememberMeManager(rememberMeManager());
- return securityManager;
- }
2、修改 LoginController.java
主要是修改
- UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(loginParam.getUsername(), loginParam.getPassword(), loginParam.isRememberMe());
最终如下
- /**
- * 登录提交
- *
- * @param loginParam
- * @param model
- * @return
- * @throws Exception
- */
- @PostMapping("/login")
- public String login(LoginParam loginParam,
- Model model) throws Exception {
- //1、检验验证码
- if (loginParam.getCaptchaCode() != null) {
- String inputCode = request.getParameter("captchaCode");
- String captchaSession = (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.Subject subject = SecurityUtils.getSubject();
- UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(loginParam.getUsername(), loginParam.getPassword(), loginParam.isRememberMe());
- String msg = "";
- try {
- subject.login(usernamePasswordToken);
- return "redirect:/";
- } catch (UnknownAccountException e) {
- log.info("UnknownAccountException -- > 账号不存在:");
- msg = "账号不存在!";
- } catch (IncorrectCredentialsException e) {
- log.info("IncorrectCredentialsException -- > 密码不正确:");
- msg = "密码不正确!";
- } catch (LockedAccountException e) {
- log.info("LockedAccountException -- > 账号被锁定");
- msg = "账号被锁定!";
- } catch (Exception e) {
- 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");
然后再次尝试,发现可以访问。
五、效果图如下
首次登录无需验证码,登录错误需要验证码