本文共 22908 字,大约阅读时间需要 76 分钟。
人事管理系统是每个公司必备的管理系统,可以更方便的管理员工。
hr实现对员工的增删改查,管理员实现对hr的更改。对员工进行搜索等功能。
本项目是前后端分离项目,在服务器中运行。前端页面友好。
该系统主要是前端设计页面接受后端的JSON数据,前后端所有数据除了登录页面使用key values 形式,其余全部使用JSON格式。后端使用springdatajpa自动生成的restful风格接口。前端通过axios发起请求获取接口数据。但是在开发中,我们需要克服跨域所带来的的问题。部署阶段就不存在跨域,因为我们会把前端打包到后端,一起部署。这样就不涉及到跨域问题了。当然也可以使用nigx进行代理。
员工表:
hr表:
角色表:
添加springdata rest
依赖
org.springframework.boot spring-boot-starter-data-rest
目录结构
创建vhrdb数据库
配置jpa连接数据库
#配置数据库spring.datasource.url=jdbc:mysql://localhost:3306/vhrdb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghaispring.datasource.username=rootspring.datasource.password=123456#配置jpa#1.jpa数据库spring.jpa.database=mysql#2.在控制台打印sqlspring.jpa.show-sql=true#3.jpa数据库平台spring.jpa.database-platform=mysql#4.当对象改变更新表spring.jpa.hibernate.ddl-auto=update#5.指定方言!!!重要!!!spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect#.....
创建用户相关的表
角色表
package com.wz.vhrdb.entity;import javax.persistence.*;/** * @author: 王泽 */@Entity@Table(name = "t_role")public class Role { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String nameZh; //角色的中文名 public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getNameZh() { return nameZh; } public void setNameZh(String nameZh) { this.nameZh = nameZh; }}
用户表
package com.wz.vhrdb.entity;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;import javax.persistence.*;import java.util.ArrayList;import java.util.Collection;import java.util.List;/** * @author: 王泽 */@Entity(name = "t_user")public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; private boolean accountNonExpired; private boolean accountNonLocked; private boolean credentialsNonExpired; private boolean enabled; @ManyToMany(fetch = FetchType.EAGER,cascade = CascadeType.PERSIST) private Listroles; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public void setAccountNonExpired(boolean accountNonExpired) { this.accountNonExpired = accountNonExpired; } public void setAccountNonLocked(boolean accountNonLocked) { this.accountNonLocked = accountNonLocked; } public void setCredentialsNonExpired(boolean credentialsNonExpired) { this.credentialsNonExpired = credentialsNonExpired; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public List getRoles() { return roles; } public void setRoles(List roles) { this.roles = roles; } @Override public Collection getAuthorities() { List authorities = new ArrayList<>(); for (Role role : getRoles()) { 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 accountNonExpired; } @Override public boolean isAccountNonLocked() { return accountNonLocked; } @Override public boolean isCredentialsNonExpired() { return credentialsNonExpired; } @Override public boolean isEnabled() { return enabled; }}
运行程序,创建表。
创建UserDao接口
package com.wz.vhrdb.dao;import com.wz.vhrdb.entity.User;import org.springframework.data.jpa.repository.JpaRepository;public interface UserDao extends JpaRepository{ User findUserByUsername(String username);}
创建UserService类
package com.wz.vhrdb.service;import com.wz.vhrdb.dao.UserDao;import com.wz.vhrdb.entity.User;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.core.userdetails.UsernameNotFoundException;/** * @author: 王泽 */public class UserService implements UserDetailsService { @Autowired private UserDao userDao; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { final User user = userDao.findUserByUsername(username); if (user == null){ throw new UsernameNotFoundException("用户不存在"); } return user; }}
配置springsecurity
package com.wz.vhrdb.config;import com.wz.vhrdb.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.security.access.hierarchicalroles.RoleHierarchy;import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;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.builders.WebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.crypto.password.NoOpPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;/** * @author: 王泽 */public class SecurityConfig extends WebSecurityConfigurerAdapter { //密码不加密 @Bean PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } //校验的数据源 @Autowired UserService userService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService); } //角色关系 @Bean RoleHierarchy roleHierarchy() { RoleHierarchyImpl hierarchy = new RoleHierarchyImpl(); hierarchy.setHierarchy("ROLE_admin > ROLE_user"); return hierarchy; } //放行静态资源 @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/js/**","/css/**","/image/**"); } //配置拦截规则和表单配置 //表单配置待完善 @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**").hasRole("admin")//具备某个角色 .antMatchers("/user/**").hasAnyRole("admin","user") .anyRequest().authenticated()//除了上述两个只要登录就能访问 .and() .formLogin() .permitAll() .and() .csrf().disable(); }}
直接在service中添加增添方法,jpa为我们提供了save
//用户注册功能(增加用户) public User insertUser(User user) { return userDao.save(user); }}
在controller中写接口
/** * 新增用户 post /users */ @PostMapping("") public User addUser(@RequestBody User user){ return userService.insertUser(user); }
postman测试
{ "username":"王泽", "password":"123", "accountNonExpired":true, "accountNonLocked":true, "credentialsNonExpired":true, "enabled":true, "roles":[{ "name":"admin", "nameZh":"管理员" }]}
我们可以给实体类设置一些默认值:
private boolean accountNonExpired =true; private boolean accountNonLocked = true; private boolean credentialsNonExpired =true; private boolean enabled =true;
测试:
package com.wz.vhrdb;import com.wz.vhrdb.dao.UserDao;import com.wz.vhrdb.entity.Role;import com.wz.vhrdb.entity.User;import com.wz.vhrdb.service.UserServiceImpl;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import java.util.ArrayList;import java.util.List;@SpringBootTestclass VhrdbApplicationTests { @Autowired UserServiceImpl userService; @Test void contextLoads() { User u1 = new User(); u1.setUsername("liu"); u1.setPassword("123");// u1.setAccountNonExpired(true);// u1.setAccountNonLocked(true);// u1.setCredentialsNonExpired(true);// u1.setEnabled(true); Listrs1 = new ArrayList<>(); Role r1 = new Role(); r1.setName("ROLE_admin"); r1.setNameZh("管理员"); rs1.add(r1); u1.setRoles(rs1); userService.insertUser(u1); User u2 = new User(); u2.setUsername("小刘"); u2.setPassword("123");// u2.setAccountNonExpired(true);// u2.setAccountNonLocked(true);// u2.setCredentialsNonExpired(true);// u2.setEnabled(true); List rs2 = new ArrayList<>(); Role r2 = new Role(); r2.setName("ROLE_user"); r2.setNameZh("普通用户"); rs2.add(r2); u2.setRoles(rs2); userService.insertUser(u2); }}
主要问题有两方面:1.不能重复注册 2.密码加密问题
密码加密问题
首先我们注册的时候要添加密码加密
//用户注册功能(增加用户) public User insertUser(User user) { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10); user.setPassword(encoder.encode(user.getPassword())); return userDao.saveAndFlush(user); }
然后我们需要在springsecurity中配置
@Configurationpublic class SecurityConfig extends WebSecurityConfigurerAdapter { //密码加密 @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(10); } //校验的数据源 @Autowired UserServiceImpl userService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder()); }}
检查重复用户
public User insertUser(User user) { User username = userDao.findUserByUsername(user.getUsername()); if (username == null) { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10); user.setPassword(encoder.encode(user.getPassword())); return userDao.save(user); }else { throw new RuntimeException("用户名已经存在"); } }}
测试:
我们先用userDao.deleteAll()删除所有记录,然后来测试重复添加
我们首先需要一个员工的表(实体类),员工有姓名,性别,年龄,电话,部门,入职时间
。
创建实体类Personnel
package com.wz.vhrdb.entity;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;import java.util.Date;/** * @author: 王泽 */@Entity(name = "t_personnel")public class Personnel { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String pName; private String pSex; private Integer pAge; private String pClass; private String pTel; private Date pJoin; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getpName() { return pName; } public void setpName(String pName) { this.pName = pName; } public String getpSex() { return pSex; } public void setpSex(String pSex) { this.pSex = pSex; } public Integer getpAge() { return pAge; } public void setpAge(Integer pAge) { this.pAge = pAge; } public String getpClass() { return pClass; } public void setpClass(String pClass) { this.pClass = pClass; } public String getpTel() { return pTel; } public void setpTel(String pTel) { this.pTel = pTel; } public Date getpJoin() { return pJoin; } public void setpJoin(Date pJoin) { this.pJoin = pJoin; }}
对于员工表我们的需求有,特定的查询,以及增删改查,分页等。
springdatajpa自带的接口基本足够我们本项目的使用
目前所需要的自动生成的接口有:
1.查询所有用户: http://localhost:8989/users (get)2.查询所有员工: http://localhost:8989/personnels (get)3.增加用户hr: http://localhost:8989/user/adduser (post)4.增加员工: http://localhost:8989/personnels (post)5.分页查询员工: http://localhost:8989/personnels?page=0&size=56.删除hr: http://localhost:8989/users/id (delete)7.删除员工: http://localhost:8989/personnels/id (delete)8.修改员工: http://localhost:8989/personnels/id (put)
主要思路:成功与失败都有信息来提示前端
编码:
编写vo包(传值的)中的result类
package com.wz.vhrdb.vo;/** * @author: 王泽 */public class Result { private Boolean status =true; private String msg; public Boolean getStatus() { return status; } public void setStatus(Boolean status) { this.status = status; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; }}
编写controller
@PostMapping("/adduser") public Result addUser(@RequestBody User user){ Result result =new Result(); try{ userService.insertUser(user); result.setMsg("hr增加成功"); }catch (Exception e){ result.setStatus(false); result.setMsg("新增hr失败"+e.getMessage()); } return result; }
其余接口均使用springdatajpa自动生成的接口!
响应尤大的号召,使用vite!
npm init @vitejs/app vhrui --template vue
cd vhrui
npm install
至此项目创建完成!接下来我们需要引入我们所需要的其他组件
引入路由
npm install vue-router@next
创建router目录来存放router配置
import { createRouter,createWebHistory} from "vue-router";// 路由信息const routes = [ { path: "/", name: "Index", component: () => import('../views/idnex.vue'), },];// 导出路由const router = createRouter({ history: createWebHistory(), routes});export default router;
在main.js中使用路由
import { createApp } from 'vue'import App from './App.vue'import router from "./router/router";const app = createApp(App)app.mount('#app')app.use(router)
注意:在使用的时候要到inde.vue
npm install element-plus --save
在main.js中使用
import { createApp } from 'vue'import ElementPlus from 'element-plus';import 'element-plus/lib/theme-chalk/index.css';import App from './App.vue';const app = createApp(App)app.use(ElementPlus)app.mount('#app')
npm install --save axios vue-axios
在main.js 中使用
import Vue from 'vue'import axios from 'axios'import VueAxios from 'vue-axios'Vue.use(VueAxios, axios)
使用方法:
this.$http.get(api).then((response) => { console.log(response.data)})
import { createRouter,createWebHistory} from "vue-router";// 路由信息const routes = [ { path: "/", name: "index", component: () => import('../components/index.vue'), }, { path: "/login", name: "login", component: () => import('../components/login.vue'), }, { path: "/register", name: "register", component: () => import('../views/user/add.vue'), }, { path: "/personnel", name: "personnel", component: () => import('../components/personnel.vue'), children:[ { path: "/users", name: "users", component: () => import('../views/user/users.vue'), }, { path: "/pindex", name: "pindex", component: () => import('../views/personnel/pindex.vue'), }, { path: "/class", name: "class", component: () => import('../views/personnel/class.vue'), }, { path: "/personnels", name: "personnels", component: () => import('../views/personnel/personnels.vue'), }, ] },];// 导出路由const router = createRouter({ history: createWebHistory(), routes});export default router;
这里只写出首页的主要代码
公司主页 员工管理 部门管理 hr管理 退出
在前后端分离这样的开发架构下,前后端的交互都是通过 JSON 来进行,无论登录成功还是失败,都不会有什么服务端跳转或者客户端跳转之类。
登录成功了,服务端就返回一段登录成功的提示 JSON 给前端,前端收到之后,该跳转该展示,由前端自己决定,就和后端没有关系了。
登录失败了,服务端就返回一段登录失败的提示 JSON 给前端,前端收到之后,该跳转该展示,由前端自己决定,也和后端没有关系了。
successHandler 的功能十分强大,甚至已经囊括了 defaultSuccessUrl 和 successForwardUrl 的功能。
.successHandler((req, resp, authentication) -> { Object principal = authentication.getPrincipal(); resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); out.write(new ObjectMapper().writeValueAsString(principal)); out.flush(); out.close();})
只有开发时存在这种跨域的情况。
解决思路:
正解:
前端接收:发送axios异步请求,用tableData接收
//tableData[] findAll(){ this.axios.get('http://localhost:8989/users').then(res=>{ this.tableData=res.data; })}// 初始化的时候调用方法created() { this.findAll(); }
前端发来的post,后端接收
这时候,我们就需要再复习一下后端所用到的一些注解:
@RequestBody :可以将body里面所有的json数据传到后端,后端再进行解析。
@RequestParam :接收的参数是来自requestHeader中,即请求头。通常用于GET请求
@RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的);GET方式无请求体,所以使用@RequestBody接收数据时,前端不能使用GET方式提交数据,而是用POST方式进行提交。在后端的同一个接收方法里,@RequestBody与@RequestBody可以同时使用,@RequestBody最多只能有一个,而@RequestParam()可以有多个。
注解@RequestParam接收的参数是来自requestHeader中,即请求头。通常用于GET请求,比如常见的url:http://localhost:8081/spring-boot-study/novel/findByAuthorAndType?author=唐家三少&type=已完结
@RequestParam有三个配置参数:
required
表示是否必须,默认为 true
,必须。defaultValue
可设置请求参数的默认值。value
为接收url的参数名(相当于key值)。如果参数前写了@RequestParam(xxx),那么前端必须有对应的xxx名字才行(不管其是否有值,当然可以通
过设置该注解的required属性来调节是否必须传),如果没有xxx名的话,那么请求会出错,报400。如果参数前不写@RequestParam(xxx)的话,那么就前端可以有可以没有对应的xxx名字才行,如果有xxx名
的话,那么就会自动匹配;没有的话,请求也能正确发送。@JsonAlias注解,实现:json转模型时,使json中的特定key能转化为特定的模型属性;但是模型转json时,
对应的转换后的key仍然与属性名一致.@JsonAlias("Name","name123")private String name;
此时,json字符串转换为模型时,json中key为Name或为name123或为name的都能识别。
结论②:@JsonProperty注解,实现:json转模型时,使json中的特定key能转化为指定的模型属性;同样的,模
型转json时,对应的转换后的key为指定的key,见:示例中的motto字段的请求与响应。 以下图进一步说明:@JsonProperty("name123")private String name;
此时,json字符串转换为模型时,key为name123的能识别,但key为name的不能识别。
结论③:@JsonAlias注解需要依赖于setter、getter,而@JsonProperty注解不需要。
结论④:在不考虑上述两个注解的一般情况下,key与属性匹配时,默认大小写敏感。
结论⑤:有多个相同的key的json字符串中,转换为模型时,会以相同的几个key中,排在最后的那个key的值给模
型属性复制,因为setter会覆盖原来的值。见示例中的gender属性。结论⑥:后端@RequestBody注解对应的类在将HTTP的输入流(含请求体)装配到目标类(即:@RequestBody后面
的类)时,会根据json字符串中的key来匹配对应实体类的属性,如果匹配一致且json中的该key对应的值 符合(或可转换为)实体类的对应属性的类型要求时,会调用实体类的setter方法将值赋给该属性。后端:
package com.wz.vhrdb.vo;/** * @author: 王泽 */public class Result { private Boolean status =true; private String msg; public Boolean getStatus() { return status; } public void setStatus(Boolean status) { this.status = status; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; }}
@PostMapping("/adduser") public Result addUser(@RequestBody User user){ Result result =new Result(); try{ userService.insertUser(user); result.setMsg("hr增加成功"); }catch (Exception e){ result.setStatus(false); result.setMsg("新增hr失败"+e.getMessage()); } return result; }
前端:
submitForm(){ //提交表单到后端 this.$http.post("http://localhost:8989/user/adduser",this.pForm).then(res=>{ console.log(res.data); if(res.data.status){ this.$message({ message:'恭喜你'+res.data.msg, type:'success' }) // 成功后的处理:清空表单信息,刷新所有 this.pForm={ }; this.$emit('findAll'); }else { this.$message.error(this.date.msg); } }) },
问题等价于父组件向子组件传值!
在父组件的 子组件标签
写要传的值
在子组件中使用
props:['id'], methods: { submitForm() { console.log(this.id); this.$http.put("http://localhost:8989/users/"+this.id).then(res=>{ console.log(res.data); this.$emit('findAll'); }) }, }
导入 qs;import qs from 'qs';然后data 使用data:qs.stringify(param)
let params = { username: this.loginForm.username, password: this.loginForm.password};console.log(params);let kv =qs.stringify(params);console.log(kv);this.$http.post('http://localhost:8989/login',kv)
这前后端分离开发后,认证这一块到底是使用传统的 session 还是使用像 JWT 这样的 token 来解决呢?
这确实代表了两种不同的方向。
传统的通过 session 来记录用户认证信息的方式我们可以理解为这是一种有状态登录,而 JWT 则代表了一种无状态登录。可能有小伙伴对这个概念还不太熟悉,我这里就先来科普一下有状态登录和无状态登录。
有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如 Tomcat 中的 Session。例如登录:用户登录后,我们把用户的信息保存在服务端 session 中,并且给用户一个 cookie 值,记录对应的 session,然后下次请求,用户携带 cookie 值来(这一步有浏览器自动完成),我们就能识别到对应 session,从而找到用户的信息。这种方式目前来看最方便,但是也有一些缺陷,如下:
微服务集群中的每个服务,对外提供的都使用 RESTful 风格的接口。而 RESTful 风格的一个最重要的规范就是:服务的无状态性,即:
那么这种无状态性有哪些好处呢?
无状态登录的流程:
使用 session 最大的优点在于方便。你不用做过多的处理,一切都是默认的即可。松哥本系列前面几篇文章我们也都是基于 session 来讲的。
但是使用 session 有另外一个致命的问题就是如果你的前端是 Android、iOS、小程序等,这些 App 天然的就没有 cookie,如果非要用 session,就需要这些工程师在各自的设备上做适配,一般是模拟 cookie,从这个角度来说,在移动 App 遍地开花的今天,我们单纯的依赖 session 来做安全管理,似乎也不是特别理想。
这个时候 JWT 这样的无状态登录就展示出自己的优势了,这些登录方式所依赖的 token 你可以通过普通参数传递,也可以通过请求头传递,怎么样都行,具有很强的灵活性。
此项目是我写第一个前后端分离项目,项目功能很简单,但是遇到的问题很多,解决问题的过程让我对前后端分离的开发有了更深刻的理解,也让我对springsecurity的安全更加敬佩。对于前端vue中router以及axios的使用更加熟练,学会了ui组件库的使用方法。对于后端,对springboot以及springdatajpa和springsecurity都有新的收获!springboot+vue的开发方式更明确的定位了程序员的分工。但是一个出色的程序员我认为应该是对技术栈都有所掌握的。当然一种技术有更深的造诣是我们所追求的,但是知识面千万不能窄。这就是我本次项目开发的总结。
本次项目开发,感谢bilibili中suns老师的课程,他的课程垫定了我的spring基础。还有百知教育的陈老师,讲的vue与elementui课程很好。感谢 王松(江南一点雨)的springboot与springsecurity以及springdatajpa,mybatis…的教学。让我真正的学会了开发知识!
springboot官方文档
vue官方文档
axios官方文档
element ui 官方文档
vite官方文档
深入浅出springsecurity ——王松(江南一点雨)
MySQL官方文档
转载地址:http://uksef.baihongyu.com/