Vue-element-admin使用mock.js数据进行用户和权限的验证,使用该框架开发的首要事情就是用户集成改造,使用本地测试环境的用户和角色信息完成登录验证;
目录
一、vue-element-admin的登录逻辑
二、后端改造--用户查询接口
1、表和model
2、mapper
3、service和Impi
4、controller
5、filter拦截器
三、用户登录集成的前端改造
一、vue-element-admin的登录逻辑
在/views/login目录中index.vue是登录界面,SocialSignin.vue是第三方登录页面,auth-redirect.vue没看懂;
login
| components
| | SocialSignin.vue
| auth-redirect.vue
| index.vue
index.vue中的登录按钮触发handleLogin()这个methods,调用store/user.js中login这个action实现状态的更改,login()这个action再调用api/user/login的接口请求,从后端拿用户数据,如下:
// hadleLogin方法完成用户登录动作
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid) {
this.loading = true
this.$store.dispatch('user/login', this.loginForm)
.then(() => {
this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
this.loading = false
})
.catch(() => {
this.loading = false
})
} else {
console.log('error submit!!')
return false
}
})
}
//store/user.js中login这个action的代码:
const actions = {
// user login
login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({ username: username.trim(), password: password }).then(response => {
const { data } = response
commit('SET_TOKEN', data.token)
setToken(data.token)
resolve()
}).catch(error => {
reject(error)
})
})
}
}
// api/user/login的接口请求代码:
import request from '@/utils/request'
export function login(data) {
return request({
url: '/vue-element-admin/user/login',
method: 'post',
data
})
}
mock/user.js定义了/vue-element-admin/user/login这个rest请求的返回数据,定义了admin和editor这2个角色和token内容,定义了post请求(参数是loginname)的返回是一个R(code+tokens信息)嵌套json,code是请求的响应码,tokens是用户的单一状态信息(全局维护~),user/getInfo方法根据token参数得到用户和角色信息,后端需要提供相应接口;
const tokens = {
admin: {
token: 'admin-token'
},
editor: {
token: 'editor-token'
}
}
const users = {
'admin-token': {
roles: ['admin'],
introduction: 'I am a super administrator',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
name: 'Super Admin'
},
'editor-token': {
roles: ['editor'],
introduction: 'I am an editor',
avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
name: 'Normal Editor'
}
}
module.exports = [
// user login
{
url: '/vue-element-admin/user/login',
type: 'post',
response: config => {
const { username } = config.body
const token = tokens[username]
// mock error
if (!token) {
return {
code: 60204,
message: 'Account and password are incorrect.'
}
}
return {
code: 20000,
data: token
}
}
}
]
逻辑并没有结束,前后端分离的路由逻辑写在框架里面,可以发现在登录页已定义redirect(http://localhost:9527/#/login?redirect=%2Fdashboard),因为涉及到路由的跳转就要验证权限问题,在根目录下有个全局的permission.js负责全局的导航守卫,里面定义了首页登录成功后需要在vuex中取权限,vuex再次发起user/getInfo请求得到对应用户的权限(role),如果有权限才会跳转,没有权限报错;
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist
//导航守卫,在router之前,拿到to和from以及下一步动作next
router.beforeEach(async(to, from, next) => {
// 开启进度条
NProgress.start()
// 拿title
document.title = getPageTitle(to.meta.title)
// 判断浏览器是否持有token
const hasToken = getToken()
// 有token若是登录页就放行,有token不是登录页就验证是否有权限,有就放行,没有的话在vuex中拿,vuex发起user/getinfo请求拿权限,并记住想要跳转到的页面,也就是redirect的位置;
if (hasToken) {
if (to.path === '/login') {
next({ path: '/' })
NProgress.done() // hack: https://github.com/PanJiaChen/vue-element-admin/pull/2939
} else {
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
const {roles} = await store.dispatch('user/getInfo')
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
router.addRoutes(accessRoutes)
next({...to, replace: true})
} catch (error) {
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
//没有token的话白名单就放行,不是的话就跳到登录页记录要去的页面;
if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
next()
} else {
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
// 结束进度条
NProgress.done()
})
所以能够得到大致的流程,前端在登录页发起login请求,在路由守卫时候取vuex中的token(login请求后端返给前端的加密串),再次发起user/info请求从后端得到用户的权限信息,虽然token里面的加密信息附带了用户的所有信息,但是前端不做解密,登录后前端每次向后端请求数据,都会携带token头,然后后端根据token头再去验证并返回前端相应的信息;
二、后端改造--用户查询接口
本地测试首先解决跨域问题:集成中的CORS(跨域资源共享)问题
还有前端调试问题:使用Vscode在chrome中调试vue
需要完成model、mapper、service、Impi、filter(请求过滤器)、co模块
后端完成后的git地址:后端代码
1、表和model
基本的用户登录模块至少需要4个表:user表(用户基本信息)、role表(用户权限信息)、auth表(用户密码和tokens信息)、user和role的维表,在idea的h2数据库建3个相应的表,参考之前博文:基于springboot的web开发配置,使用springboot、mybatisPlus、h2、lambok、swagger系列配置完成后端接口的开发;
// user表
create table LT_SYS_USER
(
USER_ID VARCHAR(20) not null
primary key,
USER_CODE VARCHAR(64),
USER_NAME VARCHAR(100) not null,
USER_NAME_EN VARCHAR(100),
ORGAN_ID VARCHAR(20),
ORGAN_CODE VARCHAR(64),
ORGAN_NAME VARCHAR(100),
EMAIL TEXT,
MOBILE VARCHAR(100),
PHONE VARCHAR(100),
SEX INT,
AVATAR TEXT,
SIGN VARCHAR(200),
USER_SORT INT,
STATUS INT not null,
CREATE_BY VARCHAR(64),
CREATE_DATE DATETIME,
UPDATE_BY VARCHAR(64),
UPDATE_DATE DATETIME,
REMARKS TEXT,
DELETED VARCHAR(5) not null,
CORP_ID VARCHAR(64),
REG_DATE DATETIME
);
// role角色表
create table LT_SYS_ROLE
(
ROLE_ID VARCHAR(20) not null
primary key,
ROLE_CODE VARCHAR(64) not null,
ROLE_NAME VARCHAR(100) not null,
ROLE_TYPE VARCHAR(100),
ROLE_SORT INT,
DATA_SCOPE INT,
BIZ_SCOPE VARCHAR(255),
DELETED TINYINT not null,
CREATE_BY VARCHAR(64) not null,
CREATE_DATE DATETIME not null,
UPDATE_BY VARCHAR(64),
UPDATE_DATE DATETIME,
REMARKS TEXT,
CORP_ID VARCHAR(64)
);
//auth权限表
create table LT_SYS_USER_AUTH
(
AUTH_ID VARCHAR(20) not null
primary key,
USER_ID VARCHAR(20) not null,
LOGIN_NAME VARCHAR(100) not null,
PASSWD VARCHAR(100) not null,
TOKEN TEXT,
EXPIRE_TIME DATETIME,
DINGTALK_OPENID LONGTEXT,
WELINK_OPENID VARCHAR(100),
WX_OPENID VARCHAR(100),
MOBILE_IMEI VARCHAR(100),
USER_TYPE VARCHAR(16),
PWD_SECURITY_LEVEL INT,
PWD_UPDATE_DATE DATETIME,
PWD_UPDATE_RECORD TEXT,
PWD_QUEST_UPDATE_DATE DATETIME,
LAST_LOGIN_IP VARCHAR(100),
LAST_LOGIN_DATE DATETIME,
FREEZE_DATE DATETIME,
FREEZE_CAUSE VARCHAR(200),
USER_WEIGHT INT,
CREATE_BY VARCHAR(64),
CREATE_DATE DATETIME,
UPDATE_BY VARCHAR(64),
UPDATE_DATE DATETIME,
REMARKS TEXT,
DELETED INT default 0,
STATUS VARCHAR(100)
);
// USER和role维表
create table LT_SYS_USER_ROLE
(
USER_ID VARCHAR(20) not null,
ROLE_ID VARCHAR(20) not null,
primary key (USER_ID, ROLE_ID)
);
comment on column LT_SYS_USER_ROLE.USER_ID is '用户id';
comment on column LT_SYS_USER_ROLE.ROLE_ID is '角色id';
2、mapper
3个表分别插入测试数据后,开始写后端restAPI查询接口,首先model/目录下创建userModel文件夹,model/userModel下新建3个类User.java,Role.java,Auth.java使用lambok和swagger注解提供构造器和接口使用的便利;然后在mapper/userMapper目录下创建对应的接口UserMapper和UserAuthMapper等,这里使用mybatisplus的继承basemapper再自定义一些查询;
// usermapper接口
package com.example.testspring.mapper.userMapper;
import com.example.testspring.model.userModel.Role;
import com.example.testspring.model.userModel.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.testspring.model.userModel.UserRole;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface UserMapper extends BaseMapper<User>{
User getByIdLazy(String userId);
User findById(String userId);
boolean deleteRoleByUserId(String userId);
boolean insertRolesBatch(@Param("list") List<UserRole> list);}
//RoleMapper 接口
package com.example.testspring.mapper.userMapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.testspring.model.userModel.Role;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface RoleMapper extends BaseMapper<Role> {
List<Role> getListByUserId(String userId);
Role findById(String roleId);
}
//AuthMapper
package com.example.testspring.mapper.userMapper;
import com.example.testspring.model.userModel.Auth;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AuthMapper extends BaseMapper<Auth>{
// Auth getByLoginName(String loginName);
}
3、service和Impi
然后在service/userService目录下创建对应的实例化类UserService和AuthService等,Impi是类实现,写不下;
// UserService
package com.example.testspring.service.userService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.testspring.mapper.userMapper.RoleMapper;
import com.example.testspring.mapper.userMapper.UserMapper;
import com.example.testspring.model.userModel.Role;
import com.example.testspring.model.userModel.User;
import com.example.testspring.model.userModel.UserRole;
import com.example.testspring.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
private UserMapper UserMapper;
@Autowired
private RoleMapper RoleMapper;
@Override
public User getUserAndRolesById(String userId) {
User ltSysUser = UserMapper.findById(userId);
List<Role> roles = RoleMapper.getListByUserId(userId);
ltSysUser.setRoles(roles);
return ltSysUser;
}
@Override
public boolean saveRoleIdsByUserId(String userId, List<String> roleIds) {
UserMapper.deleteRoleByUserId(userId);
List<UserRole> list = roleIds.stream().map(roleId -> {
return new UserRole(roleId, userId);
}).collect(Collectors.toList());
return UserMapper.insertRolesBatch(list);
}
}
//RoseService
package com.example.testspring.service.userService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.testspring.mapper.userMapper.RoleMapper;
import com.example.testspring.model.userModel.Role;
import com.example.testspring.service.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {
@Autowired
private RoleMapper roleMapper;
@Override
public List<Role> getListByUserId(String userId) {
return roleMapper.getListByUserId(userId);
}
}
//AuthService AuthService 类
package com.example.testspring.service.userService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.testspring.mapper.userMapper.AuthMapper;
import com.example.testspring.model.userModel.Auth;
import com.example.testspring.service.AuthService;
import com.example.testspring.utils.SecurityUtil;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class AuthServiceImpl extends ServiceImpl <AuthMapper, Auth> implements AuthService {
@Override
public Auth getByLoginName(String loginName) {
LambdaQueryWrapper<Auth> queryWrapper = Wrappers.<Auth>lambdaQuery()
.eq(Auth::getLoginName, loginName);
List<Auth> ltSysUserAuthList = this.baseMapper.selectList(queryWrapper);
if (CollectionUtils.isEmpty(ltSysUserAuthList)) {
return null;
}
else if (ltSysUserAuthList.size() > 1) {
log.error("用户账号下包含多个用户信息,请检查数据!");
}
return ltSysUserAuthList.get(0);
}
@Override
public boolean updatePwdByUserId(String userId, String newPwd) {
String entryptPassword = SecurityUtil.entryptPassword(newPwd);
LambdaUpdateWrapper<Auth> wrapper = Wrappers.<Auth>lambdaUpdate()
.eq(Auth::getUserId, userId).set(Auth::getPasswd, entryptPassword);
return this.baseMapper.update(null, wrapper) > 0;
}
}
然后在resoureces/mybatis/下创建对应的sql查询xml文件,保持跟mapper文件一一对应,可以参照github;
4、controller
最后,就是controller的restAPI,前端是/user/login这个post请求(参数是loginname和passwd,回传的是一个嵌套json(code+tokes)),因此本地要构造一个对应的API,还有user/info是get请求返回的是用户信息,以login为例,这里的controller需要tokenfilter过滤器来完成对请求的拦截
@PostMapping("/login")
@ApiImplicitParam(name = "req", value = "用户登陆信息", dataType = "LoginReq")
public R login(@RequestBody @NotNull LoginReq req) throws Exception {
String loginName = req.getLoginName();
String passwd = req.getPasswd();
Auth info = authService.getByLoginName(loginName);
// 验证用户状态
LambdaQueryWrapper<User> wrapper = Wrappers.<User>lambdaQuery().eq(User::getUserId, info.getUserId()).eq(User::getStatus, 0);
User user = userService.getOne(wrapper);
Assert.notNull(user, "用户已禁用, 请联系管理员");
if (SecurityUtil.validatePassword(passwd, info.getPasswd())) {
// 封装 Token
String jwtToken = generateByUserInfo(info);
return R.ok(jwtToken);
}
else {
throw new LightUapException(LightErrorCode.FORBIDDEN);
}
}
private String generateByUserInfo(Auth auth) throws Exception {
String userId = auth.getUserId();
User user = userService.getUserAndRolesById(userId);
if (user == null) {
throw new LightUapException("用户信息不存在,请联系管理员");
}
// 查询用户账户信息
LightUserEntity userEntity = BeanConverter.convert(user, LightUserEntity.class);
userEntity.setLoginName(auth.getLoginName());
/**
* 返回前台时,只返回当前用户的角色
*/
List<Role> roles = user.getRoles();
if (!CollectionUtils.isEmpty(roles) && roles.size() >= 1) {
List<LightRoleEntity> collect = roles.stream().map(r -> r.setUsers(null)).map(r -> LightRoleEntity.builder()
.roleId(r.getRoleId()).roleCode(r.getRoleCode()).roleName(r.getRoleName()).roleType(r.getRoleType())
.build()).collect(Collectors.toList());
userEntity.setRoles(collect);
// 默认一个个角色为主要角色
// userEntity.setRole(collect.get(0));
}
// 生成 token
// 去除用户信息中的头像防止token过大
userEntity.setAvatar("");
String jwtToken = LightTokenUtil.createJwtDefaultExp(userEntity);
return jwtToken;
}
5、filter拦截器
包括对于CORS的设置和对请求的拦截都可以写成配置类,这里的tokenInceptor完成了预检飞行和请求的分类,login请求放行,其他请求先拿到header后对token统一进行解析后,再进入API的controller逻辑;
package com.example.testspring.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.testspring.model.userModel.LightUserEntity;
import com.example.testspring.req.LightException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.util.WebUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.Base64;
import java.util.Enumeration;
public class TokenInterceptor extends HandlerInterceptorAdapter {
/**
* 根据请求不同对token进行处理
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 预检请求,预检飞行
if(CorsUtils.isCorsRequest(request) && "OPTIONS".equals(request.getMethod())){
response.setCharacterEncoding( "UTF-8");
response.setContentType( "application/json; charset=utf-8");
response.setStatus(200);
response.setHeader("Access-Control-Allow-Credentials","true");
response.setHeader("Access-Control-Allow-Origin","http://localhost:9527");
response.setHeader("Access-Control-Allow-Headers","x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN,token,username,client");
response.setHeader("Access-Control-Expose-Headers","x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN,token,username,client");
PrintWriter out = null ;
try{
JSONObject res = new JSONObject();
res.put( "200", "sucess");
out = response.getWriter();
out.append(res.toString());
return false;
}
catch (Exception e){
e.printStackTrace();
response.sendError( 500);
return false;
}
}
String accessToken = request.getHeader("Authorization");
//System.out.println(request.getHeader("Authorization"));
//String str=request.getParameter("Authorization");
if (StringUtils.isNotBlank(accessToken)) {
LightUserEntity subject = LightTokenUtil.getSubject(accessToken);
request.getSession().setAttribute("USER_INFO", subject);
return true;
}
throw new LightException("TOKEN不合法,访问拒绝");
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
super.postHandle(request, response, handler, modelAndView);
}
}
最后,使用postman测试登录接口和info接口;
三、用户登录集成的前端改造
默认是mock数据,所以先修改.env.development中BASE_API地址为本地的后端地址;
VUE_APP_BASE_API = 'http://localhost:9090'
测试登录集成完成: