谷粒学院项目中权限管理模块BUG解决

本博文是在权限管理模块的前后端都已经搭建完成的基础上,针对其中的bug进行解决的。

1、权限管理流程

(1)前端项目启动

前端项目启动后,会自动访问 http://localhost:9528/ ,但是实际跳转的是http://localhost:9528/login

这是由于在src目录下的permission.js中定义了路由跳转之前的判断规则。

// 该操作会在路由跳转之前执行
router.beforeEach(async(to, from, next) => {
  // start progress bar
  NProgress.start()

  // 再路由跳转之前获取用户存储在cookie中token
  const hasToken = getToken()
  
  // 已登录,进入这里
  if (hasToken) {
    if (to.path === '/login') {
      // if is logged in, redirect to the home page
      next({ path: '/' })
      NProgress.done()
    } else {
      // determine whether the user has obtained his permission roles through getInfo
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      if (hasRoles) {
        next()
      } else {
        try {
            
          const { roles } = await store.dispatch('GetInfo')
          // generate accessible routes map based on roles
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)

          // dynamically add accessible routes

          router.addRoutes(accessRoutes)

          next({ ...to, replace: true })
        } catch (error) {
          // remove token and go to login page to re-login
          await store.dispatch('FedLogOut')
          Message.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  }
    
 // 没有登录,进入这里
 else {
    /* has no token*/
    // 验证要跳转的路由地址是否在白名单中
    if (whiteList.indexOf(to.path) !== -1) {
      // 在白名单中,直接跳转
      next()
    } else {
      // 不在白名单中,跳转到登录页面
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})

以上的代码,在路由跳转之前进行判断,如果已经登录(判断标准为是否可以获取到token),直接跳转到相关页面。没有登录,跳转到登录页面进行登录。

其中获取token的方法由 import { getToken } from ‘@/utils/auth’ 导入,该js文件中定义了token从cookie中获取。

export function getToken() {
  return Cookies.get(TokenKey)
}

(2)没有登录token的执行流程

// 路由跳转的白名单
const whiteList = ['/login', '/auth-redirect']

// 没有登录,进入这里
 else {
    /* has no token*/
    // 验证要跳转的路由地址是否在白名单中
    if (whiteList.indexOf(to.path) !== -1) {
      // 在白名单中,直接跳转
      next()
    } else {
      // 不在白名单中,跳转到登录页面
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }

先判断是否在白名单中,是则跳转。

不是的话,跳转到登录页面

以上两个步骤都是在获取登录之后的信息,那么是如何登录的呢,页面上定义的跳转到登录页面的代码为:

 next(`/login?redirect=${to.path}`)

要跳转的页面为:src/views/login/index.vue,在该页面上有一系列的校验方法去获取正确的表单信息,然后点击登录时的处理方法为;

<el-button :loading="loading" type="primary" style="width:100%;" @click="handleLogin">
    登录
</el-button>
<script>
	 handleLogin() {
      // 表单数据校验
      this.$refs.loginForm.validate(valid => {
        if (valid) {
         // debugger
          this.loading = true
          // 调用函数进行登录
          this.$store.dispatch('Login', this.loginForm).then(() => {
            this.loading = false
            this.$router.push({ path: this.redirect || '/' })
          }).catch(() => {
            this.loading = false
          })
        } else {
          console.log('error submit!!')
          return false
        }
      })
    }
</script>

这里去调用登录的函数时明显使用了很明显用了单一状态管理库vuex,dispatch派发了一个异步动作,具体要调用的函数是:

src/store/module/user.js中定义。

import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'

const user = {
  state: {
    token: getToken(),
    name: '',
    avatar: '',
    buttons: [],
    roles: []
  },
}

actions: {
    // 登录
    Login({ commit }, userInfo) {
      const username = userInfo.username.trim()
      return new Promise((resolve, reject) => {
        // 调用定义的登录函数
        login(username, userInfo.password).then(response => {
         // debugger
          const data = response.data
          setToken(data.token)
          commit('SET_TOKEN', data.token)
          resolve()
        }).catch(error => {
          reject(error)
        })
      })
    },
  }
}

export default user

该函数调用了src/api/login.js中的函数

// 登录
export function login(username, password) {
 // debugger
  return request({
    url: '/admin/acl/login',
    method: 'post',
    data: {
      username,
      password
    }
  })
}

后端方法,这个方法在service-acl模块中是无法找到的,因为这是springsecurity自己定义的登录界面。

接下来去追溯在common/spring-security中定义的执行逻辑

SpringSecurity的权限管理拦截规则一般都在定义的配置类中定义。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {

    private UserDetailsService userDetailsService;
    private TokenManager tokenManager;
    private DefaultPasswordEncoder defaultPasswordEncoder;
    private RedisTemplate redisTemplate;

    @Autowired
    public TokenWebSecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder,
                                  TokenManager tokenManager, RedisTemplate redisTemplate) {
        this.userDetailsService = userDetailsService;
        this.defaultPasswordEncoder = defaultPasswordEncoder;
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }

    /**
     * 配置设置
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()
                .authenticationEntryPoint(new UnauthorizedEntryPoint())
                .and().csrf().disable()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and().logout().logoutUrl("/admin/acl/index/logout")
                .addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate)).and()
                .addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))
                .addFilter(new TokenAuthenticationFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic();
    }

    /**
     * 密码处理
     * @param auth
     * @throws Exception
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
    }

    /**
     * 配置哪些请求不拦截
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
//        web.ignoring().antMatchers("/api/**",
//                "/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**"
//               );
        //web.ignoring().antMatchers("/*/**");
    }
}

注:springSecurity使用的是过滤器Filter而不是拦截器Interceptor,意味着SpringSecurity 能够管理的不仅仅是 SpringMVC 中的 handler 请求,还包含 Web 应用中所有请求。比如: 项目中的静态资源也会被拦截,从而进行权限控制。

配置类中需要注意的点为:

  • @EnableWebSecurity 注解表示启用 Web 安全功能。

  • springsecurity的过滤器链中添加了两个自定义的过滤器类,其中一个是TokenLoginFilter,另一个是TokenAuthenticationFilter,这里重点去跟踪TokenLoginFilter。

    TokenLoginFilter:登录过滤器

    public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
    
        private AuthenticationManager authenticationManager;
        private TokenManager tokenManager;
        private RedisTemplate redisTemplate;
    
        public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
            this.authenticationManager = authenticationManager;
            this.tokenManager = tokenManager;
            this.redisTemplate = redisTemplate;
            this.setPostOnly(false);
            // 定义了登录要访问的地址
            this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));
        }
    	
        // 登录的处理方法
        @Override
        public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
                throws AuthenticationException {
            try {
                // 获取用户信息
                User user = new ObjectMapper().readValue(req.getInputStream(), User.class);
                System.out.println(user);
                // 
                return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
    
        }
    

    访问登录处理方法时

      User user = new ObjectMapper().readValue(req.getInputStream(), User.class);
    

    用来获取用户信息,这里的User和service_acl中entity中的User.java相似,只是部分字段不同。

    • service_acl中的User对应的是数据库中的acl_user表
    • spring_security中的User.java是spring security要用的。

    获取用户登录信息之后,执行下面的代码

     return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
    

    执行以上代码的时候会去service_acl模块下的UserDetailsServiceImpl.java中获取用户信息和用户权限列表。

    @Service("userDetailsService")
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Autowired
        private UserService userService;
    
        @Autowired
        private PermissionService permissionService;
    
        // 根据账号获取用户信息
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
            // 从数据库中取出用户信息
            User user=userService.selectByUsername(username);
    
            // 判断用户是否存在
            if(null==user){
    
            }
    
            // 讲师数据库中查询的用户转换为SpringSecurity需要的登录用户类型
            com.zjl.security.entity.User curUser = new com.zjl.security.entity.User();
            BeanUtils.copyProperties(user,curUser);
            
    		// 调acl模块下的service获取用户的权限信息
            List<String> authorities = permissionService.selectPermissionValueByUserId(user.getId());
            
    		// 根据SpringSecurity需要的登录用户和用户的权限信息创建SecurityUser对象
            SecurityUser securityUser = new SecurityUser(curUser);
    
            securityUser.setPermissionValueList(authorities);
    
    
            return securityUser;
        }
    }
    

    获取到用户信息后,登录成功,调用登录过滤器中的登录成功方法,将登录token存入redis中。

        @Override
        protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
                                                Authentication auth) throws IOException, ServletException {
            SecurityUser user = (SecurityUser) auth.getPrincipal();
            String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
            // 认证成功,将登陆的用户信息放入redis中
            redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());
    
            ResponseUtil.out(res, R.ok().data("token", token));
        }
    

    否则登录失败,抛出异常提示。

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {
        ResponseUtil.out(response, R.error());
    	}
    }
    

(3)有token时如何根据不同用户获取不同的界面

 // 已登录,进入这里
  if (hasToken) {
    if (to.path === '/login') {
      // if is logged in, redirect to the home page
      next({ path: '/' })
      NProgress.done()
    } else {
      // determine whether the user has obtained his permission roles through getInfo
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      if (hasRoles) {
        next()
      } else {
        try {
            
          const { roles } = await store.dispatch('GetInfo')
          // generate accessible routes map based on roles
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)

          // dynamically add accessible routes

          router.addRoutes(accessRoutes)

          next({ ...to, replace: true })
        } catch (error) {
          // remove token and go to login page to re-login
          await store.dispatch('FedLogOut')
          Message.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  }

上面的代码中,如果有token且要跳转的页面是登录页面的话,直接重定向到首页。否则,先根据用户的token获取角色的权限。

代码中获取角色的方法为:

import store from './store' 
const hasRoles = store.getters.roles && store.getters.roles.length > 0

src/store/getters中的方法

const getters = {
  sidebar: state => state.app.sidebar,
  device: state => state.app.device,
  token: state => state.user.token,
  avatar: state => state.user.avatar,
  name: state => state.user.name,
  buttons: state => state.user.buttons,
  roles: state => state.user.roles,
  routes: state => state.permission.routes,
  addRoutes: state => state.permission.addRoutes
}
export default getters

获取用户角色的方法在src/store/module/user.js中的 GetInfo() 方法。

import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'

const user = {
  state: {
    token: getToken(),
    name: '',
    avatar: '',
    buttons: [],
    roles: []
  },
}

actions: {

    // 获取用户信息
    async GetInfo({ commit, state }) {
      return new Promise((resolve, reject) => {
        getInfo(state.token).then(response => {
          // debugger
          const data = response.data
          // 从用户信息中获取用户角色
          if (data.roles && data.roles.length > 0) { // 验证返回的roles是否是一个非空数组
            commit('SET_ROLES', data.roles)
          } else {
            reject('getInfo: roles must be a non-null array !')
          }

          const buttonAuthList = []
          data.permissionValueList.forEach(button => {
            buttonAuthList.push(button)
          })

          commit('SET_NAME', data.name)
          commit('SET_AVATAR', data.avatar)
          commit('SET_BUTTONS', buttonAuthList)
          resolve(response)
        }).catch(error => {
          reject(error)
        })
      })
    },
}

export default user

定义的调用后端接口的方法,src/api/login.js中的getInfo() 方法。

import request from '@/utils/request'

// 登录
export function login(username, password) {
 // debugger
  return request({
    url: '/admin/acl/login',
    method: 'post',
    data: {
      username,
      password
    }
  })
}

// 获取用户信息
export function getInfo(token) {
  return request({
    url: '/admin/acl/index/info',
    method: 'get',
    params: { token }
  })
}

// 登出
export function logout() {
  //debugger
  return request({
    url: '/admin/acl/index/logout',
    method: 'post'
  })
}

// 获取菜单权限数据
export function getMenu() {
  return request({
    url: '/admin/acl/index/menu',
    method: 'get'
  })
}

访问对应的service-acl模块中的controller相关方法之前,会先经过SpringSecurity中定义的访问过滤器。

public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;

    public TokenAuthenticationFilter(AuthenticationManager authManager, TokenManager tokenManager,RedisTemplate redisTemplate) {
        super(authManager);
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        logger.info("================="+req.getRequestURI());
        // 判断是不是admin,超级管理员用户,如是,直接通过。
        if(req.getRequestURI().indexOf("admin") == -1) {
            chain.doFilter(req, res);
            return;
        }
		// 不是admin,超级管理员用户
        UsernamePasswordAuthenticationToken authentication = null;
        try {
            // 根据请求获取用户权限
            authentication = getAuthentication(req);
        } catch (Exception e) {
            ResponseUtil.out(res, R.error());
        }
		// 权限不为空
        if (authentication != null) {
            // 设置权限
            SecurityContextHolder.getContext().setAuthentication(authentication);
        } else {
            ResponseUtil.out(res, R.error());
        }
        chain.doFilter(req, res);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        // token置于header里
        String token = request.getHeader("token");
        if (token != null && !"".equals(token.trim())) {
            // 根据token获取用户信息
            String userName = tokenManager.getUserFromToken(token);
            
			// 从redis中获取用户信息
            List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName);
            
            // 权限封装
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            for(String permissionValue : permissionValueList) {
                if(StringUtils.isEmpty(permissionValue)) continue;
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
                authorities.add(authority);
            }
			// 用户存在权限,给用户添加权限并返回。
            if (!StringUtils.isEmpty(userName)) {
                return new UsernamePasswordAuthenticationToken(userName, token, authorities);
            }
            return null;
        }
        return null;
    }
}

经过访问过滤器中的方法获取到用户的权限信息后,访问service-acl中的controller方法,获取用户的具体信息,返回给前端,设置到cookie中。

  /**
     * 根据token获取用户信息
     */
@GetMapping("info")
public R info(){
    // 获取当前登录用户用户名
    String username = SecurityContextHolder.getContext().getAuthentication().getName();
    Map<String, Object> userInfo = indexService.getUserInfo(username);
    return R.ok().data(userInfo);
}

(4)总体的权限调用流程图

谷粒学院项目中权限管理模块BUG解决

2、菜单管理不显示

搞清楚上述的权限调用流程之后,使用admin用户登录,登录后的显示效果为:

谷粒学院项目中权限管理模块BUG解决

树形表格并未完全显示,后台的代码并未出现任何问题,前台打印从后端获取的数据有数据,但是树形结构不显示。

其原因是 element ui 的版本不够,所以显示出现问题。

执行下面的代码,更换其版本。

npm uninstall element-ui -S

npm install element-ui -S

更改后的显示效果为:
谷粒学院项目中权限管理模块BUG解决

3、角色的权限不显示

对不同的角色赋予不同的权限时,由于树形控件只选择一个节点时,父类的id不会被传递,而后台展示数据时需要从父节点开始遍历,所以赋值后登录用户无法展示菜单。

谷粒学院项目中权限管理模块BUG解决

要解决树形控件,没全选时,无法获取父级id的问题,需要将 node_modules\element-ui\lib\element-ui.common.js 中TreeStore.prototype.getCheckedNodes方法,将其中的

childNodes.forEach(function (child) {
    if ((child.checked || includeHalfChecked && child.indeterminate) && (!leafOnly || leafOnly && child.isLeaf)) {
        checkedNodes.push(child.data);
}

改为:

if ((child.checked || child.indeterminate) && (!leafOnly || leafOnly && child.isLeaf)) {
    checkedNodes.push(child.data)
}
上一篇:MongoDB--安全认证


下一篇:洛谷题解 P1125 【笨小猴】