本文介绍 SpringBoot 整合 shiro,相对于 Spring Security 而言,shiro 更加简单,没有那么复杂。
目前我的需求是一个博客系统,有用户和管理员两种角色。一个用户可能有多个角色,每个角色可能有多个权限,每个角色关联不同的菜单(也可以权限和菜单关联)。
本文主要介绍 Shiro 的使用,这里只介绍用户和角色,不需要权限也行。
一、数据库设计
三张表:user、role、user_role
1. -- ----------------------------
2. -- Table structure for `role`
3. -- ----------------------------
4. DROP TABLE IF EXISTS `role`;
5. CREATE TABLE `role` (
6. int(11) NOT NULL AUTO_INCREMENT,
7. varchar(255) DEFAULT NULL,
8. varchar(255) DEFAULT NULL,
9. PRIMARY KEY (`id`)
10. ) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
11.
12. -- ----------------------------
13. -- Table structure for `user`
14. -- ----------------------------
15. DROP TABLE IF EXISTS `user`;
16. CREATE TABLE `user` (
17. int(10) NOT NULL AUTO_INCREMENT,
18. password` varchar(100) NOT NULL,
19. varchar(20) NOT NULL COMMENT '用于登录的账号',
20. varchar(20) DEFAULT NULL COMMENT '显示的用户名',
21. varchar(100) DEFAULT NULL COMMENT '电子邮箱',
22. varchar(255) DEFAULT NULL COMMENT '个人主页',
23. varchar(255) DEFAULT NULL COMMENT '头像',
24. varchar(255) DEFAULT NULL COMMENT '简介',
25. DEFAULT NULL,
26. DEFAULT NULL,
27. NOT NULL DEFAULT '1' COMMENT '正常1,禁止登录0, 已删除-1',
28. PRIMARY KEY (`id`),
29. UNIQUE KEY `uq_user_username` (`username`) USING BTREE,
30. UNIQUE KEY `uq_user_displayname` (`display_name`) USING BTREE,
31. UNIQUE KEY `uq_user_email` (`email`) USING BTREE
32. ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
33.
34. -- ----------------------------
35. -- Table structure for `user_role`
36. -- ----------------------------
37. DROP TABLE IF EXISTS `user_role`;
38. CREATE TABLE `user_role` (
39. int(11) NOT NULL,
40. int(11) NOT NULL,
41. KEY `role_id` (`role_id`),
42. KEY `user_id` (`user_id`)
43. ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
44.
45. SET FOREIGN_KEY_CHECKS = 1;
二、SpringBoot 整合 Shiro
1、添加 shiro 依赖
1. <!--Shiro-->
2. <dependency>
3. <groupId>org.apache.shiro</groupId>
4. <artifactId>shiro-spring</artifactId>
5. <version>1.4.0</version>
6. </dependency>
2、MyShiroRealm.java
1. package com.liuyanzhao.blog.web.config;
2.
3. import com.liuyanzhao.blog.api.model.Role;
4. import com.liuyanzhao.blog.api.model.User;
5. import com.liuyanzhao.blog.api.service.RoleService;
6. import com.liuyanzhao.blog.api.service.UserService;
7. import com.liuyanzhao.blog.api.util.Response;
8. import com.liuyanzhao.blog.web.enums.UserStatus;
9. import org.apache.shiro.authc.*;
10. import org.apache.shiro.authz.AuthorizationInfo;
11. import org.apache.shiro.authz.SimpleAuthorizationInfo;
12. import org.apache.shiro.realm.AuthorizingRealm;
13. import org.apache.shiro.subject.PrincipalCollection;
14. import org.springframework.beans.factory.annotation.Autowired;
15.
16. import java.util.List;
17.
18. /**
19. * @author 言曌
20. * @date 2018/9/1 上午10:47
21. */
22.
23. public class MyShiroRealm extends AuthorizingRealm {
24.
25. @Autowired
26. private UserService userService;
27.
28. @Autowired
29. private RoleService roleService;
30.
31. public static final String SALT = "com.liuyanzhao";
32.
33. @Override
34. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
35. "权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
36. new SimpleAuthorizationInfo();
37. User user = (User) principals.getPrimaryPrincipal();
38. List<Role> roleList = roleService.listRolesByUser(user);
39. for (Role role : roleList) {
40. authorizationInfo.addRole(role.getRole());
41. }
42. return authorizationInfo;
43. }
44.
45. @Override
46. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
47. throws AuthenticationException {
48. "MyShiroRealm.doGetAuthenticationInfo()");
49. //获取用户的输入的账号.
50. String username = (String) token.getPrincipal();
51. System.out.println(token.getCredentials());
52. //通过username从数据库中查找 User对象,如果找到,没找到.
53. //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
54. Response<User> response = userService.getUserByUsername(username);
55. if (!response.getSuccess()) {
56. return null;
57. }
58. User user = response.getData();
59. if (UserStatus.LOCKED.getCode().equals(user.getStatus())) {
60. throw new LockedAccountException(username + "账号被锁定,请联系管理员!");
61. }
62. new SimpleAuthenticationInfo(
63. user,
64. user.getPassword(),
65. getName()
66. );
67. return authenticationInfo;
68. }
69. }
这个是自定义验证账号密码和验证是否有权限。
其中 UserService 和 RoleService 这里就不用给了,一个是根据用户名获得用户,一个是根据获得用户的权限列表。
3、ShiroConfig.java
1. package com.liuyanzhao.blog.web.config;
2.
3. import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
4. import org.apache.shiro.mgt.SecurityManager;
5. import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
6. import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
7. import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
8. import org.springframework.context.annotation.Bean;
9. import org.springframework.context.annotation.Configuration;
10. import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
11.
12. import java.util.LinkedHashMap;
13. import java.util.Map;
14. import java.util.Properties;
15.
16. /**
17. * @author 言曌
18. * @date 2018/8/20 上午6:19
19. */
20.
21. @Configuration
22. public class ShiroConfig {
23.
24.
25. @Bean
26. public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
27. "ShiroConfiguration.shirFilter()");
28. new ShiroFilterFactoryBean();
29. shiroFilterFactoryBean.setSecurityManager(securityManager);
30. //拦截器.
31. new LinkedHashMap<String,String>();
32. // 配置不会被拦截的链接 顺序判断
33. "/css/**", "anon");
34. "/js/**", "anon");
35. "/img/**", "anon");
36. "/components/**", "anon");
37. "/favicon.ico", "anon");
38. //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
39. "/logout", "logout");
40. //<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
41. //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
42. "/admin/**", "authc");
43. "/user/**", "authc");
44. "/**", "anon");
45. shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
46.
47. // 如果不设置默认会自动寻找Web工程根目录下的"/login"页面
48. "/login");
49. // 登录成功后要跳转的链接
50. "/");
51. //未授权界面;
52. "/403");
53.
54. return shiroFilterFactoryBean;
55. }
56.
57. /**
58. * 凭证匹配器
59. * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
60. * )
61. @return
62. */
63. @Bean
64. public HashedCredentialsMatcher hashedCredentialsMatcher(){
65. new HashedCredentialsMatcher();
66. //散列算法:这里使用MD5算法;
67. "md5");
68. //散列的次数,比如散列两次,相当于 md5(md5(""));
69. hashedCredentialsMatcher.setHashIterations(2);
70. return hashedCredentialsMatcher;
71. }
72.
73. @Bean
74. public MyShiroRealm myShiroRealm(){
75. new MyShiroRealm();
76. myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
77. return myShiroRealm;
78. }
79.
80.
81. @Bean
82. public SecurityManager securityManager(){
83. new DefaultWebSecurityManager();
84. //设置realm
85. securityManager.setRealm(myShiroRealm());
86. return securityManager;
87. }
88.
89.
90.
91. /**
92. * 开启shiro aop注解支持.
93. * 使用代理方式;所以需要开启代码支持;
94. * @param securityManager
95. * @return
96. */
97. @Bean
98. public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
99. new AuthorizationAttributeSourceAdvisor();
100. authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
101. return authorizationAttributeSourceAdvisor;
102. }
103.
104. @Bean(name="simpleMappingExceptionResolver")
105. public SimpleMappingExceptionResolver
106. createSimpleMappingExceptionResolver() {
107. new SimpleMappingExceptionResolver();
108. new Properties();
109. //数据库异常处理
110. "DatabaseException", "databaseError");
111. "UnauthorizedException","403");
112. r.setExceptionMappings(mappings);
113. "error");
114. "message");
115. return r;
116. }
117. }
这里补充一下链接器链,是按顺序匹配的。必须使用LinkedHashMap,因为HashMap遍历是无序的。
目前我是放行除 /admin/** 和 /user/** 之外所有的页面,通常情况下是放行匿名的页面,其他的一律需要授权验证,如
filterChainDefinitionMap.put("/**", "authc");
4、LoginParam.java
1. package com.liuyanzhao.blog.web.param;
2.
3. import lombok.Data;
4.
5. import java.io.Serializable;
6.
7. /**
8. * 登录参数
9. * @author 言曌
10. * @date 2018/9/9 上午11:42
11. */
12. @Data
13. public class LoginParam implements Serializable{
14.
15. private static final long serialVersionUID = 166457193110647497L;
16.
17. private String username;
18.
19. private String password;
20.
21. private String captchaCode;
22.
23. private boolean rememberMe;
24.
25. private String CSRFToken;
26. }
5、LoginController.java
1. /**
2. * 登录页面
3. *
4. * @return
5. */
6. @GetMapping("/login")
7. public String loginPage() {
8. return "login";
9. }
10.
11.
12. /**
13. * 登录提交
14. *
15. * @param loginParam
16. * @param model
17. * @return
18. * @throws Exception
19. */
20. @PostMapping("/login")
21. public String login(LoginParam loginParam,
22. throws Exception {
23. //1、验证用户名和密码
24. org.apache.shiro.subject.Subject subject = SecurityUtils.getSubject();
25. new UsernamePasswordToken(loginParam.getUsername(), loginParam.getPassword(), loginParam.isRememberMe());
26. "";
27. try {
28. subject.login(usernamePasswordToken);
29. return "redirect:/";
30. catch (UnknownAccountException e) {
31. "UnknownAccountException -- > 账号不存在:");
32. "账号不存在!";
33. catch (IncorrectCredentialsException e) {
34. "IncorrectCredentialsException -- > 密码不正确:");
35. "密码不正确!";
36. catch (LockedAccountException e) {
37. "LockedAccountException -- > 账号被锁定");
38. "账号被锁定!";
39. catch (Exception e) {
40. log.info(e.getMessage());
41. }
42. "msg", msg);
43. return "login";
44. }
6、login.html
1. <form name="loginform" id="loginform" action="/login" method="post">
2. <p>
3. <label for="username">用户名或电子邮件地址<br/>
4. <input type="text" name="username" id="username" class="input" size="20" required/>
5. </label>
6. </p>
7. <p>
8. <label for="password">密码<br/>
9. <input type="password" name="password" id="password" class="input" size="20" required/>
10. </label>
11. </p>
12. <p th:if="${msg}">
13. <label for="captchaCode">验证码<br/>
14. <input type="text" name="captchaCode" id="captchaCode" class="input" size="20"
15. style="float:left;width: 40%; " required/>
16. <img src="/img/getKaptchaImage" alt="" style="float:left;padding-top: 3px;">
17. <span>换一张</span>
18. </label>
19. </p>
20. <input type="hidden" name="CSRFToken" th:value="${session.CSRFToken}">
21. <div style="clear: both;"></div>
22. <p class="forgetmenot">
23. <label for="rememberme">
24. <input name="rememberMe" type="checkbox" id="rememberMe"
25. checked="checked"> 记住我的登录信息</label>
26. </p>
27. <p class="submit">
28. <input type="submit" class="button button-primary button-large" value="登录"/>
29. </p>
30. <br>
31. </form>
这里主要关注 form 表单提交的 username 和 password 即可,其中 CSRF 防护忽略,rememberMe 下面要用到。
三、添加 kaptcha 验证码
1、添加验证码依赖
1. <!--验证码-->
2. <dependency>
3. <groupId>com.github.penggle</groupId>
4. <artifactId>kaptcha</artifactId>
5. <version>2.3.2</version>
6. </dependency>
2、验证码配置类
1. package com.liuyanzhao.blog.web.config;
2.
3. import com.google.code.kaptcha.impl.DefaultKaptcha;
4. import com.google.code.kaptcha.util.Config;
5. import org.springframework.context.annotation.Bean;
6. import org.springframework.context.annotation.Configuration;
7.
8. import java.util.Properties;
9.
10. /**
11. * 验证码图片样式配置
12. * @author 言曌
13. * @date 2018/9/2 上午10:23
14. */
15.
16. @Configuration
17. public class KaptchaConfig {
18. @Bean(name="captchaProducer")
19. public DefaultKaptcha getKaptchaBean(){
20. new DefaultKaptcha();
21. new Properties();
22. //验证码字符范围
23. "kaptcha.textproducer.char.string", "23456789");
24. //图片边框颜色
25. "kaptcha.border.color", "245,248,249");
26. //字体颜色
27. "kaptcha.textproducer.font.color", "black");
28. //文字间隔
29. "kaptcha.textproducer.char.space", "1");
30. //图片宽度
31. "kaptcha.image.width", "100");
32. //图片高度
33. "kaptcha.image.height", "35");
34. //字体大小
35. "kaptcha.textproducer.font.size", "30");
36. //session的key
37. // properties.setProperty("kaptcha.session.key", "code");
38. //长度
39. "kaptcha.textproducer.char.length", "4");
40. //字体
41. "kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑");
42. new Config(properties);
43. defaultKaptcha.setConfig(config);
44. return defaultKaptcha;
45. }
46. }
3、验证码控制器
1. package com.liuyanzhao.blog.web.controller.common;
2.
3. import com.google.code.kaptcha.Constants;
4. import com.google.code.kaptcha.Producer;
5. import lombok.extern.slf4j.Slf4j;
6. import org.springframework.beans.factory.annotation.Autowired;
7. import org.springframework.stereotype.Controller;
8. import org.springframework.web.bind.annotation.GetMapping;
9.
10. import javax.imageio.ImageIO;
11. import javax.servlet.ServletOutputStream;
12. import java.awt.image.BufferedImage;
13.
14. /**
15. * 验证码控制器
16. * @author 言曌
17. * @date 2018/9/2 上午10:41
18. */
19.
20. @Controller
21. @Slf4j
22. public class KaptchaController extends BaseController {
23.
24. @Autowired
25. private Producer captchaProducer;
26.
27.
28. @GetMapping("/img/getKaptchaImage")
29. public void getKaptchaImage() throws Exception {
30. "Expires", 0);
31. // Set standard HTTP/1.1 no-cache headers.
32. "Cache-Control", "no-store, no-cache, must-revalidate");
33. // Set IE extended HTTP/1.1 no-cache headers (use addHeader).
34. "Cache-Control", "post-check=0, pre-check=0");
35. // Set standard HTTP/1.0 no-cache header.
36. "Pragma", "no-cache");
37. // return a jpeg
38. "image/jpeg");
39. // create the text for the image
40. String capText = captchaProducer.createText();
41. //将验证码存到session
42. session.setAttribute(Constants.KAPTCHA_SESSION_KEY, capText);
43. log.info(capText);
44. // create the image with the text
45. BufferedImage bi = captchaProducer.createImage(capText);
46. ServletOutputStream out = response.getOutputStream();
47. // write the data out
48. "jpg", out);
49. try {
50. out.flush();
51. finally {
52. out.close();
53. }
54. }
55. }
4、修改 LoginController.java
1. /**
2. * 登录提交
3. *
4. * @param loginParam
5. * @param model
6. * @return
7. * @throws Exception
8. */
9. @PostMapping("/login")
10. public String login(LoginParam loginParam,
11. throws Exception {
12.
13. //1、检验验证码
14. if (loginParam.getCaptchaCode() != null) {
15. "captchaCode");
16. String captchaSession = (String) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
17. if (!Objects.equals(inputCode, captchaSession)) {
18. "验证码错误,用户输入:{}, 正确验证码:{}", inputCode, captchaSession);
19. "msg", "验证码不正确!");
20. CsrfTokenUtil.refreshToken(request);
21. return "login";
22. }
23. }
24.
25. //2、验证用户名和密码
26. org.apache.shiro.subject.Subject subject = SecurityUtils.getSubject();
27. new UsernamePasswordToken(loginParam.getUsername(), loginParam.getPassword());
28. "";
29. try {
30. subject.login(usernamePasswordToken);
31. return "redirect:/";
32. catch (UnknownAccountException e) {
33. "UnknownAccountException -- > 账号不存在:");
34. "账号不存在!";
35. catch (IncorrectCredentialsException e) {
36. "IncorrectCredentialsException -- > 密码不正确:");
37. "密码不正确!";
38. catch (LockedAccountException e) {
39. "LockedAccountException -- > 账号被锁定");
40. "账号被锁定!";
41. catch (Exception e) {
42. log.info(e.getMessage());
43. }
44. "msg", msg);
45. CsrfTokenUtil.refreshToken(request);
46. return "login";
47. }
四、配置记住我
1、修改 ShiroConfig.java
1. /**
2. * cookie对象;
3. * rememberMeCookie()方法是设置Cookie的生成模版,比如cookie的name,cookie的有效时间等等。
4. * @return
5. */
6. @Bean
7. public SimpleCookie rememberMeCookie(){
8. //System.out.println("ShiroConfiguration.rememberMeCookie()");
9. //这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
10. new SimpleCookie("rememberMe");
11. //<!-- 记住我cookie生效时间30天 ,单位秒;-->
12. 259200);
13. return simpleCookie;
14. }
15.
16. /**
17. * cookie管理对象;
18. * rememberMeManager()方法是生成rememberMe管理器,而且要将这个rememberMe管理器设置到securityManager中
19. * @return
20. */
21. @Bean
22. public CookieRememberMeManager rememberMeManager(){
23. //System.out.println("ShiroConfiguration.rememberMeManager()");
24. new CookieRememberMeManager();
25. cookieRememberMeManager.setCookie(rememberMeCookie());
26. //rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
27. "2AvVhdsgUs0FSA3SDFAdag=="));
28. return cookieRememberMeManager;
29. }
30.
31.
32. @Bean
33. public SecurityManager securityManager(){
34. new DefaultWebSecurityManager();
35. //设置realm
36. securityManager.setRealm(myShiroRealm());
37. //用户授权/认证信息Cache, 采用EhC//注入记住我管理器
38. securityManager.setRememberMeManager(rememberMeManager());
39. return securityManager;
40. }
2、修改 LoginController.java
主要是修改
1. UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(loginParam.getUsername(), loginParam.getPassword(), loginParam.isRememberMe());
最终如下
1. /**
2. * 登录提交
3. *
4. * @param loginParam
5. * @param model
6. * @return
7. * @throws Exception
8. */
9. @PostMapping("/login")
10. public String login(LoginParam loginParam,
11. throws Exception {
12.
13. //1、检验验证码
14. if (loginParam.getCaptchaCode() != null) {
15. "captchaCode");
16. String captchaSession = (String) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
17. if (!Objects.equals(inputCode, captchaSession)) {
18. "验证码错误,用户输入:{}, 正确验证码:{}", inputCode, captchaSession);
19. "msg", "验证码不正确!");
20. CsrfTokenUtil.refreshToken(request);
21. return "login";
22. }
23. }
24.
25. //2、验证用户名和密码
26. org.apache.shiro.subject.Subject subject = SecurityUtils.getSubject();
27. new UsernamePasswordToken(loginParam.getUsername(), loginParam.getPassword(), loginParam.isRememberMe());
28. "";
29. try {
30. subject.login(usernamePasswordToken);
31. return "redirect:/";
32. catch (UnknownAccountException e) {
33. "UnknownAccountException -- > 账号不存在:");
34. "账号不存在!";
35. catch (IncorrectCredentialsException e) {
36. "IncorrectCredentialsException -- > 密码不正确:");
37. "密码不正确!";
38. catch (LockedAccountException e) {
39. "LockedAccountException -- > 账号被锁定");
40. "账号被锁定!";
41. catch (Exception e) {
42. log.info(e.getMessage());
43. }
44. "msg", msg);
45. CsrfTokenUtil.refreshToken(request);
46. return "login";
47. }
3、login.html 添加记住我的复选框
name为之前填的 rememberMe
4、修改 ShiroConfig.java
上面的配置后,当登录后,会创建rememberMe的 cookie,退出浏览器,cookie依然存在。
但是我们访问需要登录(authc)的页面会被拦截到登录页面
解决办法是修改 authc 为 user
我们要修改
1. filterChainDefinitionMap.put("/admin/**", "authc");
2. filterChainDefinitionMap.put("/user/**", "authc");
为
1. filterChainDefinitionMap.put("/admin/**", "user");
2. filterChainDefinitionMap.put("/user/**", "user");
然后再次尝试,发现可以访问。
五、效果图如下
首次登录无需验证码,登录错误需要验证码