前言
角色菜单可以控制侧边栏的显示,再细一点的粒度就是权限了,可以控制页面或接口是否可以访问。
实现
配置
ShiroConfig.java
开启权限注解切点扫描
@Configuration
public class ShiroConfig {
/**
* 注入ShiroRealm,自定义的realm 后面的认证和授权全在这里编写
* @return
*/
@Bean
public ShiroRealm shiroRealm() {
return new ShiroRealm();
}
/**
* 创建SecurityManager
* @return
*/
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(ShiroRealm shiroRealm) {
/**
* securityManager对象,shiroRealm进行托管
*/
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置支持的AuthenticationToken
shiroRealm.setAuthenticationTokenClass(BearerToken.class);
securityManager.setRealm(shiroRealm);
/**
* 禁用session
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
/**
* 过滤器配置
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
factoryBean.setLoginUrl("/login");
// 添加自定义过滤器
Map<String, Filter> filterMap = new HashMap<>(16);
filterMap.put("tokenFilter", new BearerTokenFilter());
factoryBean.setFilters(filterMap);
/**
* 自定义拦截规则
*/
Map<String, String> filterRuleMap = new HashMap<>(16);
// 对swagger相关url请求不进行拦截
filterRuleMap.put("/swagger-ui.html", "anon");
filterRuleMap.put("/doc.html/**", "anon");
filterRuleMap.put("/swagger-resources/**", "anon");
filterRuleMap.put("/v2/**", "anon");
filterRuleMap.put("/webjars/**", "anon");
filterRuleMap.put("/favicon.ico", "anon");
// 其余请求都要经过BearerTokenFilter自定义拦截器
filterRuleMap.put("/**", "tokenFilter");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
/**
* 开启shiro 权限相关注解的切点
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
- 接口添加权限注解
@RequiresPermissions()
@RequiresPermissions("sys:role:view")
@GetMapping(value = "roles/list")
@ApiOperation(value = "角色列表", notes = "接口描述")
public ResResult listRole(){
return this.roleService.listRole();
}
ShiroRealm
返回当前用户权限
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private MenuService menuService;
@Autowired
private RedisUtil redisUtil;
/**
* 授权方法
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
String username = JwtUtil.getUsername(UserUtil.getTokenWithoutPrefix(principalCollection.toString()));
User user = this.userService.getOne(new QueryWrapper<User>().eq("username", username));
if (user != null) {
// 获取当前用户拥有的权限
List<String> perms = this.menuService.findPremByUser(user.getId());
for (String perm : perms) {
authorizationInfo.addStringPermission(perm);
}
}
return authorizationInfo;
}
/**
* 认证方法
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 获取身份信息
BearerToken bearerToken = (BearerToken) authenticationToken;
// 去掉token前缀
String token = bearerToken.getToken();
if (StrUtil.isEmpty(token)) {
throw new AuthenticationException("token is empty");
}
token = token.replace(Constant.TOKEN_PREFIX, "").trim();
// jwt解析token 获取用户名
String username = JwtUtil.getUsername(token);
if (StrUtil.isEmpty(username)) {
throw new AuthenticationException("token invalid");
}
// 查询数据库用户是否存在
QueryWrapper queryWrapper = new QueryWrapper<User>();
queryWrapper.eq("username", username);
User user = userService.getOne(queryWrapper);
if (user == null) {
throw new AuthenticationException("User didn't existed!");
}
//验证token是否合法
if (!JwtUtil.verify(token, username, user.getPassword())) {
throw new AuthenticationException("Username or password error");
}
// 验证token 是否过期
String cacheToken = (String) redisUtil.get(Constant.LOGIN_PREFIX + user.getId());
if (StrUtil.isEmpty(cacheToken) || !StrUtil.equals(cacheToken, token)) {
throw new AuthenticationException("cacheToken is empty or token incorrect");
}
//验证通过刷新token 时间
redisUtil.expire(Constant.LOGIN_PREFIX + user.getId(), Constant.TOKEN_EXPIRE);
return new SimpleAuthenticationInfo(bearerToken.getToken(), bearerToken.getToken(), "shiroRealm");
}
}
- 全局异常捕获
@RequiresPermissions()
修饰的接口权限不够的话会抛出UnauthorizedException
异常,捕获一下返回403给前端,让前端处理
/**
* 功能描述: 异常统一返回, 统一返回500状态
*/
@ResponseStatus(HttpStatus.FORBIDDEN)
@ExceptionHandler(UnauthorizedException.class)
public ResResult handleUnauthorizedException(Exception e) {
log.error("权限异常:{}", e);
return ResResult.failure(ResResultCode.FORBIDDEN);
}
页面
静态路由新增403和404页面
export const constantRoutes = [
{
path: '/',
redirect: '/login',
hidden: true
},
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/errorPage',
name: 'errorPage',
component: Layout,
hidden: true,
children: [{
path: '/404',
component: () => import('@/views/404'),
},
{
path: '/403',
component: () => import('@/views/403'),
}]
}
]
request.js
全局响应处理403和404
import axios from 'axios'
import router from '../router'
import store from '@/store'
import { getToken } from '@/utils/auth'
import IMessage from './IMessage'
// create an axios instance
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
if (store.getters.token) {
// let each request carry token
// ['X-Token'] is a custom headers key
// please modify it according to the actual situation
config.headers['Authorization'] = getToken()
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/
/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/
response => {
const res = response.data
// if the custom code is not 200, it is judged as an error.
if (res.code !== 200) {
IMessage.error(res.msg || '网络通讯异常,请稍后再试!')
return Promise.reject(new Error(res.msg || 'Error'))
} else {
return res
}
},
error => {
if (error.response && error.response.status === 401) {
IMessage.error('登录失效,请重新登录')
router.replace({
path: '/login',
query: { redirect: router.currentRoute.fullPath }
})
}
else if (error.response && error.response.status === 403) {
router.replace({
path: '/403'
})
}
else {
IMessage.error('网络通讯异常,请稍后再试!')
}
return Promise.reject(error)
}
)
export default service
效果
- 403访问权限不足
- 404访问路由不存在