小言_互联网的博客

Vue和Vue-Element-Admin(四):vue-element-admin的用户登录集成

603人阅读  评论(0)

Vue-element-admin使用mock.js数据进行用户和权限的验证,使用该框架开发的首要事情就是用户集成改造,使用本地测试环境的用户和角色信息完成登录验证;

 一、vue-element-admin的登录逻辑

在/views/login目录中index.vue是登录界面,SocialSignin.vue是第三方登录页面,auth-redirect.vue没看懂;


  
  1. login
  2. | components
  3. | | SocialSignin.vue
  4. | auth-redirect.vue
  5. | index.vue

 index.vue中的登录按钮触发handleLogin()这个methods,调用store/user.js中login这个action实现状态的更改,login()这个action再调用api/user/login的接口请求,从后端拿用户数据,如下:


  
  1. // hadleLogin方法完成用户登录动作
  2. handleLogin() {
  3. this.$refs.loginForm.validate( valid => {
  4. if (valid) {
  5. this.loading = true
  6. this.$store.dispatch( 'user/login', this.loginForm)
  7. .then( () => {
  8. this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
  9. this.loading = false
  10. })
  11. .catch( () => {
  12. this.loading = false
  13. })
  14. } else {
  15. console.log( 'error submit!!')
  16. return false
  17. }
  18. })
  19. }
  20. //store/user.js中login这个action的代码:
  21. const actions = {
  22. // user login
  23. login({ commit }, userInfo) {
  24. const { username, password } = userInfo
  25. return new Promise( (resolve, reject) => {
  26. login({ username: username.trim(), password: password }).then( response => {
  27. const { data } = response
  28. commit( 'SET_TOKEN', data.token)
  29. setToken(data.token)
  30. resolve()
  31. }).catch( error => {
  32. reject(error)
  33. })
  34. })
  35. }
  36. }
  37. // api/user/login的接口请求代码:
  38. import request from '@/utils/request'
  39. export function login(data) {
  40. return request({
  41. url: '/vue-element-admin/user/login',
  42. method: 'post',
  43. data
  44. })
  45. }

mock/user.js定义了/vue-element-admin/user/login这个rest请求的返回数据,定义了admin和editor这2个角色和token内容,定义了post请求(参数是loginname)的返回是一个R(code+tokens信息)嵌套json,code是请求的响应码,tokens是用户的单一状态信息(全局维护~),user/getInfo方法根据token参数得到用户和角色信息,后端需要提供相应接口;


  
  1. const tokens = {
  2. admin: {
  3. token: 'admin-token'
  4. },
  5. editor: {
  6. token: 'editor-token'
  7. }
  8. }
  9. const users = {
  10. 'admin-token': {
  11. roles: [ 'admin'],
  12. introduction: 'I am a super administrator',
  13. avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
  14. name: 'Super Admin'
  15. },
  16. 'editor-token': {
  17. roles: [ 'editor'],
  18. introduction: 'I am an editor',
  19. avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',
  20. name: 'Normal Editor'
  21. }
  22. }
  23. module.exports = [
  24. // user login
  25. {
  26. url: '/vue-element-admin/user/login',
  27. type: 'post',
  28. response: config => {
  29. const { username } = config.body
  30. const token = tokens[username]
  31. // mock error
  32. if (!token) {
  33. return {
  34. code: 60204,
  35. message: 'Account and password are incorrect.'
  36. }
  37. }
  38. return {
  39. code: 20000,
  40. data: token
  41. }
  42. }
  43. }
  44. ]

 逻辑并没有结束,前后端分离的路由逻辑写在框架里面,可以发现在登录页已定义redirect(http://localhost:9527/#/login?redirect=%2Fdashboard),因为涉及到路由的跳转就要验证权限问题,在根目录下有个全局的permission.js负责全局的导航守卫,里面定义了首页登录成功后需要在vuex中取权限,vuex再次发起user/getInfo请求得到对应用户的权限(role),如果有权限才会跳转,没有权限报错;


  
  1. import router from './router'
  2. import store from './store'
  3. import { Message } from 'element-ui'
  4. import NProgress from 'nprogress' // progress bar
  5. import 'nprogress/nprogress.css' // progress bar style
  6. import { getToken } from '@/utils/auth' // get token from cookie
  7. import getPageTitle from '@/utils/get-page-title'
  8. NProgress.configure({ showSpinner: false }) // NProgress Configuration
  9. const whiteList = [ '/login', '/auth-redirect'] // no redirect whitelist
  10. //导航守卫,在router之前,拿到to和from以及下一步动作next
  11. router.beforeEach( async(to, from, next) => {
  12. // 开启进度条
  13. NProgress.start()
  14. // 拿title
  15. document.title = getPageTitle(to.meta.title)
  16. // 判断浏览器是否持有token
  17. const hasToken = getToken()
  18. // 有token若是登录页就放行,有token不是登录页就验证是否有权限,有就放行,没有的话在vuex中拿,vuex发起user/getinfo请求拿权限,并记住想要跳转到的页面,也就是redirect的位置;
  19. if (hasToken) {
  20. if (to.path === '/login') {
  21. next({ path: '/' })
  22. NProgress.done() // hack: https://github.com/PanJiaChen/vue-element-admin/pull/2939
  23. } else {
  24. const hasRoles = store.getters.roles && store.getters.roles.length > 0
  25. if (hasRoles) {
  26. next()
  27. } else {
  28. try {
  29. const {roles} = await store.dispatch( 'user/getInfo')
  30. const accessRoutes = await store.dispatch( 'permission/generateRoutes', roles)
  31. router.addRoutes(accessRoutes)
  32. next({...to, replace: true})
  33. } catch (error) {
  34. await store.dispatch( 'user/resetToken')
  35. Message.error(error || 'Has Error')
  36. next( `/login?redirect=${to.path}`)
  37. NProgress.done()
  38. }
  39. }
  40. }
  41. } else {
  42. //没有token的话白名单就放行,不是的话就跳到登录页记录要去的页面;
  43. if (whiteList.indexOf(to.path) !== -1) {
  44. // in the free login whitelist, go directly
  45. next()
  46. } else {
  47. next( `/login?redirect=${to.path}`)
  48. NProgress.done()
  49. }
  50. }
  51. })
  52. router.afterEach( () => {
  53. // 结束进度条
  54. NProgress.done()
  55. })

所以能够得到大致的流程,前端在登录页发起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系列配置完成后端接口的开发;


  
  1. // user表
  2. create table LT_SYS_USER
  3. (
  4. USER_ID VARCHAR( 20) not null
  5. primary key,
  6. USER_CODE VARCHAR( 64),
  7. USER_NAME VARCHAR( 100) not null,
  8. USER_NAME_EN VARCHAR( 100),
  9. ORGAN_ID VARCHAR( 20),
  10. ORGAN_CODE VARCHAR( 64),
  11. ORGAN_NAME VARCHAR( 100),
  12. EMAIL TEXT,
  13. MOBILE VARCHAR( 100),
  14. PHONE VARCHAR( 100),
  15. SEX INT,
  16. AVATAR TEXT,
  17. SIGN VARCHAR( 200),
  18. USER_SORT INT,
  19. STATUS INT not null,
  20. CREATE_BY VARCHAR( 64),
  21. CREATE_DATE DATETIME,
  22. UPDATE_BY VARCHAR( 64),
  23. UPDATE_DATE DATETIME,
  24. REMARKS TEXT,
  25. DELETED VARCHAR( 5) not null,
  26. CORP_ID VARCHAR( 64),
  27. REG_DATE DATETIME
  28. );
  29. // role角色表
  30. create table LT_SYS_ROLE
  31. (
  32. ROLE_ID VARCHAR( 20) not null
  33. primary key,
  34. ROLE_CODE VARCHAR( 64) not null,
  35. ROLE_NAME VARCHAR( 100) not null,
  36. ROLE_TYPE VARCHAR( 100),
  37. ROLE_SORT INT,
  38. DATA_SCOPE INT,
  39. BIZ_SCOPE VARCHAR( 255),
  40. DELETED TINYINT not null,
  41. CREATE_BY VARCHAR( 64) not null,
  42. CREATE_DATE DATETIME not null,
  43. UPDATE_BY VARCHAR( 64),
  44. UPDATE_DATE DATETIME,
  45. REMARKS TEXT,
  46. CORP_ID VARCHAR( 64)
  47. );
  48. //auth权限表
  49. create table LT_SYS_USER_AUTH
  50. (
  51. AUTH_ID VARCHAR( 20) not null
  52. primary key,
  53. USER_ID VARCHAR( 20) not null,
  54. LOGIN_NAME VARCHAR( 100) not null,
  55. PASSWD VARCHAR( 100) not null,
  56. TOKEN TEXT,
  57. EXPIRE_TIME DATETIME,
  58. DINGTALK_OPENID LONGTEXT,
  59. WELINK_OPENID VARCHAR( 100),
  60. WX_OPENID VARCHAR( 100),
  61. MOBILE_IMEI VARCHAR( 100),
  62. USER_TYPE VARCHAR( 16),
  63. PWD_SECURITY_LEVEL INT,
  64. PWD_UPDATE_DATE DATETIME,
  65. PWD_UPDATE_RECORD TEXT,
  66. PWD_QUEST_UPDATE_DATE DATETIME,
  67. LAST_LOGIN_IP VARCHAR( 100),
  68. LAST_LOGIN_DATE DATETIME,
  69. FREEZE_DATE DATETIME,
  70. FREEZE_CAUSE VARCHAR( 200),
  71. USER_WEIGHT INT,
  72. CREATE_BY VARCHAR( 64),
  73. CREATE_DATE DATETIME,
  74. UPDATE_BY VARCHAR( 64),
  75. UPDATE_DATE DATETIME,
  76. REMARKS TEXT,
  77. DELETED INT default 0,
  78. STATUS VARCHAR( 100)
  79. );
  80. // USER和role维表
  81. create table LT_SYS_USER_ROLE
  82. (
  83. USER_ID VARCHAR( 20) not null,
  84. ROLE_ID VARCHAR( 20) not null,
  85. primary key (USER_ID, ROLE_ID)
  86. );
  87. comment on column LT_SYS_USER_ROLE.USER_ID is '用户id';
  88. 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再自定义一些查询;


  
  1. // usermapper接口
  2. package com.example.testspring.mapper.userMapper;
  3. import com.example.testspring.model.userModel.Role;
  4. import com.example.testspring.model.userModel.User;
  5. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  6. import com.example.testspring.model.userModel.UserRole;
  7. import org.apache.ibatis.annotations.Mapper;
  8. import org.apache.ibatis.annotations.Param;
  9. import java.util.List;
  10. @Mapper
  11. public interface UserMapper extends BaseMapper<User>{
  12. User getByIdLazy(String userId);
  13. User findById(String userId);
  14. boolean deleteRoleByUserId(String userId);
  15. boolean insertRolesBatch(@Param("list") List<UserRole> list);}
  16. //RoleMapper 接口
  17. package com.example.testspring.mapper.userMapper;
  18. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  19. import com.example.testspring.model.userModel.Role;
  20. import org.apache.ibatis.annotations.Mapper;
  21. import java.util.List;
  22. @Mapper
  23. public interface RoleMapper extends BaseMapper<Role> {
  24. List<Role> getListByUserId(String userId);
  25. Role findById(String roleId);
  26. }
  27. //AuthMapper
  28. package com.example.testspring.mapper.userMapper;
  29. import com.example.testspring.model.userModel.Auth;
  30. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
  31. import org.apache.ibatis.annotations.Mapper;
  32. @Mapper
  33. public interface AuthMapper extends BaseMapper<Auth>{
  34. // Auth getByLoginName(String loginName);
  35. }

 3、service和Impi

然后在service/userService目录下创建对应的实例化类UserService和AuthService等,Impi是类实现,写不下;


  
  1. // UserService
  2. package com.example.testspring.service.userService;
  3. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  4. import com.example.testspring.mapper.userMapper.RoleMapper;
  5. import com.example.testspring.mapper.userMapper.UserMapper;
  6. import com.example.testspring.model.userModel.Role;
  7. import com.example.testspring.model.userModel.User;
  8. import com.example.testspring.model.userModel.UserRole;
  9. import com.example.testspring.service.UserService;
  10. import org.springframework.beans.factory.annotation.Autowired;
  11. import org.springframework.stereotype.Service;
  12. import java.util.List;
  13. import java.util.stream.Collectors;
  14. @Service
  15. public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
  16. @Autowired
  17. private UserMapper UserMapper;
  18. @Autowired
  19. private RoleMapper RoleMapper;
  20. @Override
  21. public User getUserAndRolesById(String userId) {
  22. User ltSysUser = UserMapper.findById(userId);
  23. List<Role> roles = RoleMapper.getListByUserId(userId);
  24. ltSysUser.setRoles(roles);
  25. return ltSysUser;
  26. }
  27. @Override
  28. public boolean saveRoleIdsByUserId(String userId, List<String> roleIds) {
  29. UserMapper.deleteRoleByUserId(userId);
  30. List<UserRole> list = roleIds.stream().map(roleId -> {
  31. return new UserRole(roleId, userId);
  32. }).collect(Collectors.toList());
  33. return UserMapper.insertRolesBatch(list);
  34. }
  35. }
  36. //RoseService
  37. package com.example.testspring.service.userService;
  38. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  39. import com.example.testspring.mapper.userMapper.RoleMapper;
  40. import com.example.testspring.model.userModel.Role;
  41. import com.example.testspring.service.RoleService;
  42. import org.springframework.beans.factory.annotation.Autowired;
  43. import org.springframework.stereotype.Service;
  44. import java.util.List;
  45. @Service
  46. public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {
  47. @Autowired
  48. private RoleMapper roleMapper;
  49. @Override
  50. public List<Role> getListByUserId(String userId) {
  51. return roleMapper.getListByUserId(userId);
  52. }
  53. }
  54. //AuthService AuthService 类
  55. package com.example.testspring.service.userService;
  56. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  57. import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  58. import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
  59. import com.baomidou.mybatisplus.core.toolkit.Wrappers;
  60. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  61. import com.example.testspring.mapper.userMapper.AuthMapper;
  62. import com.example.testspring.model.userModel.Auth;
  63. import com.example.testspring.service.AuthService;
  64. import com.example.testspring.utils.SecurityUtil;
  65. import org.springframework.stereotype.Service;
  66. import java.util.List;
  67. @Service
  68. public class AuthServiceImpl extends ServiceImpl <AuthMapper, Auth> implements AuthService {
  69. @Override
  70. public Auth getByLoginName(String loginName) {
  71. LambdaQueryWrapper<Auth> queryWrapper = Wrappers.<Auth>lambdaQuery()
  72. .eq(Auth::getLoginName, loginName);
  73. List<Auth> ltSysUserAuthList = this.baseMapper.selectList(queryWrapper);
  74. if (CollectionUtils.isEmpty(ltSysUserAuthList)) {
  75. return null;
  76. }
  77. else if (ltSysUserAuthList.size() > 1) {
  78. log.error( "用户账号下包含多个用户信息,请检查数据!");
  79. }
  80. return ltSysUserAuthList.get( 0);
  81. }
  82. @Override
  83. public boolean updatePwdByUserId(String userId, String newPwd) {
  84. String entryptPassword = SecurityUtil.entryptPassword(newPwd);
  85. LambdaUpdateWrapper<Auth> wrapper = Wrappers.<Auth>lambdaUpdate()
  86. .eq(Auth::getUserId, userId).set(Auth::getPasswd, entryptPassword);
  87. return this.baseMapper.update( null, wrapper) > 0;
  88. }
  89. }

然后在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过滤器来完成对请求的拦截


  
  1. @PostMapping("/login")
  2. @ApiImplicitParam(name = "req", value = "用户登陆信息", dataType = "LoginReq")
  3. public R login(@RequestBody @NotNull LoginReq req) throws Exception {
  4. String loginName = req.getLoginName();
  5. String passwd = req.getPasswd();
  6. Auth info = authService.getByLoginName(loginName);
  7. // 验证用户状态
  8. LambdaQueryWrapper<User> wrapper = Wrappers.<User>lambdaQuery().eq(User::getUserId, info.getUserId()).eq(User::getStatus, 0);
  9. User user = userService.getOne(wrapper);
  10. Assert.notNull(user, "用户已禁用, 请联系管理员");
  11. if (SecurityUtil.validatePassword(passwd, info.getPasswd())) {
  12. // 封装 Token
  13. String jwtToken = generateByUserInfo(info);
  14. return R.ok(jwtToken);
  15. }
  16. else {
  17. throw new LightUapException(LightErrorCode.FORBIDDEN);
  18. }
  19. }
  20. private String generateByUserInfo(Auth auth) throws Exception {
  21. String userId = auth.getUserId();
  22. User user = userService.getUserAndRolesById(userId);
  23. if (user == null) {
  24. throw new LightUapException( "用户信息不存在,请联系管理员");
  25. }
  26. // 查询用户账户信息
  27. LightUserEntity userEntity = BeanConverter.convert(user, LightUserEntity.class);
  28. userEntity.setLoginName(auth.getLoginName());
  29. /**
  30. * 返回前台时,只返回当前用户的角色
  31. */
  32. List<Role> roles = user.getRoles();
  33. if (!CollectionUtils.isEmpty(roles) && roles.size() >= 1) {
  34. List<LightRoleEntity> collect = roles.stream().map(r -> r.setUsers( null)).map(r -> LightRoleEntity.builder()
  35. .roleId(r.getRoleId()).roleCode(r.getRoleCode()).roleName(r.getRoleName()).roleType(r.getRoleType())
  36. .build()).collect(Collectors.toList());
  37. userEntity.setRoles(collect);
  38. // 默认一个个角色为主要角色
  39. // userEntity.setRole(collect.get(0));
  40. }
  41. // 生成 token
  42. // 去除用户信息中的头像防止token过大
  43. userEntity.setAvatar( "");
  44. String jwtToken = LightTokenUtil.createJwtDefaultExp(userEntity);
  45. return jwtToken;
  46. }

5、filter拦截器 

包括对于CORS的设置和对请求的拦截都可以写成配置类,这里的tokenInceptor完成了预检飞行和请求的分类,login请求放行,其他请求先拿到header后对token统一进行解析后,再进入API的controller逻辑;


  
  1. package com.example.testspring.utils;
  2. import com.alibaba.fastjson.JSON;
  3. import com.alibaba.fastjson.JSONObject;
  4. import com.example.testspring.model.userModel.LightUserEntity;
  5. import com.example.testspring.req.LightException;
  6. import org.apache.commons.lang3.StringUtils;
  7. import org.springframework.http.server.reactive.ServerHttpRequest;
  8. import org.springframework.http.server.reactive.ServerHttpResponse;
  9. import org.springframework.web.cors.CorsUtils;
  10. import org.springframework.web.server.ServerWebExchange;
  11. import org.springframework.web.servlet.ModelAndView;
  12. import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
  13. import org.springframework.web.util.WebUtils;
  14. import javax.servlet.http.HttpServletRequest;
  15. import javax.servlet.http.HttpServletResponse;
  16. import java.io.PrintWriter;
  17. import java.util.Base64;
  18. import java.util.Enumeration;
  19. public class TokenInterceptor extends HandlerInterceptorAdapter {
  20. /**
  21. * 根据请求不同对token进行处理
  22. * @param request
  23. * @param response
  24. * @param handler
  25. * @return
  26. * @throws Exception
  27. */
  28. @Override
  29. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  30. // 预检请求,预检飞行
  31. if(CorsUtils.isCorsRequest(request) && "OPTIONS".equals(request.getMethod())){
  32. response.setCharacterEncoding( "UTF-8");
  33. response.setContentType( "application/json; charset=utf-8");
  34. response.setStatus( 200);
  35. response.setHeader( "Access-Control-Allow-Credentials", "true");
  36. response.setHeader( "Access-Control-Allow-Origin", "http://localhost:9527");
  37. response.setHeader( "Access-Control-Allow-Headers", "x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN,token,username,client");
  38. response.setHeader( "Access-Control-Expose-Headers", "x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN,token,username,client");
  39. PrintWriter out = null ;
  40. try{
  41. JSONObject res = new JSONObject();
  42. res.put( "200", "sucess");
  43. out = response.getWriter();
  44. out.append(res.toString());
  45. return false;
  46. }
  47. catch (Exception e){
  48. e.printStackTrace();
  49. response.sendError( 500);
  50. return false;
  51. }
  52. }
  53. String accessToken = request.getHeader( "Authorization");
  54. //System.out.println(request.getHeader("Authorization"));
  55. //String str=request.getParameter("Authorization");
  56. if (StringUtils.isNotBlank(accessToken)) {
  57. LightUserEntity subject = LightTokenUtil.getSubject(accessToken);
  58. request.getSession().setAttribute( "USER_INFO", subject);
  59. return true;
  60. }
  61. throw new LightException( "TOKEN不合法,访问拒绝");
  62. }
  63. @Override
  64. public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
  65. ModelAndView modelAndView) throws Exception {
  66. super.postHandle(request, response, handler, modelAndView);
  67. }
  68. }

最后,使用postman测试登录接口和info接口;

三、用户登录集成的前端改造

默认是mock数据,所以先修改.env.development中BASE_API地址为本地的后端地址;

VUE_APP_BASE_API = 'http://localhost:9090'

 测试登录集成完成:


转载:https://blog.csdn.net/yezonggang/article/details/109850163
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场