# 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/` ![image-20210221152215326](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).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 ​ ![image-20210221154107900](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).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 ``` ​ ![image-20210221154400345](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).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> ``` ​ ![image-20210221155923310](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).assets/image-20210221155923310.png) ​ ![image-20210221155951100](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).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 ``` ![image-20210221160106761](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).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")); } } ``` ![image-20210221160849144](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).assets/image-20210221160849144.png) 在外面使用redis-cli连接获取一下刚刚set的值 ``` #切换数据库 select 0 # 获取数据 get name ``` ​ ![image-20210221161104652](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).assets/image-20210221161104652.png) 至此,单机Redis整合完成,redis集群整合也很简单,springboot配置这边只需要修改配置文件即可,如果后面涉及到redis集群再进行修改。 ![image-20210221162025813](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).assets/image-20210221162025813.png) ## 五、整合MyBatis ### 建库建表 首先要构建的是资源访问权限的控制,因此mysql需要先创建这几个表 ![image-20210221162435762](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).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插件一键创建两层模型 ​ ![image-20210221164032574](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).assets/image-20210221164032574.png) 需要自己手动选择package,不然会创建与src/main/java同级的包 ![image-20210221163942760](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).assets/image-20210221163942760.png) 按照上图选择,生成的RoleDao类会提供默认六个方法: ​ ![image-20210221164248472](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).assets/image-20210221164248472.png) 这里注意: **如果只使用@Repository注解,需要在启动类加上@MapperScan("com.trtan.hr.dao")注解,才能扫到RoleDao;可以只使用@Mapper或者同时使用@Mapper和@Repository,不过只使用@Mapper,IDEA在@Autowired自动注入RoleDao时会有红色波浪线**。 ​ ![image-20210221164824374](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).assets/image-20210221164824374.png) 同时也会创建一个RoleDao.xml文件 ![image-20210221165008973](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).assets/image-20210221165008973.png) 以及生成Role实体类 ​ ![image-20210221165120965](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).assets/image-20210221165120965.png) ### 测试 ```java /** * Mysql连接测试 */ @Autowired SysmsgDao sysmsgDao; @Test void mysqlConnectTest() { sysmsgDao.selectByPrimaryKey(1); } ``` ![image-20210221165406213](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).assets/image-20210221165406213.png) 测试通过,成功执行sysmsgDao的selectByPrimaryKey()方法。 ## 六、整合Spring Security ### 依赖及配置 只需加入一个依赖 ```xml org.springframework.boot spring-boot-starter-security ``` 添加依赖后启动项目访问任意页面会自动跳转到`/login`,这是Spring Security提供的默认登录页面。 ![image-20210221170007403](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).assets/image-20210221170007403.png) 启动时控制台会打印一个密码,默认用户名是user,输入用户名密码后就看正常进入页面。 ![image-20210221170319654](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).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表,作为登录的用户表,暂时插入三个用户用作测试 ![image-20210221171939901](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).assets/image-20210221171939901.png) role表,三种角色 ![image-20210221172026813](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).assets/image-20210221172026813.png) hr角色关系表,hr_role ![image-20210221172120649](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).assets/image-20210221172120649.png) 菜单模式表menu,暂定将url存储pattern模式串 ![image-20210221172207325](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).assets/image-20210221172207325.png) 菜单角色关系表,menu_role ![image-20210221172255473](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).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实现了。 ![image-20210221174129920](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).assets/image-20210221174129920.png) 根据泛型的上边界通配符,这里userDetailsService()方法需要传入一个UserDetailsService或其子类的实例。 ![image-20210221174344788](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).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 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帮我们去做。 ### 动态配置权限 现在用户已经可以从数据库里读取了,但是资源和角色之间的关系还是在代码里写死的 ![image-20210221180637675](%E5%BE%AE%E4%BA%BA%E4%BA%8B(springboot+vue).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 menus = menuDao.getAllMenus(); //遍历所有的菜单模式 for (Menu menu : menus) { //判断请求url是否匹配菜单模式 if (antPathMatcher.match(menu.getUrl(), requestUrl)) { //获取菜单模式所有角色 List 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在数据库菜单表中不存在相应的模式,则登陆后即可访问,给与login角色 return SecurityConfig.createList("ROLE_login"); } /** * 返回所有定义好的角色资源 * spring security在启动时会校验相关配置是否正确 * 如果不需要校验,返回null即可 * @return */ @Override public Collection getAllConfigAttributes() { return null; } /** * 判断返回类对象是否支持校验 * @param aClass * @return */ @Override public boolean supports(Class aClass) { return FilterInvocation.class.isAssignableFrom(aClass); } } ``` ```java /** * 当请求走完FilterInvocationSecurityMetadataSource的getAttributes方法后 * 就会来到AccessDecisionManager类中进行角色信息的比对 */ @Component public class CustomAccessDecisionManager implements AccessDecisionManager { /** * * @param authentication 包含当前登录用户的信息 * @param o 一个FilterInvocation对象可以获得当前请求对象等 * @param collection FilterInvocationSecurityMetadataSource中getAttributes方法的返回值,即包含请求资源(菜单模式)所需的角色 * @throws AccessDeniedException * @throws InsufficientAuthenticationException */ @Override public void decide(Authentication authentication, Object o, Collection collection) throws AccessDeniedException, InsufficientAuthenticationException { //获取登录用户的角色信息 Collection authorities = authentication.getAuthorities(); //遍历该资源(菜单模式)所需的角色,任意满足一个即可访问((FilterInvocation) o).getRequestUrl(); for (ConfigAttribute configAttribute : collection) { /** * 如果是不在菜单表中的资源(即getAttributes返回的是"ROLE_login") * 并且authentication是UsernamePasswordAuthenticationToken的实例(用户已经登录) * 则该资源登录即可访问,至此结束 */ if (("ROLE_login".equals(configAttribute.getAttribute()) && authentication instanceof UsernamePasswordAuthenticationToken)) { return; } //若该用户具有任意访问该资源所需的角色即正常结束 for(GrantedAuthority authority : authorities) { if (configAttribute.getAttribute().equals(authority.getAuthority())) { return; } } } //用户未登录(无任何角色)或不具有访问该资源所需的角色 throw new AccessDeniedException("权限不足!"); } @Override public boolean supports(ConfigAttribute configAttribute) { return true; } @Override public boolean supports(Class aClass) { return true; } } ``` ```java @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/**"需要的角色 //将自定义的两个实例设置进去,即可实现动态配置权限 .withObjectPostProcessor(new ObjectPostProcessor() { @Override public O postProcess(O o) { o.setSecurityMetadataSource(cfisms()); o.setAccessDecisionManager(cadm()); return o; } }) .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; } /** * 在spring security配置类中配置两个自定义类 * @return */ @Bean CustomFilterInvocationSecurityMetadataSource cfisms() { return new CustomFilterInvocationSecurityMetadataSource(); } /** * 在spring security配置类中配置两个自定义类 * @return */ @Bean CustomAccessDecisionManager cadm() { return new CustomAccessDecisionManager(); } } ``` 当配置好后,修改menu_role即可动态配置权限。 至此,今天所写的都用文档记录完了,之后每写一部分,都会以文档记录回顾一遍。(写文档真耗时间,写了一下午... 写文档时间>写项目时间)。

2021/02/21 18:46

--- ## 八、自定义响应码 今天又是新的一天,下午回来吃完晚饭就7点多了,我也是一坐下来就想着抓紧时间写一写,能写多少算多少,玩手机太消磨时间了。 经过昨天的实现,已经可以动态权限认证了,现在访问接口只能看到404、403这些默认的响应信息。 比如登陆后访问没有权限访问的页面: ![image-20210222204533089](README.assets/image-20210222204533089.png) 或者登录成功后: ![image-20210222204828142](README.assets/image-20210222204828142.png) 这些响应信息都是springboot提供的默认格式的信息。 所以我们可以定义自己的响应信息,在登录成功或失败的时候提示不一样的信息。 首先,创建一个响应信息类: ```java package com.trtan.hr.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * 自定义响应类 */ @Data @AllArgsConstructor @NoArgsConstructor public class RespBean { private Integer status; private String msg; private Object object; public static RespBean build() { return new RespBean(); } public static RespBean ok(String msg) { return new RespBean(200, msg, null); } public static RespBean ok(String msg, Object object) { return new RespBean(200, msg, object); } public static RespBean error(String msg) { return new RespBean(500, msg, null); } public static RespBean error(String msg, Object object) { return new RespBean(500, msg, object); } public RespBean setObject(Object object) { this.object = object; return this; } } ``` 再对Spring Security的HttpSecurity中配置登录成功或失败的响应 ```java //WebSecurityConfig.java /** * 配置拦截规则,表单登录,登陆成功或失败的响应 * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() //开启HttpSecurity的配置 .withObjectPostProcessor(new ObjectPostProcessor() { @Override public O postProcess(O o) { o.setSecurityMetadataSource(filterInvocationSecurityMetadataSource); o.setAccessDecisionManager(accessDecisionManager); return o; } }) .and() .formLogin().loginPage("/login_p").loginProcessingUrl("/login") .usernameParameter("username").passwordParameter("password") .failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=UTF-8"); RespBean respBean = null; if (e instanceof BadCredentialsException || e instanceof UsernameNotFoundException) { respBean = RespBean.error("账户名或密码输入错误!"); } else if (e instanceof LockedException) { respBean = RespBean.error("账户被锁定,请联系管理员!"); } else if (e instanceof AccountExpiredException) { respBean = RespBean.error("账户过期,请联系管理员!"); } else if (e instanceof DisabledException) { respBean = RespBean.error("账户被禁用,请联系管理员!"); } else { respBean = RespBean.error("登录失败!"); } httpServletResponse.setStatus(401); PrintWriter out = httpServletResponse.getWriter(); //对象转JSON字符串 out.write(new ObjectMapper().writeValueAsString(respBean)); out.flush(); out.close(); } }) .successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=UTF-8"); RespBean respBean = RespBean.ok("登录成功!", Hr.getCurrentHr()); PrintWriter out = httpServletResponse.getWriter(); //对象转JSON字符串 out.write(new ObjectMapper().writeValueAsString(respBean)); out.flush(); out.close(); } }) .permitAll() .and() .logout().permitAll() .and() .csrf().disable() //关闭csrf .exceptionHandling().accessDeniedHandler(accessDeniedHandler); //配置异常处理 } ``` `accessDeniedHandler`可以配置用户访问没有访问权限资源时返回的响应信息 ```java @Component public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); //403 禁止访问 httpServletResponse.setContentType("application/json;charset=UTF-8"); PrintWriter out = httpServletResponse.getWriter(); RespBean error = RespBean.error("权限不足,请联系管理员"); //对象转JSON字符串 out.write(new ObjectMapper().writeValueAsString(error)); out.close(); } } ``` ```java /** * 获取当前登录用户 * @return */ public static Hr getCurrentHr() { return (Hr) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); } ``` 现在重新启动项目,登录成功即可返回想返回的信息 ![image-20210222210205149](README.assets/image-20210222210205149.png) 登录失败时返回的信息 ![image-20210222210247440](README.assets/image-20210222210247440.png) 访问无权访问的资源 ![image-20210222210419437](README.assets/image-20210222210419437.png) 在这之后遇到了一个问题,根据文档配置好需要忽略认证的资源后 ![image-20210222214924345](README.assets/image-20210222214924345.png) 然后访问,发现根本不行,还是跳转到了登录页面,在这中间找了好几种方法,最终还是用一种最简单的方法放行了。 ```java http.authorizeRequests() //开启HttpSecurity的配置 .antMatchers("/login_p", "/index.html", "/static/**").permitAll().anyRequest().authenticated() .and() ``` 然后即可自定义不登录时想访问的资源了。 今天就写到这了,主要是遇到了好几个坑,毕竟版本还是有些差别的,一些改动可能就会导致一些方法不适用。

2021/2/22 21:53

--- 今天太晚了,写个锤子,写完leetcode题已经22点多了,本来想实现前端登录页面并跳转的,时间不太够了,明晚整!

2021/2/23 22:08

--- ## 九、自定义登录页面 现在都是讲究前后端分离,当时毕设还是用的前后端分离(SSM+Vue),对Vue还有点印象,正好这次再回忆一下。 那么今晚就用Vue先写个前端页面,然后跨域请求后端接口实现登录。 首先本地需要安装了NodeJs,然后再cmd下执行( ```bash npm install -g vue-cli # 会在执行的路径下生成一个名为vuehr的项目 vue init webpack vuehr cd vuehr npm run dev ``` 根据提示一步步构建完即可,如果`npm run dev`成功运行,访问即可看到如下页面 ![image-20210224222427102](README.assets/image-20210224222427102.png) 接着安装ElementUI和Axios ```bash npm i element-ui -S npm i axios -S ``` 之后在main.js中引入Element ```js import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' Vue.use(ElementUI) ``` 然后封装网络请求 自己创建一个util包,新建api.js,也可以自定义消息myMessage.js ```java import axios from "axios"; import {Message} from "element-ui"; import router from '../router/index' import {myMessage} from "./myMessage"; axios.interceptors.response.use(success => { if (success.status && success.status == 200 && success.data.status == 500) { Message.error({message: success.data.msg}) return; } if (success.data.msg) { Message.success({message: success.data.msg}) } return success.data; }, error => { if (error.response.status == 504 || error.response.status == 404) { Message.error({message: '服务器被吃了( ╯□╰ )\''}) } else if (error.response.status == 403) { Message.error({message: '权限不足,请联系管理员'}) } else if (error.response.status == 401) { myMessage.error({message: error.response.data.msg ? error.response.data.msg : '尚未登录,请先登录'}) router.replace('/') } else{ if (error.response.data.msg) { Message.error({message: error.response.data.msg}) } else { Message.error({message: '未知错误'}) } } return; }) let base = ''; export const postKeyValueRequest = (url, params) => { return axios({ method: 'post', url: `${base}${url}`, data: params, transformRequest: [function (data) { let ret = ''; for (let i in data) { ret += encodeURIComponent(i) + '=' + encodeURIComponent(data[i]) + '&' } return ret; }], headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); } export const postRequest = (url, params) => { return axios({ method: 'post', url: `${base}${url}`, data: params }) } export const getRequest = (url, params) => { return axios({ method: 'get', url: `${base}${url}`, data: params }) } export const putRequest = (url, params) => { return axios({ method: 'put', url: `${base}${url}`, data: params }) } export const deleteRequest = (url, params) => { return axios({ method: 'delete', url: `${base}${url}`, data: params }) } ``` ```java import {Message} from "element-ui"; const showMessage = Symbol('showMessage') class JavaBoyMessage { [showMessage](type, options, single) { if (single) { if (document.getElementsByClassName('el-message').length == 0) { Message[type](options) } else { Message[type](options) } } } info(options, single = true) { this[showMessage]('info', options, single) } warning(options, single = true) { this[showMessage]('warning', options, single) } error(options, single = true) { this[showMessage]('error', options, single) } success(options, single = true) { this[showMessage]('success', options, single) } } export const myMessage = new JavaBoyMessage(); ``` 新建登录页面 ```vue ``` 配置config/index.js中的proxyTable请求转发实现跨域请求 ```js proxyTable: { '/': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/': '' } }, '/ws/*': { target: 'ws://127.0.0.1:8080', ws: true } }, ``` 配置路由router/index.js ```java import Vue from 'vue' import Router from 'vue-router' import Login from "../components/Login"; import HelloWorld from "../components/HelloWorld"; Vue.use(Router) export default new Router({ routes: [ { path: '/login', name: 'Login', component: Login, hidden: true }, { path: '/', name: '主页', component: HelloWorld, hidden: true, meta: { requireAuth: true } }, { path: '*', component: HelloWorld } ] }) ``` main.js ```js // The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import Vue from 'vue' import App from './App' import router from './router' import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' import {getRequest} from "./util/api"; import {postRequest} from "./util/api"; import {putRequest} from "./util/api"; import {deleteRequest} from "./util/api"; import {postKeyValueRequest} from "./util/api"; Vue.config.productionTip = false Vue.use(ElementUI) Vue.prototype.getRequest = getRequest; Vue.prototype.postRequest = postRequest; Vue.prototype.putRequest = putRequest; Vue.prototype.deleteRequest = deleteRequest; Vue.prototype.postKeyValueRequest = postKeyValueRequest; router.beforeEach((to, from, next) => { if (to.path == '/login') { next(); } else { if (window.sessionStorage.getItem("user")) { console.log(window.sessionStorage.getItem("user"), to.path) next(); } else { next('/login?redirect=' + to.path); } } }) /* eslint-disable no-new */ new Vue({ el: '#app', router, components: { App }, template: '' }) ``` 这时在未登录时都会跳转到`/login`页面,并附带参数,以至于登录成功后跳转到之前访问的页面。 ![image-20210224225751213](README.assets/image-20210224225751213.png) 登录后则会访问主页,可以看到响应信息 ![image-20210224225900915](README.assets/image-20210224225900915.png) 好了今天晚上就做了这么点,又到晚上11点了,中间有个坑,就是要能够顺利将LoginForm表单的数据传到后端,那么需要使用下面这个请求 ![image-20210224230230160](README.assets/image-20210224230230160.png) 否则会导致loadUserByUsername的username参数一直为null![image-20210224230322223](README.assets/image-20210224230322223.png) 具体原因是因为在springsecurity中设置了.formLogin(),那么前台提交就要用form表单提交,但是现在使用的是axios,所以需要模拟form提交 即: ![image-20210224230721243](README.assets/image-20210224230721243.png) 以及data格式需要转换一下 ![image-20210224230819466](README.assets/image-20210224230819466.png) 溜了溜了,困了,明天再整!

2021/2/24 23:09

---