Springboot+Shiro+Mybatis+mysql实现权限安全认证
Shiro是Apache 的一个强大且易用的Java安全框架,执行身份验证、授权、密码学和会话管理。Shiro 主要分为两个部分就是认证和授权两部分
一、介绍
-
Subject代表了当前用户的安全操作
-
SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
-
Authenticator即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。
-
Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
-
Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
-
sessionManager即会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上。
Shiro相关类介绍
-
(1)Authentication 认证 —— 用户登录
-
(2)Authorization 授权 —- 用户具有哪些权限
-
(3)Cryptography 安全数据加密
-
(4)Session Management 会话管理
-
(5)Web Integration web系统集成
-
(6)Interations 集成其它应用,spring、缓存框架
二、依赖引入
完整的pom文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
<relativePath></relativePath> <!-- lookup parent from repository -->
</parent>
<groupId>com.gt.shiro</groupId>
<artifactId>com.sunyue.shiro</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<java.version>1.8</java.version>
<druid.verzion>1.1.10</druid.verzion>
<pagehelper.version>1.2.10</pagehelper.version>
<mybatis.version>2.1.4</mybatis.version>
<thymeleaf-layout-dialect.version>2.0.4</thymeleaf-layout-dialect.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 排除默认的tomcat -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 重新依赖Jetty的starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<!--shiro整合spring-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.verzion}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper-spring-boot-starter -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- spring boot maven插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.gt.shiro.SpringShiroApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
三、配置文件
application.yml配置文件:
# 开发时关闭缓存,不然没法看到实时页面
spring.thymeleaf.cache=false
# 用非严格的 HTML
spring.thymeleaf.mode=HTML
spring.thymeleaf.encoding=utf-8
spring.thymeleaf.servlet.content-type=text/html
spring.datasource.druid.url=jdbc:mysql://localhost:3306/shiro?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
spring.datasource.druid.username=root
spring.datasource.druid.password=admin
spring.datasource.druid.initial-size=1
spring.datasource.druid.min-idle=1
spring.datasource.druid.max-active=20
spring.datasource.druid.test-on-borrow=true
#springbootjdbc导入包不和以前一样
spring.datasource.druid.driver-class-name= com.mysql.cj.jdbc.Driver
mybatis.type-aliases-package=com.gt.shiro.entity
mybatis.mapper-locations=classpath:mapper/*.xml
#打印数据库的操作
logging.level.com.example.springsecurity.dao=debug
#redis缓存
### 配置Redis
mybatis.configuration.cache-enabled=true
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=...
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=sunyue
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-idle=200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=1000
Shiro两个重要的配置类:
-
1.UserRealm
package com.gt.shiro.config; import com.gt.shiro.entity.TestUser; import com.gt.shiro.server.TestUserServer; import org.apache.shiro.SecurityUtils; 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.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; public class UserRealm extends AuthorizingRealm { @Autowired private TestUserServer testUserServer; /** * 执行授权逻辑 * * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行授权逻辑"); /*获取当前登录的用户信息*/ Subject subject = SecurityUtils.getSubject(); TestUser testUser = (TestUser) subject.getPrincipal(); //设置角色,多个角色 /*Set<String> rolesSet = new HashSet<>(); rolesSet.add(testUser.getRole());*/ //SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(rolesSet); //给资源进行授权 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); /*可以在以下list加入多个权限*/ /*List<String> roles = new ArrayList<>(); roles.add(testUser.getPerms()); info.addRoles(roles);*/ //设置权限 info.addRole(testUser.getRole()); //需要判断权限是否为空值(null是没有地址,""是有地址但是里面的内容是空的) if (testUser.getPerms() != null && !testUser.getPerms().equals("")) { info.addStringPermission(testUser.getPerms()); } return info; } /** * 执行认证逻辑 * * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("执行认证逻辑"); /*获取令牌*/ UsernamePasswordToken passwordToken = (UsernamePasswordToken) authenticationToken; //取出用户名并且判断用户名是否和数据库一致 TestUser testUser = testUserServer.selectOneByName(passwordToken.getUsername()); if (testUser != null) { //进行认证,将正确数据给shiro处理 //密码不用自己比对,AuthenticationInfo认证信息对象,一个接口,new他的实现类对象SimpleAuthenticationInfo /* 第一个参数随便放,可以放user对象,程序可在任意位置获取 放入的对象 * 第二个参数必须放密码, * 第三个参数放 当前realm的名字,因为可能有多个realm*/ //若密码不正确则返回IncorrectCredentialsException异常 return new SimpleAuthenticationInfo(testUser, testUser.getPassword(), this.getName()); } //若用户名不存在则返回UnknownAccountException异常 return null; } }
-
2.ShiroConfig
package com.gt.shiro.config; import at.pollux.thymeleaf.shiro.dialect.ShiroDialect; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.beans.factory.annotation.Qualifier; 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; @Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //设置安全管理器 shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager); //添加一些Shiro的内置过滤器 /** * Shiro 的内置过滤器可以实现权限的相关拦截 * 常用过滤器 * 1.anon:无需认证 * 2.authc:必须认证才能访问 * 3.user:如果使用rememberme功能可以访问 * 4.perms:对应权限才能访问 * 5.role:对应角色才能访问 */ //登录状态下才可以访问main页面,manage权限可访问manage页面,admin角色可访问admin页面 Map<String, String> filterMap = new LinkedHashMap<String, String>(); filterMap.put("/main", "authc"); filterMap.put("/manage", "perms[manage]"); filterMap.put("/admin", "roles[admin]"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap); //未登录状态下访问将跳转至login页面 // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面 shiroFilterFactoryBean.setLoginUrl("/login"); // 登录成功后要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/"); //无授限状态下访问将请求unauthor shiroFilterFactoryBean.setUnauthorizedUrl("/unAuth"); return shiroFilterFactoryBean; } @Bean(name = "securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) { DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(); //DefaultWebSecurityManager需要关联一个Realm defaultWebSecurityManager.setRealm(userRealm); return defaultWebSecurityManager; } /** * 创建realm */ @Bean(name = "userRealm") public UserRealm getRealm() { return new UserRealm(); } @Bean public ShiroDialect shiroDialect() { return new ShiroDialect(); } /** * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions) * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能 * * @return */ @Bean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } /** * 开启 shiro 的@RequiresPermissions注解 * * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * shiro出现权限异常可通过此异常实现制定页面的跳转(或接口跳转) * * @return */ @Bean public SimpleMappingExceptionResolver simpleMappingExceptionResolver() { SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver(); Properties properties = new Properties(); /*未授权处理页*/ properties.setProperty("org.apache.shiro.authz.UnauthorizedException", "/error.html"); /*身份没有验证*/ properties.setProperty("org.apache.shiro.authz.UnauthenticatedException", "/error.html"); resolver.setExceptionMappings(properties); return resolver; } }
四、数据连接和业务逻辑
-
1.实体类
package com.gt.shiro.entity; import lombok.Data; import lombok.experimental.Accessors; import java.io.Serializable; import java.util.Date; @Data @Accessors(chain = true) public class TestUser implements Serializable { private Integer id; private String username; private String password; /*权限*/ private String perms; /*角色*/ private String role; /*加盐密码*/ private String salt; }
-
2.Dao和Mapper
package com.gt.shiro.dao; import com.gt.shiro.entity.TestUser; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface TestUserMapper { List<TestUser> findAll(); TestUser selectOne(Integer id); TestUser selectOneByName(String username); void insert(TestUser testUser); void update(TestUser testUser); void delete(Integer id); }
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gt.shiro.dao.TestUserMapper">
<select id="findAll" resultType="TestUser">
select * from test_user
</select>
<select id="selectOne" resultType="TestUser">
select * from test_user where id=#{id}
</select>
<select id="selectOneByName" resultType="TestUser">
select * from test_user where username=#{username}
</select>
<insert id="insert">
insert into test_user (id,username,password,perms,role,salt) value (#{id},#{username},#{password},#{perms},#{role},#{salt})
</insert>
<update id="update">
update test_user set username = #{username},password=#{password},perms=#{perms},role=#{role},salt=#{salt} where id = #{id}
</update>
<delete id="delete">
delete from test_user where id = #{id}
</delete>
</mapper>
-
3.业务层及其实现
package com.gt.shiro.server; import com.gt.shiro.entity.TestUser; import org.springframework.stereotype.Service; import java.util.List; @Service public interface TestUserServer { /*查询所有*/ List<TestUser> selectAll(); /*查询一个用户*/ TestUser selectByOne(Integer id); /*通过名字查询一个用户*/ TestUser selectOneByName(String name); /*增加一个用户*/ void insert(TestUser testUser); /*删除一个用户*/ void delete(Integer id); /*更新一个用户*/ void update(TestUser testUser); }
package com.gt.shiro.server.serverImpl;
import com.gt.shiro.dao.TestUserMapper;
import com.gt.shiro.entity.TestUser;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.sunyue.shiro.server.TestUserServer;
import java.util.List;
@Service
public class TestUserServerImpl implements TestUserServer {
@Autowired
private TestUserMapper testUserMapper;
@Override
public List<TestUser> selectAll() {
return testUserMapper.findAll();
}
@Override
public TestUser selectByOne(Integer id) {
return testUserMapper.selectOne(id);
}
@Override
public TestUser selectOneByName(String name) {
return testUserMapper.selectOneByName(name);
}
@Override
public void insert(TestUser testUser) {
//加密写法
String salt = new SecureRandomNumberGenerator().nextBytes().toString();
String password= new SimpleHash("md5",testUser.getPassword(),salt,2).toString();
testUser.setPassword(password);
testUser.setSalt(salt);
testUserMapper.insert(testUser);
}
@Override
public void delete(Integer id) {
testUserMapper.delete(id);
}
@Override
public void update(TestUser testUser) {
testUserMapper.update(testUser);
}
}
-
4.控制层
package com.gt.shiro.controller; import com.gt.shiro.entity.TestUser; import com.gt.shiro.server.TestUserServer; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.crypto.hash.SimpleHash; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; @Controller public class indexController { @Autowired private TestUserServer testUserServer; @GetMapping("/{url}") public String redirect(@PathVariable("url") String url) { return url; } @RequestMapping(value = {"/", "/index"}, method = RequestMethod.GET) private String index() { return "index"; } @PostMapping("/login") public String login(String username, String password, Model model) { Subject subject = SecurityUtils.getSubject(); TestUser testUser = testUserServer.selectOneByName(username); if (testUser != null) { //根据salt值和用户输入的密码计算加密后的密码 String salt = testUser.getSalt(); password = new SimpleHash("md5", password, salt, 2).toString(); System.out.println(password); } UsernamePasswordToken token = new UsernamePasswordToken(username, password); //UsernamePasswordToken token = new UsernamePasswordToken(username, testUser.getPassword());(不加密写法) try { //将用户名和密码通过token传给shiro进行认证 subject.login(token); TestUser user = (TestUser) subject.getPrincipal(); subject.getSession().setAttribute("testUser", user); return "index"; } catch (UnknownAccountException e) { e.printStackTrace(); model.addAttribute("msg", "用户名不存在"); return "login"; } catch (IncorrectCredentialsException e) { e.printStackTrace(); model.addAttribute("msg", "密码有误"); return "login"; } } @ResponseBody @GetMapping("/unauthor") public String unauthor() { return "权限不足,无法访问"; } @GetMapping("/logout") public String logout() { Subject subject = SecurityUtils.getSubject(); subject.logout(); return "login"; } @PostMapping("/register") public String register(TestUser testUser, Model model) { String username = testUser.getUsername(); String password = testUser.getPassword(); if (username ** null || username.equals("")) { model.addAttribute("msg", "用户名不能为空"); return "register"; } else if (password ** null || password.equals("")) { model.addAttribute("msg", "密码不能为空"); return "register"; } else if (testUserServer.selectOneByName(username) != null) { model.addAttribute("msg", "用户名已被占用"); return "register"; } else { testUserServer.insert(testUser); return "login"; } } }
-
5.前端页面
-
(1)index.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.thymrleaf.org/thymeleaf-extras-shiro"> <head> <meta charset="UTF-8"> <title>Insert title here</title> <link rel="shortcut icon" href="#"/> </head> <body> <div th:if="${session.testUser != null}"> <span th:text="'欢迎回来 '+${session.testUser.username}+'! '"> </span><a href="/logout">退出</a> </div> <a href="/main">main</a> <span shiro:hasPermission="manage"> | <a href="/manage">manage</a></span> <span shiro:hasRole="admin"> | <a href="/admin">admin</a></span> <br> </body> </html>
-
(2)login.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Insert title here</title> <link rel="shortcut icon" href="#"/> </head> <body> <form action="/login" method="post"> <span th:text="${msg}" style="color: red"></span> <table> <tr> <td>用户名:</td> <td><input type="text" name="username"/></td> </tr> <tr> <td>密码:</td> <td><input type="password" name="password"/></td> </tr> <tr> <td><input type="submit" value="登录"/></td> <td><a href="/register"> <button type="button" value="注册">注册</button> </a> </td> </tr> </table> </form> </body> </html>
-
(3)register.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Insert title here</title> <link rel="shortcut icon" href="#"/> </head> <body> <form action="/register" method="post"> <span th:text="${msg}" style="color: red"></span> <table> <tr> <td>用户名:</td> <td><input type="text" name="username"/></td> </tr> <tr> <td>密码:</td> <td><input type="password" name="password"/></td> </tr> <tr> <td><input type="submit" value="注册"/></td> </tr> </table> </form> </body> </html>
-
(4)main.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> <link rel="shortcut icon" href="#"/> </head> <body> <h1>main</h1> </body> </html>
-
(5)manage.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> <link rel="shortcut icon" href="#"/> </head> <body> <h1>manage</h1> </body> </html>
-
(6)admin.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> <link rel="shortcut icon" href="#"/> </head> <body> <h1>admin</h1> </body> </html>
-
-
6.数据库文件
/* Navicat MySQL Data Transfer Source Server : sunyue Source Server Version : 50724 Source Host : localhost:3306 Source Database : shiro Target Server Type : MYSQL Target Server Version : 50724 File Encoding : 65001 Date: 2021-01-11 22:00:47 */ SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for test_user -- ---------------------------- DROP TABLE IF EXISTS `test_user`; CREATE TABLE `test_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(120) DEFAULT NULL, `password` varchar(120) DEFAULT NULL, `perms` varchar(120) DEFAULT NULL, `role` varchar(120) DEFAULT NULL, `salt` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4; -- ---------------------------- -- Records of test_user -- ---------------------------- INSERT INTO `test_user` VALUES ('4', 'admin', '4867df2e009d0096c4cd8d9be8cc104c', 'manage', 'admin', 'GQR2m1N1o3nSLjtOzMITRQ**'); INSERT INTO `test_user` VALUES ('5', 'user', '636502f40cf197dd2f4b19f56f475b24', '', '', 'Kxw3HZiFmgnlUu8fmjMY7Q**'); INSERT INTO `test_user` VALUES ('6', 'user1', '43f3133aa7e0ef9cf8373521dff8d8e8', 'manage', null, 'J8fn4HpauvNOrlUaRl/Spg**'); INSERT INTO `test_user` VALUES ('7', '1', '1', 'manage', null, null);