# hr
**Repository Path**: trtan/hr
## Basic Information
- **Project Name**: hr
- **Description**: 微人事项目(SpringBoot练手项目)
- **Primary Language**: Java
- **License**: MulanPSL-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 1
- **Created**: 2021-02-21
- **Last Updated**: 2024-01-18
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 微人事项目开发记录
项目参考《SpringBoot+Vue全栈开发实战_王松2018-12-01.pdf》,以做练手项目。
## 一、创建springboot项目
`https://start.spring.io`可能无法访问,可以修改为aliyu的源`https://start.aliyun.com/`
.assets/image-20210221152215326.png)
一步步往下依次创建好项目
## 二、导入项目所需依赖
当我整合完动态配置权限后才想起要写个文档记录下,虽然会多花费一些时间,但是可以加深印象。
至此:已经整合了redis、mybatis、spring security基于数据库认证及动态权限配置。
```xml
org.springframework.boot
spring-boot-starter-web
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.4
org.springframework.boot
spring-boot-starter-data-redis
io.lettuce
lettuce-core
redis.clients
jedis
com.alibaba
druid
1.2.4
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-cache
org.projectlombok
lombok
1.18.18
provided
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
```
## 三、修改配置文件
为了注释显得好看些,并没有使用.yaml,使用了.properties,一行一行对齐显得好看些。
```properties
# 应用名称
spring.application.name=hr
#单机缓存配置
#Redis缓存名称 也是Key的前缀(默认前缀就是: 缓存名称::)
spring.cache.cache-names=c1,c2
#Redis中Key的过期时间
spring.cache.redis.time-to-live=1800s
#Redis配置
#Redis库编号(0~15)
spring.redis.database=0
#Redis实例地址
spring.redis.host=localhost
#Redis端口号,默认6379
spring.redis.port=6379
#Redis登录密码
spring.redis.password=1234
#Redis连接池最大连接数
spring.redis.jedis.pool.max-active=8
#Redis连接池最大空闲连接数
spring.redis.jedis.pool.max-idle=8
#Redis连接池最大阻塞等待时间,默认为-1,表示没有限制
spring.redis.jedis.pool.max-wait=-1ms
#Redis连接池最小空闲连接数
spring.redis.jedis.pool.min-idle=0
#如果项目使用Lettuce,只需将jedis改为lettuce即可
#数据库连接配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/oa?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=1234
#MyBatis映射文件路径
mybatis.mapper-locations=classpath:dao/*.xml
#spring security 配置
#配置用户名、密码、角色
spring.security.user.name=trtan
spring.security.user.password=1234
spring.security.user.roles=admin
```
## 四、整合Redis
### 配置Redis
暂时没有linux机器,所有只能windows下安装了一个redis。
修改redis.windows.conf文件下列几项值:
```conf
# NOT SUPPORTED ON WINDOWS daemonize no
daemonize yes #是否以守护进程运行(即在后台运行),windows下不支持,所以改为yes也没用
#bind 127.0.0.1 #允许连接该Redis实例的地址,默认只允许本地连接,将其注释掉外网就能连接Redis了
requirepass 1234 #表示登录该Redis所需的密码
protected-mode yes #关闭保护模式时,外部网络可以直接访问;开启保护模式时,需配置bind ip或配置访问密码
```
### 运行Redis
.assets/image-20210221154107900.png)
上面窗口关了redis就会关掉,不过可以把redis作为windows下的一个服务,这样在服务里启动就不用担心关掉窗口导致关了redis了。
```bash
#安装服务
D:\developtools\Redis\redis-server.exe --service-install D:\developtools\Redis\redis.windows.conf --service-name Redis
#启动服务
D:\developtools\Redis\redis-server.exe --service-start --service-name Redis
```
.assets/image-20210221154400345.png)
如果不安装服务的话:
```bash
#手动启动redis
redis-server.exe redis.windows.conf
```
连接Redis:
```bash
#连接redis
redis-cli.exe -p 6379 -a 1234
#或者
redis-cli.exe -p 6379
127.0.0.1:6379>auth 1234
OK
127.0.0.1:6379>
```
.assets/image-20210221155923310.png)
.assets/image-20210221155951100.png)
也可以在桌面写一个bat文件,这样双击就可以运行了:
```bash
@echo off
title redis-server
set ENV_HOME="D:\developtools\Redis"
D:
color 0a
cd %ENV_HOME%
redis-server redis.windows.conf
exit
```
.assets/image-20210221160106761.png)
### Springboot整合Redis
redis所需依赖:
```xml
org.springframework.boot
spring-boot-starter-data-redis
io.lettuce
lettuce-core
redis.clients
jedis
org.springframework.boot
spring-boot-starter-cache
```
application.properties配置文件中添加redis配置
```properties
#单机缓存配置
#Redis缓存名称 也是Key的前缀(默认前缀就是: 缓存名称::)
spring.cache.cache-names=c1,c2
#Redis中Key的过期时间
spring.cache.redis.time-to-live=1800s
#Redis配置
#Redis库编号(0~15)
spring.redis.database=0
#Redis实例地址
spring.redis.host=localhost
#Redis端口号,默认6379
spring.redis.port=6379
#Redis登录密码
spring.redis.password=1234
#Redis连接池最大连接数
spring.redis.jedis.pool.max-active=8
#Redis连接池最大空闲连接数
spring.redis.jedis.pool.max-idle=8
#Redis连接池最大阻塞等待时间,默认为-1,表示没有限制
spring.redis.jedis.pool.max-wait=-1ms
#Redis连接池最小空闲连接数
spring.redis.jedis.pool.min-idle=0
#如果项目使用Lettuce,只需将jedis改为lettuce即可
```
### 测试
添加好Redis配置后,springboot就可以直接使用Redis了
```java
@SpringBootTest
class HrApplicationTests {
/**
* StringRedisTemplate是RedisTemplate的子类,StringRedisTemplate中的key、value都是字符串
* Redis连接测试
*/
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
void redisConnectTest() {
stringRedisTemplate.getConnectionFactory().getConnection().ping();
//StringRedisTemplate通过opsForValue获取一个对象,再使用操作对象完成数据的读取
ValueOperations ops1 = stringRedisTemplate.opsForValue();
ops1.set("name", "trtan");
System.out.println(ops1.get("name"));
}
}
```
.assets/image-20210221160849144.png)
在外面使用redis-cli连接获取一下刚刚set的值
```
#切换数据库
select 0
# 获取数据
get name
```
.assets/image-20210221161104652.png)
至此,单机Redis整合完成,redis集群整合也很简单,springboot配置这边只需要修改配置文件即可,如果后面涉及到redis集群再进行修改。
.assets/image-20210221162025813.png)
## 五、整合MyBatis
### 建库建表
首先要构建的是资源访问权限的控制,因此mysql需要先创建这几个表
.assets/image-20210221162435762.png)
### 依赖及配置
整合mybatis需要导入依赖
```xml
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.4
com.alibaba
druid
1.2.4
mysql
mysql-connector-java
```
为了创建实体类方便,添加lombok依赖,使用注解可省略getter、setter等方法的编写
```xml
org.projectlombok
lombok
1.18.18
provided
```
由于src/main/java下只会编译java文件,其它文件会被忽略,`pom.xml`再添加如下配置
```
src/main/java
**/*.properties
**/*.xml
true
src/main/resources
**/*.properties
**/*.xml
```
修改`application.properties`
```properties
#数据库连接配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/oa?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=1234
#MyBatis映射文件路径
mybatis.mapper-locations=classpath:dao/*.xml
```
这里xml文件还是放在src/main/resources的dao目录下
### 创建实体类及数据访问层
这里可以使用mybatis插件一键创建两层模型
.assets/image-20210221164032574.png)
需要自己手动选择package,不然会创建与src/main/java同级的包
.assets/image-20210221163942760.png)
按照上图选择,生成的RoleDao类会提供默认六个方法:
.assets/image-20210221164248472.png)
这里注意:
**如果只使用@Repository注解,需要在启动类加上@MapperScan("com.trtan.hr.dao")注解,才能扫到RoleDao;可以只使用@Mapper或者同时使用@Mapper和@Repository,不过只使用@Mapper,IDEA在@Autowired自动注入RoleDao时会有红色波浪线**。
.assets/image-20210221164824374.png)
同时也会创建一个RoleDao.xml文件
.assets/image-20210221165008973.png)
以及生成Role实体类
.assets/image-20210221165120965.png)
### 测试
```java
/**
* Mysql连接测试
*/
@Autowired
SysmsgDao sysmsgDao;
@Test
void mysqlConnectTest() {
sysmsgDao.selectByPrimaryKey(1);
}
```
.assets/image-20210221165406213.png)
测试通过,成功执行sysmsgDao的selectByPrimaryKey()方法。
## 六、整合Spring Security
### 依赖及配置
只需加入一个依赖
```xml
org.springframework.boot
spring-boot-starter-security
```
添加依赖后启动项目访问任意页面会自动跳转到`/login`,这是Spring Security提供的默认登录页面。
.assets/image-20210221170007403.png)
启动时控制台会打印一个密码,默认用户名是user,输入用户名密码后就看正常进入页面。
.assets/image-20210221170319654.png)
可以自己定义用户名密码以及角色
```properties
#spring security 配置
#配置用户名、密码、角色
spring.security.user.name=trtan
spring.security.user.password=1234
spring.security.user.roles=admin
```
这时重新启动项目就可以用新的账户密码登录了。
### 自定义Spring Security配置类
创建一个类WebSecurityConfig继承自WebSecurityConfigurerAdapter
```java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
//为了方便,暂时使用未加密的密码
return NoOpPasswordEncoder.getInstance();
}
/**
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//将用户名、密码、角色写入内存进行认证
auth.inMemoryAuthentication().withUser("root").password("123").roles("admin", "dba");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //开启HttpSecurity的配置
//基于HttpSecurity配置认证授权,缺点:不够灵活,无法实现资源和角色之间的动态调整
.antMatchers("/admin/**").hasRole("admin") //用户访问 "/admin/**"需要的角色
.antMatchers("/db/**").hasRole("dba") //用户访问 "/db/**"需要的角色
.antMatchers("/user/**").hasRole("user") //用户访问 "/user/**"需要的角色
.anyRequest().authenticated() //表示前面定义的url模式外,用户访问其它任何url都需要认证(登录)后访问
.and()
.formLogin()//开启表单登录,不开启无法访问登录页面
.and()
.csrf().disable();//关闭csrf
}
/**
* 已经定义了三种角色ROLE_dba、ROLE_admin、ROLE_user
* 但是这三种角色现在没有任何关系,可以在spring security配置类中提供一个RoleHierarchy描述这种继承关系
* 下面ROLE_dba可以访问ROLE_admin及ROLE_user的资源了,ROLE_admin可以访问ROLE_user可以访问的资源了
*
* 注意:动态配置权限后,权限继承关系会失效,需要在数据库中定义
* @return
*/
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ROLE_dba > ROLE_admin > ROLE_user";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
}
```
以上配置可以实现固定的几个用户、几个角色、几个菜单模式的访问关系,比如上面的root用户使用密码123登录,具有admin以及dba的权限,所以可以访问`/admin/**`、`/dba/**`路径下的所有资源。但是加上下面的roleHierarchy配置后,角色之间具有继承关系了,dba具有admin以及user角色的权限,admin具有user角色的权限。因此现在的root用户具有admin、dba、user三种角色,登录后可以访问所有资源。
## 七、动态配置权限
### 数据库插入数据
hr表,作为登录的用户表,暂时插入三个用户用作测试
.assets/image-20210221171939901.png)
role表,三种角色
.assets/image-20210221172026813.png)
hr角色关系表,hr_role
.assets/image-20210221172120649.png)
菜单模式表menu,暂定将url存储pattern模式串
.assets/image-20210221172207325.png)
菜单角色关系表,menu_role
.assets/image-20210221172255473.png)
### 从数据库进行用户认证
之前是将用户名、密码、角色写入内存中进行认证
```java
auth.inMemoryAuthentication().withUser("root").password("123").roles("admin", "dba");
```
现在需要从数据库读取用户名、密码以及角色进行权限的认证。
```java
/**
* 将创建好的hrService配置到AuthenticationManagerBuilder里去
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//将用户名、密码、角色写入内存进行认证
//auth.inMemoryAuthentication().withUser("root").password("123").roles("admin", "dba");
//从数据库进行认证
auth.userDetailsService(hrService);
}
```
将hrService配置到AuthenticationManagerBuilder去后剩下的事情就交给Spring Security实现了。
.assets/image-20210221174129920.png)
根据泛型的上边界通配符,这里userDetailsService()方法需要传入一个UserDetailsService或其子类的实例。
.assets/image-20210221174344788.png)
即业务层HrService需要实现UserDetailService的loadUserByUsername方法,对于HrService来说,要根据username查询对应的hr数据。
首先对实体类Hr需要实现UserDetails类的7个方法
```java
@Data
public class Hr implements UserDetails {
private Integer id;
private String name;
private String phone;
private String telephone;
private String address;
private boolean enabled;
private String username;
private String password;
private String userface;
private String remark;
private List roles;
/**
* 获取当前用户对象所具有的角色信息
* @return
*/
@Override
public Collection extends GrantedAuthority> getAuthorities() {
List authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
/**
* 获取当前用户对象的密码
* @return
*/
@Override
public String getPassword() {
return password;
}
/**
* 获取当前用户对象的名称
* @return
*/
@Override
public String getUsername() {
return username;
}
/**
* 当前账户是否未过期
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 当前账户是否未锁定
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 当前账户密码是否未过期
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 当前账户密码是否可用
* @return
*/
@Override
public boolean isEnabled() {
return enabled;
}
}
```
对dao数据访问层提供两个方法,一是根据用户名称查询对应用户,二是根据查到的用户通过用户id查到所具有的角色。
那么service业务层将查询到的角色roles通过setter方法注入到Hr实体类中
```java
@Mapper
@Repository
public interface HrDao {
Hr loadUserByUsername(String username);
List getUserRoleByUid(Integer id);
}
```
```xml
```
```java
@Service
public class HrService implements UserDetailsService {
@Autowired
HrDao hrDao;
/**
* loadUserByUsername在用户登录时会被自动调用
* 流程:用户登录时输入用户名密码->通过用户名去数据库查找用户(没有查找则抛出账户不存在异常)->
* 用户存在则查找用户的角色信息->将获取到的用户hr对象返回->DaoAuthenticationProvider类对比密码是否正确
* @param username 登录时输入的用户名
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Hr hr = hrDao.loadUserByUsername(username);
if (hr == null) {
throw new UsernameNotFoundException("账户不存在");
}
hr.setRoles(hrDao.getUserRoleByUid(hr.getId()));
return hr;
}
}
```
通过以上代码就能实现从数据库进行用户权限的认证了。
Spring Security配置文件
```java
package com.trtan.hr.config;
import com.trtan.hr.service.HrService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;
import sun.plugin2.applet.context.NoopExecutionContext;
@Configuration
/**
* 可以开启基于注解的安全配置
* prePostEnabled=true 会解锁@PreAuthorize和@PostAuthorize两个注解
* @PreAuthorize 会在方法执行前进行验证
* @PostAuthorize 会在方法执行后进行验证
* secureEnabled=true 会解锁@Secured注解
*/
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
HrService hrService;
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
/**
* 将创建好的hrService配置到AuthenticationManagerBuilder里去
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//将用户名、密码、角色写入内存进行认证
//auth.inMemoryAuthentication().withUser("root").password("123").roles("ADMIN", "DBA");
//从数据库进行认证
auth.userDetailsService(hrService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //开启HttpSecurity的配置
//基于HttpSecurity配置认证授权,缺点:不够灵活,无法实现资源和角色之间的动态调整
.antMatchers("/admin/**").hasRole("admin") //用户访问 "/admin/**"需要的角色
.antMatchers("/db/**").hasRole("dba") //用户访问 "/db/**"需要的角色
.antMatchers("/user/**").hasRole("user") //用户访问 "/user/**"需要的角色
//将自定义的两个实例设置进去,即可实现动态配置权限
.anyRequest().authenticated() //表示前面定义的url模式外,用户访问其它任何url都需要认证(登录)后访问
.and()
.formLogin()//开启表单登录,不开启无法访问登录页面
.and()
.csrf().disable();//关闭csrf
}
/**
* 已经定义了三种角色ROLE_dba、ROLE_admin、ROLE_user
* 但是这三种角色现在没有任何关系,可以在spring security配置类中提供一个RoleHierarchy描述这种继承关系
* 下面ROLE_dba可以访问ROLE_admin及ROLE_user的资源了,ROLE_admin可以访问ROLE_user可以访问的资源了
*
* 注意:动态配置权限后,权限继承关系会失效,需要在数据库中定义
* @return
*/
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ROLE_dba > ROLE_admin > ROLE_user";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
}
```
配置类仅仅是将从内存的认证改为`auth.userDetailsService(hrService);`从数据库认证,然后提供所需的HrService、Hr、HrDao类,剩下的只需交给Spring Security帮我们去做。
### 动态配置权限
现在用户已经可以从数据库里读取了,但是资源和角色之间的关系还是在代码里写死的
.assets/image-20210221180637675.png)
要实现动态配置权限,还需要自定义FilterInvocationSecurityMetadataSource以及AccessDecisionManager,再将这两个类配置到Spring Security的配置类里。
```java
/**
* 实现动态配置资源访问权限(菜单访问权限)
* FilterInvocationSecurityMetadataSource默认实现类是DefaultFilterInvocationSecurityMetadataSource
* 可以参考DefaultFilterInvocationSecurityMetadataSource来定义自己的FilterInvocationSecurityMetadataSource
*/
@Component
public class CustomFilterInvocationSecurityMetadataSource
implements FilterInvocationSecurityMetadataSource {
//主要用来实现ant风格的url匹配
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Autowired
MenuDao menuDao;
/**
* 确定一个请求需要哪些角色
* @param o
* @return
* @throws IllegalArgumentException
*/
@Override
public Collection getAttributes(Object o) throws IllegalArgumentException {
//从FilterInvocation实例中提取请求的url
String requestUrl = ((FilterInvocation) o).getRequestUrl();
//从数据库获取所有的资源信息(菜单模式),也可以改用从缓存中获取
List