基于前后端分离的开发模式中,权限控制分为前端可见性权限与后端API接口可访问性权限。
前端的权限控制 主要围绕在菜单是否对该角色可见,以及是否具有操作该按钮的权限两方面展开的

1. 前端可见性 权限 实现思路

vue工程中,菜单可以简单的理解为vue中的路由,只需要根据登录用户的权限信息动态的加载路由列表就可以动态的构造出访问菜单。

  1. 登录成功后获取用户信息,包含权限列表(菜单权限,按钮权限)
  2. 根据用户菜单权限列表,动态构造路由(根据路由名称和权限标识比较)
  3. 页面按钮权限通过自定义方法控制可见性

1.1 路由钩子函数

vue路由提供的钩子函数(beforeEach)主要用来在加载之前拦截导航,让它完成跳转或取消。可以在路由钩子函数中进行校验是否对某个路由具有访问权限

router\index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
router.beforeEach((to, from, next) => {
NProgress.start() //loading start
if (getToken()) {
if (to.path === '/login') {
next({path: '/'})
NProgress.done()
} else {
if (store.getters.roles.length === 0) {
// 判断当前用户是否已拉取完user_info信息
store.dispatch('GetUserInfo').then(res => {
// 拉取user_info: ['editor','develop']
const roles = res.data.data.roles
store.dispatch('GenerateRoutes', {roles}).then(() => {
// 根据roles权限生成可访问的路由表
// 动态添加可访问路由表
router.addRoutes(store.getters.addRouters)
// hack方法 确保addRoutes已完成
next({...to, replace: true})
})
}).catch(() => {
store.dispatch('FedLogOut').then(() => {
Message.error('验证失败, 请重新登录')
next({path: '/login'})
})
})
} else {
next()
}
}
} else {
/* has no token */
if (whiteList.indexOf(to.path) !== -1) {
// 在免登录白名单,直接进入
next()
} else {
next('/login') // 否则全部重定向到登录页
NProgress.done()
}
}
})

1.2 配置菜单权限

store\permission.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 检查是否有权限
*/
function hasPermission(roles, route) {
if (roles.menus && route.name) {
return roles.menus.some(role => {
return route.name.toLowerCase() === role.toLowerCase()
})
} else {
return false
}
}
/**
* 递归过滤异步路由表,返回符合用户角色权限的路由表
* @param asyncRouterMap
* @param roles
*/
function filterAsyncRouter(asyncRouterMap, roles) {
const accessedRouters = asyncRouterMap.filter(route => {
if (hasPermission(roles, route)) {
return true
}
return false
})
return accessedRouters
}

const permission = {
state: {
routers: constantRouterMap,
addRouters: []
},
mutations: {
SET_ROUTERS: (state, routers) => {
state.addRouters = routers
state.routers = constantRouterMap.concat(routers)
}
},
actions: {
GenerateRoutes({ commit }, data) {
return new Promise(resolve => {
const { roles } = data
//动态构造权限列表
// 调试开启全部路由
commit('SET_ROUTERS', asyncRouterMap)
resolve()
})
}
}
}

2. 后端API接口可访问性 权限 实现思路

服务端首先需要接受跨域请求,客户端登录时签发Token令牌给客户端,此后客户端在请求受保护的资源时携带这个Token,服务端进行验证再从这个Token中解析出用户的身份信息。
具体权限流程:

  1. 用户使用username和password登陆,将password通过MD5加密,通过username查询库中是否有该条记录,并比较加密后的密码是否相同,登陆成功后利用JwtUtil生成带过期时间的token,以后发送请求时都需要在header中添加Authorization字段附加该token信息;
  2. 结合程序实现一个JwtUtil,在其中实现利用登陆信息生成token,根据token获取username,token验证等方法;
  3. 实现一个JWTFilter继承BasicHttpAuthenticationFilter类,该拦截器需要拦截所有请求除(除登陆、注册等请求),用于判断请求是否带有token,并获取token的值传递给shiro的登陆认证方法作为参数,用于获取token;
  4. 定义ShiroRealm继承AuthorizingRealm类,在其中实现登陆验证及权限获取的方法;
  5. 定义ShiroConfig配置类,用于生成ShiroManage及将shiroRealm付给ShiroManage,并将jwtFilter添加进shiro的拦截器链中
  6. controller中可以使用@RequiresPermissions来对用户权限进行拦截;

2.1 JwtUtil

JwtUtil.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class JwtUtil {
//设置过期时间,这里设置15分钟
private static final long EXPIRE_TIME = 15 * 60 * 1000;
//服务端的私钥secret,在任何场景都不应该流露出去
private static final String TOKEN_SECRET = "zhengchao";
/**
* 生成签名
* @param **User**
* @param **password**
* @return
*/
public static String createToken(User user) {
try {
// 设置过期时间
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
// 私钥和加密算法
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
// 设置头部信息
Map<String, Object> header = new HashMap<>(2);
header.put("typ", "JWT");
header.put("alg", "HS256");
// 返回token字符串
return JWT.create()
.withHeader(header)
.withClaim("aud", user.getName())
.withClaim("uid", user.getId())
.withExpiresAt(date)
.sign(algorithm);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 检验token是否正确
* @param **token**
* @return
*/
public static boolean isVerify(String token){
try {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(token);
return true;
} catch (Exception e){
return false;
}
}
/**
*从token解析出uid信息,用户ID
* @param token
* @param key
* @return
*/
public static int parseTokenUid(String token) {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("uid").asInt();
}
/**
*从token解析出aud信息,用户名
* @param token
* @param key
* @return
*/
public static String parseTokenAud(String token) {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("aud").asString();
}
}

2.2 JWTFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class JWTFilter extends BasicHttpAuthenticationFilter {

    private static final String TOKEN = "Authentication";

    private AntPathMatcher pathMatcher = new AntPathMatcher();

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String[] anonUrl = StringUtils.splitByWholeSeparatorPreserveAllTokens(BraveConstant.ANON_URL, StringPool.COMMA);

        boolean match = false;
        for (String u : anonUrl) {
            if (pathMatcher.match(u, httpServletRequest.getRequestURI()))
                match = true;
        }
        if (match) return true;
        if (isLoginAttempt(request, response)) {
            return executeLogin(request, response);
        }
        return false;
    }

    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader(TOKEN);
        return token != null;
    }

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader(TOKEN);
        JWTToken jwtToken = new JWTToken(BraveUtil.decryptToken(token));
        try {
            //将token传递给shiroRealm
            getSubject(request, response).login(jwtToken);
            return true;
        } catch (Exception e) {
            log.error(e.getMessage());
            return false;
        }
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个 option请求,这里我们给 option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

2.3 ShiroRealm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class ShiroRealm extends AuthorizingRealm {

    @Autowired
    private RedisService redisService;

    @Autowired
    private BrUserManager brUserManager;

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    /**`
     * 授权模块,获取用户角色和权限
     *
     * @param token token
     * @return AuthorizationInfo 权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection token) {
        long userId = JWTUtil.getUserTokenDto(token.toString()).getUserId();
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        // 获取用户权限集
        Set<String> permissionSet = brUserManager.getUserPermissions(userId);
        simpleAuthorizationInfo.setStringPermissions(permissionSet);
        return simpleAuthorizationInfo;
    }

    /**
     * 用户认证
     *
     * @param authenticationToken 身份认证 token
     * @return AuthenticationInfo 身份认证信息
     * @throws AuthenticationException 认证相关异常
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 这里的 token是从 JWTFilter 的 executeLogin 方法传递过来的
        String token = (String) authenticationToken.getCredentials();

        TokenDto tokenDto = JWTUtil.getUserTokenDto(token);

        if (tokenDto == null)
            throw new AuthenticationException("token校验不通过");

        // 通过用户名查询用户信息
        BrUser user = brUserManager.getUser(tokenDto.getUserId());

        if (user == null)
            throw new AuthenticationException("用户名或密码错误");
        TokenDto tokenDtoNew = new TokenDto();
        tokenDtoNew.setUserName(user.getUsername());
        if (!JWTUtil.verify(token, tokenDtoNew,user.getPassword()))
            throw new AuthenticationException("token校验不通过");
        return new SimpleAuthenticationInfo(token, token, "shiro_realm");
    }
}

2.4 ShiroConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 设置 securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 在 Shiro过滤器链上加入 JWTFilter
        LinkedHashMap<String, Filter> filters = new LinkedHashMap<>();
        filters.put("jwt", new JWTFilter());
        shiroFilterFactoryBean.setFilters(filters);

        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 所有请求都要经过 jwt过滤器
        filterChainDefinitionMap.put("/**", "jwt");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 配置 SecurityManager,并注入 shiroRealm
        securityManager.setRealm(shiroRealm());
        return securityManager;
    }

    @Bean
    public ShiroRealm shiroRealm() {
        // 配置 Realm
        return new ShiroRealm();
    }

    //开启注解
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}