三、项目设计
4.6 前端权限控制
之前之所以不需要经过登录认证就可以进入到dashboard界面,是因为模板中自带了mock模拟了登录数据,所以不需要登录。
//获取用户信息
Mock.mock(/\/frame\/profile/, 'post', ProfileAPI.profile)
Mock.mock(/\/frame\/login/, 'post', LoginAPI.login)
实现目标:
-
通过判断角色,进行前端页面的侧边栏是否可视的判断
-
通过点击按钮,进行判断角色对应后端的API是否可以调用的判断
-
通过判断角色,进行按钮对应的菜单是否可见的判断
实现思路:
在Vue路由中,只需要根据用户的权限信息动态的加载路由列表就可以动态的构造出访问菜单
- 登录成功获取用户信息,包括权限相关,角色相关
GetUserInfo({ commit, state }) {
return new Promise((resolve, reject) => {
profile().then(response => {
const data = response.data.data
commit('SET_ROLES', data.roles?data.roles:{menus:[],points:[]})
commit('SET_NAME', data.username)
commit('SET_AVATAR', data.avatar)
commit('SET_INTRODUCTION', data.company)
commit('SET_APPROVALS', data.approvals)
resolve(response)
}).catch(error => {
reject(error)
})
})
},
- 根据用户菜单权限列表,动态构造路由(根据路由名称和权限表示比较)
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
//动态构造权限列表
let accessedRouters = filterAsyncRouter(asyncRouterMap, roles)
commit('SET_ROUTERS', accessedRouters)
// commit('SET_ROUTERS', asyncRouterMap) // 调试开启全部路由
resolve()
})
}
}
}
- 页面按钮权限通过自定义方法控制可见性
import store from '@/store'
// 检查是否有权限
export function hasPermission(roles, route) {
if (roles.menus && route.name) {
return roles.menus.some(role => {
return route.name.toLowerCase() === role.toLowerCase()
})
} else {
return false
}
}
// 检查是否有权限点
export function hasPermissionPoint(point) {
let points = store.getters.roles.points
if (points) {
return points.some(it => it.toLowerCase() === point.toLowerCase())
} else {
return false
}
}
4.7 有状态服务和无状态服务
有状态服务和无状态服务是两种不同的服务架构,两者的不同之处在于服务状态的处理。服务状态是服务请求所需的数据,他可以是一个变量或者是一个数据结构。
无状态不会记录服务状态,并且不同请求之间毫不相干;
有状态服务则反之。
对于服务器来说,究竟是有状态服务还是无状态服务,判断依据就是两个来自相同发起者的请求在Server端是否具备上下文关系。
4.7.1 无状态服务
简单来说,无状态服务就是我们之前用到的JWT,Server端不去存储客户端的数据,每一条登录请求互不相干,只是由服务端产生token作为用户访问的有期限的令牌。
4.7.2 有状态服务
简单来说,与无状态服务相反,服务会存储上下文之间的关系,先后的请求是可以有关联的。
通常使用的就是Session来保存用户的登录信息,将本来是无状态的http转化为有状态服务。
4.8 基于JWT的API鉴权
我们之前只是对Server发送请求获得token,并且也实现了解析token获取用户信息的操作,但是如果每一次都去解析token就会容易依靠token进行网站的攻击,所以我们就需要使用拦截器对于请求进行一定的判断。并且通过这种方法,将token的产生算法写在一起,减少代码量。
我们首先将拦截器作为我们的配置类写出来,但是也留出允许访问的请求。
package com.lms.system;
import com.lms.common.interceptor.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
@Configuration
public class SystemConfig extends WebMvcConfigurationSupport {
@Autowired
private JwtInterceptor jwtInterceptor;
/**
* 添加拦截器的配置
*/
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)//1.添加自定义拦截器
.addPathPatterns("/**")//2.指定拦截器的url
.excludePathPatterns("/sys/login", "/frame/register/**");//3.指定不拦截的url
}
}
然后通过重写HandlerInterceptorAdapter的方法,在拦截的时候就产生token,并且配置到容器中
package com.lms.common.interceptor;
import com.lms.common.entity.ResultCode;
import com.lms.common.exception.CommonException;
import com.lms.common.utils.JWTUtils;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class JwtInterceptor extends HandlerInterceptorAdapter {
@Autowired
private JWTUtils jwtUtils;
@Override//在请求响应之前就执行,返回值为boolean,意思是true为继续执行,false就不执行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/**
* 通过拦截器获取到token,来简化之前的代码,并且判断用户是否登录
*/
// 通过request获取到token
String authorization = request.getHeader("Authorization");
//判断请求头信息是否为空,或者是否以Bearer开头
if (!StringUtils.isEmpty(authorization) && authorization.startsWith("Bearer")) {
//获取token数据
String token = authorization.replace("Bearer ", "");
//解析token获取claims
Claims claims = jwtUtils.parseJwt(token);
if (claims != null) {
request.setAttribute("user_claims", claims);
return true;
}
}
throw new CommonException(ResultCode.UNAUTHENTICATED);
}
// @Override//在请求响应之后执行
// public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// super.postHandle(request, response, handler, modelAndView);
// }
//
// @Override//在所有请求结束后执行
// public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// super.afterCompletion(request, response, handler, ex);
// }
}
于是我们再用postman的时候,在不绑定请求头进行登录申请就会提示没有登录。
在这里个人记录一个奇怪的事情,之前运行程序,注入jwtUtils的时候说缺少Bean的注解,无法加入容器,结果胡改一通之后,加了一个@ComponentScan,结果登录之后就变成404了,消除这个注解之后就回复了正常,明明是检测组件的,不知为何连控制层都受到了影响。
好了,重新发送登录请求:
正常登录:
之前有正常解析数据的图,就不放了,下面说的是不加入请求头的解析登录数据:
我们可以看见是未登录的。所以拦截器也就生效了,而拦截器里的token解析也生效了。
4.9 Shiro安全框架
Apache Shiro是一个强大且简单的java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,可以快速、轻松地获得任何应用程序,从最小的移动应用到web EE级别应用程序
Shiro的主要目标是易于理解和使用,安全有时候是很复杂的,但是完全没有必要这样,框架要掩盖复杂的地方,露出干净的API,简化开发的难度。
下面是Shiro可以做大的事情
- 验证用户来核实身份
- 对用户执行访问控制,
如:判断用户是否被分类了一个确定的安全角色
判断用户是否被允许做某事 - 在任何时候使用Session API
- 在身份验证,访问控制期间或者在会话的生命周期对事件作出反应
- 聚集一个或者多个用户安全数据的数据源,并且作为一个单一的符合用户“视图”
- 单点登录SSO
- 为没有关联到登录的用户启用 remember me 服务
shiro的内部结构
- Authentication:身份认证/登录,验证用户是不是拥有响应的身份。
- Authorization:授权,验证某个已认证的用户是否拥有某个权限,判断用户是否能做事情。
- Session Management:会话管理,用户登录后就是一次会话,在没有退出之前,所有信息都会在会话中,会话可以是SE也可以是EE
- Cryptography:数据加密
- Web Support:支持Web开发
- Caching:缓存,比如用户登录之后,不需要再次查阅自己的角色和权限
- Concurrency:支持并发
- Testing:支持测试,保证安全
- “Run As”:管理脚本
- Remember Me:记住我
Shiro的测试
首先配置ini文件
[users]
#模拟从数据库查询用户
#数据格式 用户名=密码
zhangsan=123456
lisi=654321
然后书写单元测试
package cn.itcast.shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.apache.shiro.mgt.SecurityManager;
import org.junit.Test;
public class ShiroTest01 {
/**
* 测试用户认证:
* 认证:用户登录
* 1.根据配置文件创建出SecurityManagerFactory
* 2.通过工厂获取SecurityManager
* 3.将SecurityManager绑定到当前运行环境
* 4.从当前运行环境中构造subject
* 5.构造shiro登录的数据
* 6.主体登录
*/
@Test
public void testLogin() {
// 1.根据配置文件创建出SecurityManagerFactory
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-test.ini");
// 2.通过工厂获取SecurityManager
SecurityManager securityManager = factory.getInstance();
// 3.将SecurityManager绑定到当前运行环境
SecurityUtils.setSecurityManager(securityManager);
// 4.从当前运行环境中构造subject
Subject subject = SecurityUtils.getSubject();
// 5.构造shiro登录的数据
String username = "zhangsan";
String password = "123456";
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
// 6.主体登录
subject.login(token);
// 7.验证用户是否登录成功
System.out.println("用户登录="+subject.isAuthenticated());
// 8.获取登录成功的数据
System.out.println(subject.getPrincipal());
}
}