1,什么是 MyBatis
MyBatis 是一款优秀的持久化框架,原名叫做 iBatis,2010 年改名为 MyBatis。
MyBatis 支持定制化 SQL、存储过程以及高级映射。MyBatis 几乎避免了所有的 JDBC 代码手动设置参数以及获取结果集。
在传统的 SSM 框架整合中,使用 MyBatis 需要大量的 XML 配置。而在 Spring Boot 中,MyBatis 官方提供了一套自动化配置方案,可以做到 MyBatis 开箱即用。
2,安装配置
mybatis-spring-boot-starter:MyBatis 依赖
mysql-connector-java:MySQL 数据库驱动
druid:Druid 是阿里巴巴开发的号称为监控而生的数据库连接池,也是目前最好的数据库连接池。
(1).pom文件引入mybatis依赖
<!-- MyBatis依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!-- 数据库驱动依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.9</version>
</dependency>
(2)接着在 application.properties 中配置数据库连接信息:
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/security?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456
mybatis.mapper-locations=classpath:mapper/*.xml
(3)MySQL创建表
/*
/*
Navicat Premium Data Transfer
Source Server : 本地MySQL
Source Server Type : MySQL
Source Server Version : 50728
Source Host : localhost:3306
Source Schema : security
Target Server Type : MySQL
Target Server Version : 50728
File Encoding : 65001
Date: 10/12/2020 10:11:39
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for menu
-- ----------------------------
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
`id` int(11) NOT NULL,
`pattern` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of menu
-- ----------------------------
INSERT INTO `menu` VALUES (1, '/root/**');
INSERT INTO `menu` VALUES (2, '/admin/**');
INSERT INTO `menu` VALUES (3, '/user/**');
-- ----------------------------
-- Table structure for menu_role
-- ----------------------------
DROP TABLE IF EXISTS `menu_role`;
CREATE TABLE `menu_role` (
`id` int(11) NOT NULL,
`mid` int(11) NULL DEFAULT NULL,
`rid` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of menu_role
-- ----------------------------
INSERT INTO `menu_role` VALUES (1, 1, 1);
INSERT INTO `menu_role` VALUES (2, 2, 2);
INSERT INTO `menu_role` VALUES (3, 3, 3);
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL,
`name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`nameZh` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'ROLE_DBA', '数据库管理员');
INSERT INTO `role` VALUES (2, 'ROLE_ADMIN', '系统管理员');
INSERT INTO `role` VALUES (3, 'ROLE_USER', '用户');
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL,
`username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`enabled` tinyint(255) NULL DEFAULT NULL,
`locked` tinyint(255) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'root', '1234', 1, 0);
INSERT INTO `user` VALUES (2, 'admin', '1234', 1, 0);
INSERT INTO `user` VALUES (3, 'kelly', '1234', 1, 0);
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int(11) NOT NULL,
`uid` int(11) NULL DEFAULT NULL,
`rid` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1, 1);
INSERT INTO `user_role` VALUES (2, 1, 2);
INSERT INTO `user_role` VALUES (3, 2, 2);
INSERT INTO `user_role` VALUES (4, 3, 3);
SET FOREIGN_KEY_CHECKS = 1;
3.创建实体类
pom文件引入lombok
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
</dependency>
(1)首先创建一个角色和资源表对应的实体类。
package com.example.securitydemo.model;
import lombok.Setter;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Setter
@Getter
@NoArgsConstructor
public class Role {
private Integer id;
private String name;
private String nameZh;
}
package com.example.securitydemo.model;
import java.util.List;
import lombok.Setter;
import lombok.Getter;
@Setter
@Getter
public class Menu {
private Integer id;
private String pattern;
private List<Role> roles;
}
(2)接着创建用户表对应的实体类。用户实体类需要实现 UserDetails 接口,并实现该接口中的 7 个方法:
getAuthorities():获取当前用户对象所具有的角色信息
getPassword():获取当前用户对象的密码
getUsername():获取当前用户对象的用户名
isAccountNonExpired():当前账户是否未过期
isAccountNonLocked():当前账户是否未锁定
isCredentialsNonExpired():当前账户密码是否未过期
isEnabled():当前账户是否可用
(1)用户根据实际情况设置这 7 个方法的返回值。默认情况下不需要开发者自己进行密码角色等信息的比对,开发者只需要提供相关信息即可,例如:
getPassword() 方法返回的密码和用户输入的登录密码不匹配,会自动抛出 BadCredentialsException 异常
isAccountNonLocked() 方法返回了 false,会自动抛出 AccountExpiredException 异常。
本案例因为数据库中只有 enabled 和 locked 字段,故账户未过期和密码未过期两个方法都返回 true.
(2)getAuthorities 方法用来获取当前用户所具有的角色信息,本案例中,用户所具有的角色存储在 roles 属性中,因此该方法直接遍历 roles 属性,然后构造 SimpleGrantedAuthority 集合并返回。
package com.example.securitydemo.model;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.userdetails.UserDetails;
@NoArgsConstructor
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Boolean locked;
private List<Role> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return !locked;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
/** get、set 方法 **/
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public Boolean getLocked() {
return locked;
}
public void setLocked(Boolean locked) {
this.locked = locked;
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
}
(3)创建 UserMapper 接口和MenuMapper 接口:
package com.example.securitydemo.mapper;
import com.example.securitydemo.model.Role;
import com.example.securitydemo.model.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface UserMapper {
User loadUserByUsername(String username);
List<Role> getUserRolesByUid(Integer id);
}
package com.example.securitydemo.mapper;
import org.apache.ibatis.annotations.Mapper;
import com.example.securitydemo.model.Menu;
import java.util.List;
@Mapper
public interface MenuMapper {
List<Menu> getAllMenus();
}
(4)创建 UserMapper.xml 文件和 MenuMapper.xml 文件
<?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.example.securitydemo.mapper.UserMapper">
<select id="loadUserByUsername" resultType="com.example.securitydemo.model.User" parameterType="java.lang.String" >
select * from user where username=#{username,jdbcType=VARCHAR}
</select>
<select id="getUserRolesByUid" resultType="com.example.securitydemo.model.Role" parameterType="java.lang.Integer" >
select * from role r,user_role ur where r.id=ur.rid and ur.uid=#{id,jdbcType=INTEGER}
</select>
</mapper>
<?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.example.securitydemo.mapper.MenuMapper">
<resultMap id="BaseResultMap" type="com.example.securitydemo.model.Menu">
<id property="id" column="id"/>
<result property="pattern" column="pattern"/>
<collection property="roles" ofType="com.example.securitydemo.model.Role">
<id property="id" column="rid"/>
<result property="name" column="rname"/>
<result property="nameZh" column="rnameZh"/>
</collection>
</resultMap>
<select id="getAllMenus" resultMap="BaseResultMap">
SELECT m.*,r.id AS rid,r.name AS rname,r.nameZh AS rnameZh FROM menu m LEFT JOIN menu_role mr ON m.`id`=mr.`mid` LEFT JOIN role r ON mr.`rid`=r.`id`
</select>
</mapper>
(5)自定义 FilterInvocationSecurityMetadataSource
要实现动态配置权限,首先需要自定义 FilterInvocationSecurityMetadataSource:
注意:自定义 FilterInvocationSecurityMetadataSource 主要实现该接口中的 getAttributes 方法,该方法用来确定一个请求需要哪些角色。
package com.example.securitydemo.security;
import com.example.securitydemo.mapper.MenuMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import com.example.securitydemo.model.Menu;
import com.example.securitydemo.model.Role;
import java.util.Collection;
import java.util.List;
@Component
public class CustomFilterInvocationSecurityMetadataSource
implements FilterInvocationSecurityMetadataSource {
// 创建一个AnipathMatcher,主要用来实现ant风格的URL匹配。
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Autowired
MenuMapper menuMapper;
@Override
public Collection<ConfigAttribute> getAttributes(Object object)
throws IllegalArgumentException {
// 从参数中提取出当前请求的URL
String requestUrl = ((FilterInvocation) object).getRequestUrl();
// 从数据库中获取所有的资源信息,即本案例中的menu表以及menu所对应的role
// 在真实项目环境中,开发者可以将资源信息缓存在Redis或者其他缓存数据库中。
List<Menu> allMenus = menuMapper.getAllMenus();
// 遍历资源信息,遍历过程中获取当前请求的URL所需要的角色信息并返回。
for (Menu menu : allMenus) {
if (antPathMatcher.match(menu.getPattern(), requestUrl)) {
List<Role> roles = menu.getRoles();
String[] roleArr = new String[roles.size()];
for (int i = 0; i < roleArr.length; i++) {
roleArr[i] = roles.get(i).getName();
}
return SecurityConfig.createList(roleArr);
}
}
// 如果当前请求的URL在资源表中不存在相应的模式,就假设该请求登录后即可访问,即直接返回 ROLE_LOGIN.
return SecurityConfig.createList("ROLE_LOGIN");
}
// 该方法用来返回所有定义好的权限资源,Spring Security在启动时会校验相关配置是否正确。
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
// 如果不需要校验,那么该方法直接返回null即可。
return null;
}
// supports方法返回类对象是否支持校验。
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
(6),创建 UserService
package com.example.securitydemo.service;
import com.example.securitydemo.mapper.UserMapper;
import com.example.securitydemo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("账户不存在!");
}
user.setRoles(userMapper.getUserRolesByUid(user.getId()));
return user;
}
}
(7),配置 Spring Security
package com.example.securitydemo.security;
import com.example.securitydemo.manager.CustomAccessDecisionManager;
import com.example.securitydemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserService userService;
// 指定密码的加密方式
@SuppressWarnings("deprecation")
@Bean
PasswordEncoder passwordEncoder(){
// 不对密码进行加密
return NoOpPasswordEncoder.getInstance();
}
// 配置用户及其对应的角色
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
// 配置 URL 访问权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setSecurityMetadataSource(cfisms());
object.setAccessDecisionManager(cadm());
return object;
}
})
.and().formLogin().loginProcessingUrl("/login").permitAll()//开启表单登录并配置登录接口
.and().csrf().disable(); // 关闭csrf
}
@Bean
CustomFilterInvocationSecurityMetadataSource cfisms() {
return new CustomFilterInvocationSecurityMetadataSource();
}
@Bean
CustomAccessDecisionManager cadm() {
return new CustomAccessDecisionManager();
}
}
(8).接口TestController
package com.example.securitydemo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("/hello")
public String hello() {
return "欢迎访问 security-demo !";
}
@GetMapping("/admin/hello")
public String admin() {
return "hello admin";
}
@GetMapping("/user/hello")
public String user() {
return "hello user";
}
@GetMapping("/root/hello")
public String db() {
return "hello root";
}
}
重启运行浏览器访问
(1)启动项目,我们使用 kelly用户进行登录,由于该用户具有 USER 角色,所以登录后可以访问 /hello、 /user/hello 这两个接口。
(2)而由于 /root/hello 接口需要 DBA 角色,因此 kelly用户仍然无法访问。
(3)我们使用 admin 用户进行登录,由于该用户具有 ADMIN 和 USER 这两个角色,所以登录后可以访问 /hello、/admin/hello 接口。