登录页面
不同的路径,在显示不同的东西。不同的router路径在router/index.js配置,配置路径还有对应的页面(vue)
const routes = [
{
path: '/',
name: 'Login',
component: Login
}
]
服务端登录接口制作
总体步骤:
1、逆向工程,创建model包和mapper包
2、Hr实体类,实现
UserDetails
接口,并重写七个方法
3、创建HrService,根据用户名,通过HrMapper获取hr对象。并返回该对象
4、创建
SecurityConfig
配置类,继承
WebSecurityConfiguerAdapter
类;
配置密码加密;
重写
configure(AuthenticationManagerBuilder auth)
方法,并将hrService传入
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(hrService);
}
6、写一个helloController来测试一下
7、测试成功,说明,Spring Security已经和数据库连接上了
8、重写
configure(HttpSecurity http)
方法,
写登录成功时的回调,登录失败时的回调,登出的回调
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated() // 剩余所有请求都需要登录才能访问
.and()
.formLogin() // 表单登录
.usernameParameter("username") // 携带参数 username的名称
.passwordParameter("password") // 携带参数 passowrd的名称
.loginProcessingUrl("/doLogin") // 如果使用PostMan,通过访问这个路径来登录
.loginPage("/login")
.successHandler(new AuthenticationSuccessHandler() { // 登录成功后的操作
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
// authentication 表示登录成功的用户信息
Hr hr = (Hr) authentication.getPrincipal();
// 返回RespBean对象
RespBean ok = RespBean.ok("登录成功", hr);
// 输出成字符串
String s = new ObjectMapper().writeValueAsString(ok);
out.write(s);
out.flush();
out.close();
}
})
.failureHandler(new AuthenticationFailureHandler() { // 登录失败的操作
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp,
AuthenticationException exception) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.error("登录失败");
if(exception instanceof LockedException){
respBean.setMsg("账户被锁定,请联系管理员");
}else if(exception instanceof CredentialsExpiredException){
respBean.setMsg("密码过期");
}else if(exception instanceof AccountExpiredException){
respBean.setMsg("账户过期,请联系管理员");
}else if(exception instanceof DisabledException){
respBean.setMsg("账户被禁用,请联系管理员");
}else if(exception instanceof BadCredentialsException){
respBean.setMsg("用户名或者密码错误,请联系管理员");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
}
})
.permitAll() //
.and()
.logout()
.logoutSuccessHandler(new LogoutSuccessHandler() { // 登出的操作
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
}
})
.permitAll()
.and()
.csrf().disable();
}
9、使用PostMan访问
http://localhost:8080/doLogin
测试一下,
1、在hr类实现
UserDetails
接口,实现方法
2、创建HrService,并实现
UserDetailsService
接口,实现
loadUserByUsername
方法
2.1、查询hr对象,使用MyBatis查询。如果等于null,抛出异常
UsernameNotFoundException
2.2、查询
3、配置security配置类,继承
WebSecurityConfiguerAdapter
类
3.1、注入hrService
@Autowired
HrService hrService;
3.2、配置Bean,密码加密的bean
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
3.3、实现
configure
方法
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(hrService);
}
3.4、然后测试下
写一个helloController,get方式请求,返回一个hello就好
如果登录成功,说明已经介入到数据库了
3.5、因为是前后端分离,所以需要配置使用JSON的方式登录
登陆成功:返回一个登录成功的JSON
登录失败:返回一个登录失败的JSON
定义一个统一的返回Bean:RespBean,看源码里边有
实现 configure(HttpSecurity http)
接口:
configure(HttpSecurity http)
.permitAll() 表示登录相关的页面/接口不要被拦截。
登录成功的回调:
.successHandler
authentication:登录成功的用户信息
登录成功返回的是一个RespBean对象
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
Hr hr = (Hr) authentication.getPrincipal();
RespBean ok = RespBean.ok("登录成功", hr);
String s = new ObjectMapper().writeValueAsString(ok);
out.write(s);
out.flush();
out.close();
}
})
登录成功后,不显示密码
hr.setPassword(null)
登录失败的回调:
.failureHandler
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
RespBean error = RespBean.error("登录失败");
if (e instanceof LockedException) {
error.setMsg("账户被锁定,请联系管理员");
} else if (e instanceof CredentialsExpiredException) {
error.setMsg("密码过期,请联系管理员");
} else if (e instanceof AccountExpiredException) {
error.setMsg("账户过期,请联系管理员");
}else if (e instanceof DisabledException) {
error.setMsg("账户被禁用,请联系管理员");
}else if(e instanceof BadCredentialsException){
error.setMsg("用户名或者密码输入错误,请联系管理员");
}
String s = new ObjectMapper().writeValueAsString(error);
out.write(s);
out.flush();
out.close();
}
})
定义一个
LoginController
来处理登录页的问题
@RestController
public class LoginController {
@GetMapping("/login")
public RespBean login(){
return RespBean.error("尚未登录,请登录");
}
}
登出的回调:
.logoutSuccessHandler
authentication:表示需要注销的用户
直接返回一个RespBean对象
前后端接口对接
安装axios,
npm install axios
1、封装网络请求,创建utils包,再创建api.js。
- 在里边写一个响应拦截器。
这是http的响应码,success.status
这是我们返回的JSON的响应码success.data.status
import axios from 'axios'
import {Message} from 'element-ui'
/*响应的拦截器*/
axios.interceptors.response.use(success=>{
if(success.status && success.status ==200 && success.data.status==500){
Message.error({message:success.data.msg})
return;
}
return success.data;
},error => {
if(error.response.status == 504 ||error.response.status == 404 ){
Message.error({message:'没找到服务器o(╯□╰)o'})
}else if(error.response.status == 403 ){
Message.error({message: '权限不足,请联系管理员'})
}else if(error.response.status == 401 ){
Message.error({message: '没有登录,请登录'})
}else{
if(error.response.data.msg){
Message.error({message:error.response.data.nsg})
}else{
Message.error({message: '未知错误!'})
}
}
return;
})
- 再写一个POST请求的封装,(新增 封装多个请求
let base = '';
// 传递key value的请求
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])+'&'
}
console.log(ret);
return ret;
}],
headers:{
'Content-Type':'application/x-www-form-urlencoded'
}
});
}
// 传递JSON的请求
export const postRequest=(url,params)=>{
return axios({
method:'post',
url:`${base}${url}`,
data:params
})
}
export const putRequest=(url,params)=>{
return axios({
method:'put',
url:`${base}${url}`,
data:params
})
}
export const getRequest=(url,params)=>{
return axios({
method:'get',
url:`${base}${url}`,
data:params
})
}
export const deleteRequest=(url,params)=>{
return axios({
method:'delete',
url:`${base}${url}`,
data:params
})
}
将封装的请求,做成vue插件
在main.js把封装的请求都导入进来
import {postRequest} from "./utils/api";
import {postKeyValueRequest} from "./utils/api";
import {putRequest} from "./utils/api";
import {deleteRequest} from "./utils/api";
import {getRequest} from "./utils/api";
Vue.prototype.postRequest = postRequest;
Vue.prototype.postKeyValueRequest = postKeyValueRequest;
Vue.prototype.putRequest = putRequest;
Vue.prototype.deleteRequest = deleteRequest;
Vue.prototype.getRequest = getRequest;
使用:
this.封装的请求.
如:this.postRequest()
- 使用封装的请求
- 1、先导入:
import {postKeyValueRequest} from "../utils/api"
- 2、使用:
- 另外一种使用方法:
前提是已经做成了一个vue插件this.postRequest()
- 1、先导入:
postKeyValueRequest('/doLogin', this.loginForm).then(resp=>{
if(resp) {
alert(JSON.stringify(resp))
}
})
通过
if(resp)
判断请求成功还是失败,失败不用管,因为在拦截器已经处理过了
- 跨域问题处理:配置 nodejs 请求转发代理vue.config.js
let proxyObj = {};
proxyObj['/']={
ws:false,
target:'http://localhost:8081', // 把拦截的请求转发到8081端口区
changeOrigin: true,
pathRewrite:{
'^/':''
}
}
module.exports={
devServer:{
host:'localhost',
port:8080,
proxy:proxyObj
}
}
2、保存请求回来的数据到
sessionStorage
3、保存后再进行页面跳转,跳转到
Home.vue
页面
3.1、创建
Home.vue
页面
3.2、在
Router.js
中引入,并设置
3.3、跳转
this.$router.replace('/home')
postKeyValueRequest('/doLogin', this.loginForm).then(resp=>{
if(resp) {
window.sessionStorage.setItem("user", JSON.stringify(resp.obj));
this.$router.replace('/home')
}
})
Home页面制作
从
sessionStorage
读取用户信息
注销登录要把
sessionStorage
的信息清空
api.js加上一个登录成功的提示
if(success.data.msg) {
Message.success({message:success.data.msg})
}
左边导航菜单制作
1、在右边显示窗口加一个
2、在router包下index.js添加
{
path: '/home',
name: '导航一',
component: Home,
children:[
{
path: '/test1', // 要跳转的页面,这里为vue
name: '选项1',
component: Test1
}, {
path: '/test2',
name: '选项2',
component: Test2
}
]
}
3、统一Home.vue和index.js的选项页面
渲染index.js的routes到Home.vue中,添加hidden给不需要渲染的routes,用来区分。
<el-menu router>
<el-submenu index="1" v-for="(item,index) in this.$router.options.routes" v-if="!item.hidden" :key="index">
<template slot="title">
<i class="el-icon-location"></i>
<span>{{item.name}}</span>
</template>
<el-menu-item :index="child.path" v-for="(child,indexj) in item.children " :key="indexj">
{{child.name}}
</el-menu-item>
</el-submenu>
</el-menu>
4、通过数据库动态修改左边导航菜单
- menu表里存有导航菜单,分有一级菜单,二级菜单。enabled表示菜单是否启用,requireAuth表示需要登录才能访问。
- 根据用户id获取角色id,根据角色id获取menu id,再通过menu id获取用户可以操作的菜单。
目的:通过服务端根据用户id返回一个menu菜单。
1、修改menu实体类,改成驼峰法(看个人)
2、将额外的字段放到一个meta类里边,
这里将keepAlive,requireAuth定义成meta类的成员变量
在menu实体类增加一个meta成员变量,
在menu实体类增加一个children成员变量,List
类型,
在MenuMapper的resultMap修改:
<association property="meta" javaType="org.javaboy.vhr.model.Meta">
<result column="keepAlive" property="keepAlive" jdbcType="BIT" />
<result column="requireAuth" property="requireAuth" jdbcType="BIT" />
</association>
3、写一个controller查询需要的数据,系统配置的controller(SystemConfigController),创建MenuService。
通过系统保存的用户对象的id来查询,
(Hr)SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getId()
mapper进行查询:
sql语句:
select
distinct m1.*,m2.id as id2,m2.component as component2,
m2.enabled as enabled2,
m2.iconCls as iconCls2,m2.keepAlive as keepAlive2,m2.name as name2,
m2.parentId as parentId2,m2.requireAuth as requireAuth2,m2.path as path2
from
hr_role hrr,
menu_role mr,
menu m2,
menu m1
where
hrr.hrid=10
and hrr.rid=mr.rid
and m2.id = mr.mid
and m1.id = m2.parentId
and m1.enabled=true
// 目的:查询该用户所拥有的菜单信息
// 步骤:
1、查询用户为10的角色id
2、根据角色id查询该角色拥有的菜单id
3、根据菜单id查询具体的菜单信息
4、根据菜单id链接菜单的父类菜单信息
5、给子菜单的字段取个别名
重新定义一个resultMap:
<resultMap id="Menus2" type="org.javaboy.vhr.model.Menu" extends="BaseResultMap">
<collection property="children" ofType="org.javaboy.vhr.model.Menu">
<id column="id2" property="id" jdbcType="INTEGER" />
<result column="url2" property="url" jdbcType="VARCHAR" />
<result column="path2" property="path" jdbcType="VARCHAR" />
<result column="component2" property="component" jdbcType="VARCHAR" />
<result column="name2" property="name" jdbcType="VARCHAR" />
<result column="iconCls2" property="iconCls" jdbcType="VARCHAR" />
<result column="parentId2" property="parentId" jdbcType="INTEGER" />
<result column="enabled2" property="enabled" jdbcType="BIT" />
<association property="meta" javaType="org.javaboy.vhr.model.Meta">
<result column="keepAlive2" property="keepAlive" jdbcType="BIT" />
<result column="requireAuth2" property="requireAuth" jdbcType="BIT" />
</association>
</collection>
</resultMap>
5、菜单项数据加载成功之后,在前端有几个可以存放的地方:
sessionStorage
localStorage
vuex
需要把加载下来的菜单项数据,需要放在一个公共的地方,让大家都能访问
vuex:状态管理。把数据放在一个公共的地方。可以用于数据共享,互相调用。
vuex的安装:
npm install vuex
vuex的用法:
- 创建一个
包,创建store
文件index.js
- 在
引入。 在new Vue里加入main.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.user(Vuex)
export default new Vuex.Store({
state:{
routes:[] // 菜单项都放在这个数据里
},
mutations:{
initRoutes(state,data){ // 这里写怎么放
state.routes = data;
}
},
actions:{
}
})
// 引入
import store from './store'
new Vue({
// 加上一个
store,
})
6、写一个专门的工具类,功能:1、从服务端请求数据。2、把服务端返回的字符串转成前端的对象;3、把服务端返回的数据放到
router.js
中去
在utiles包中创建,
menus.js
在views包中创建组件的vue:有五个包:emp、per、sal、sta、sys。也拷贝一份到components
emp:EmpBasic.vue(基本资料)、EmpAdv.vue(高级资料)
per:PerEmp(员工资料)、PerEc(员工奖惩)、PerTrain(员工培训)、PerSalary(员工调薪)、PerMv(员工调动)
sal:SalSob(工资账套管理)、SalSobCfg(员工账套设置)、SalTable(工资表管理)、SalMonth(月末处理)、SalSearch(工资表查询)
sta:StaAll(综合信息统计)、StaScore(员工积分统计)、StaPers(人事信息统计)、StaRecord(人事记录统计)
sys:SysBasic(基础信息设置)、SysCfg(系统管理)、SysLog(操作日志管理)、SysHr(操作员管理)、SysData(备份恢复数据库)、SysInit(初始化数据库)
import {getRequest} from "../../utils/api";
export const initMenu=(router,store) =>{ // 要存到router,要保存到store
if(store.state.routes.length > 0){
// 说明有菜单数据,直接返回
return;
}
getRequest("/system/config/menu").then(data =>{
if(data){
let fmtRoutes = formatRoutes(data);
router.addRoutes(fmtRoutes);
store.commit('initRoutes', fmtRoutes); // 调用initRoutes这个方法
}
})
}
// 把一些字符串字段变成前端对象
export const formatRoutes=(routes)=>{
let fmRoutes = [];
routes.forEach(router =>{
let{
path,
component,
name,
meta,
iconCls,
children
} = router // router.path, router.name, router.component....
if(children && children instanceof Array){
// 说明是一级菜单
// 递归调用
children = formatRoutes(children);
}
let fmRouter={
path:path,
name:name,
iconCls:iconCls,
meta:meta,
children:children,
component(resolve){ // 主要是处理这个component,componen就是返回的字符串: component:"。。是这个。。"
// 动态导入(用的时候再导入这个文件,类似于import...),导入这个vue。
if(component.startsWith("Home")){
require(['../views/' + component+'.vue'],resolve);
}else if (component.startsWith("Emp")){
require(['../views/emp/' + component+'.vue'],resolve);
}else if(component.startsWith("Per")) {
require(['../views/per/' + component+'.vue'],resolve);
}else if(component.startsWith("Sal")) {
require(['../views/sal/' + component+'.vue'],resolve);
}else if(component.startsWith("Sta")) {
require(['../views/sta/' + component+'.vue'],resolve);
}else if(component.startsWith("Sys")) {
require(['../views/sys/' + component+'.vue'],resolve);
}
}
}
fmRoutes.push(fmRouter);
})
return fmRoutes;
}
7、左边导航栏菜单加载
通过路由导航守卫调用
initMenu()
方法来加载菜单项
路由导航守卫 :路由页面在跳转过程中,可以对页面的跳转进行监听,可以过滤。
https://router.vuejs.org/zh/guide/advanced/navigation-guards.html
在main.js加载路由导航守卫
router.beforeEach(to, from, next) => {
// from:从哪来
// to:去哪里
if (to.path == '/') { // 如果要去的页面是登录页,直接放行
next();
}else {
// 调用initMenu方法加载菜单项 ,记得导入 import {initMenu} from "@/store/menus"
initMenu(router, store);
next();
}
}
替换菜单项,把菜单项换成store里存的数据。使用computed
在Home.vue中添加
computed: {
routes(){
return this.$store.state.routes;
}
}
然后导航页的数据源代码换成:routes,再加一个unique-opened..看别人的那个代码
注销登录时,要清空store里的数据
this.$store.commit('initRoutes',[])
安装图标库:看网页
完善首页:看网页
前后端分离权限管理思路探讨
目的:前端做菜单显示只是为了提高用户体验,而不是进行权限控制
权限:后端会提供很多接口,每个接口都需要用户或者角色才能访问。没有权限就访问不了接口。
后端处理的权限才是安全的,前端处理的不是安全的。
权限管理:后端接口设计
1、前端先发起一个http请求,拿到请求后,先分析地址和menu表的url字段哪一个是匹配的。
2、然后去menu_role去查看,哪些角色能访问这个资源。
3、查看当前登录的用户是否具备所需要的角色,如果具备那就没问题,不具备就是越权访问。
1、写一个类,来实现:通过请求地址来获取请求地址所需要的角色
2、写一个类,来判断:当前用户是否具备有上边类返回结果所需要的角色
3、在Security配置类中,设置这两个类
4、给登录用户设置角色信息
接口设计
1、定义一个Fileter实现FilterInvocationSecurityMetadataSource接口(这个类的功能就是启动权限访问)(根据用户传来的请求地址,分析出请求需要的角色)
2、主要写
getAttributes
这个方法
3、Menu类加一个
List<Role> Roles
MenuService写一个
getAllMenusWithRole
方法。Mapper写一个sql:
在定义一个ResultMap
SELECT m.*,r.id as rid,r.name as rname,r.nameZh as rnameZh
FROM menu m,menu_role mr, role r
WHERE m.id=mr.mid and mr.rid=r.id
ORDER BY m.id
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException
//
Collection<ConfigAttribute>:当前请求需要的角色
第一步:通过分析当前请求地址,需要哪些角色才能访问
// 最终目的:通过分析当前请求地址,需要哪些角色才能访问
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) o).getRequestUrl(); // 当前请求的地址
List<Menu> menus = menuService.getAllMenusWithRole(); // 获取所有的菜单所需要的角色。需要开缓存,不然每次都会去查数据库
// 遍历menus比较当前请求路径
// antPathMatcher需要new
for (Menu menu : menus) {
if(antPathMatcher.match(menu.getUrl(),requestUrl)){
// 获取当前资源需要的角色
List<Role> roles = menu.getRoles();
String[] str = new String[roles.size()];
for (int i = 0; i < roles.size(); i++) {
str[i] = roles.get(i).getName();
}
return SecurityConfig.createList(str);
}
}
// 表示没有匹配上的,都是登录后访问
return SecurityConfig.createList("ROLE_LOGIN");
}
第二步:分析当前用户是否具备有所访问资源的角色
写一个类,实现
AccessDecisionManager
接口,用来判断当前用户是否具备所访问资源的角色,如果具有就过。
主要重写:
public void decide(Authentication authentication, Object o,
Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException
// authentication:登录成功的用户信息。Collection<ConfigAttribute> collection:是上边Fileter的返回值,就是menu需要的角色信息
@Override
public void decide(Authentication authentication, Object o,
Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute configAttribute : collection) {
String needRole = configAttribute.getAttribute(); // 需要的角色
// 判断有没有登录。如果当前用户是匿名实例的话,就说明没登录
if("ROLE_LOGIN".equals(needRole)){
if(authentication instanceof AnonymousAuthenticationToken){
throw new AccessDeniedException("尚未登录,请登录");
}else{
return;
}
}
// 获取当前登录用户的角色
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
// 如果资源需要的角色有多个,只需要判断当前用户的角色有一个在里边就可以
for (GrantedAuthority authority : authorities) {
if(authority.getAuthority().equals(needRole)){
return;
}
}
}
throw new AccessDeniedException("权限不足,请联系管理员");
}
第三步:在
SecurityConfig
引进这两个类就可以了
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(customUrlDecisionManager);
object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
return object;
}
})
第四步:给用户给角色
Hr类添加 List roles 属性
这个操作就是给用户赋角色的值
@Override
@JsonIgnore
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>(roles.size());
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
在登录成功后,获取登录用户的角色,并给Hr的roles赋值,在HrService里:
添加:
hr.setRoles(hrMapper.getHrRolesById(hr.getId()));
写sql:根据用户id查询用户用拥有的角色
第四步:测试
如果访问
/login
不用经过
Spring Security
在
SecurityConfig
加上
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/login");
}
完善权限管理:看网页!
基础信息设置页面
前端:复制别人的
vue的组件化开发,由小的页面(组件)拼接成一个完整的页面(组件)
1、在components包下创建sys包创建basic包,创建5个组件:DepMana、PosMana、JobLevelMana、EcMana、PermissMana
2、在SysBasic.vue引入这五个组件
import DepMana from '../../components/sys.......'
自己写吧。。。。
并注册组件
components:{
// 'DepMana':DepMana,前边跟后边一样的可以简写成下边这样。
DepMana
}
在<template></template>内写<DepMana></DepMana>
如:
<el-tab-pane label="部门管理" name="a"><DepMana></DepMana></el-tab-pane>