app端或者前后端分离的项目,保持用户登录认证最普遍的方法是token认证。
后端一般使用JWT进行token发放和认证,前端登录时拿到token,并在每次请求时都带上token,后端收到请求拿到token就可以认证用户信息和用户登录有效时间。
前台后台都需要设置拦截器。前台封装request方法,自动填入token和解析认证情况,后台根据配置判断token是否能通过认证
关于过期或者失效问题:我觉得应该让后台来控制认证问题,如果认证过期,返回过期即可;如果是前台主动下线(清除token),由于无状态连接的特性,无法让后台也主动下线,只需要后台判断请求token传空时返回前台让它跳转登录页面即可。
1.登录获取token并保存
登录还用普通的uni.request或者下面封装的都行,后端返回的userInfo对象中有token,更新入vuex和缓存中:
var self=this; uni.request({ url: self.baseUrl + '/login/login', method: "POST", data: { phoneNo: self.phoneNo, loginPwd:self.loginPwd }, header: { 'content-type': 'application/x-www-form-urlencoded' //自定义请求头信息 }, success: (res) => { // 获取真实数据之前,务必判断状态是否为200 if (res.data.success) { userInfo=res.data.data; //登录成功后 更新登录状态 self.$store.commit("login", res.data.data); uni.navigateBack(); }else{ uni.showToast({ icon: 'none', title: '登录失败'+res.data.msg, duration: 1500 }); } } });2.vuex中缓存
login(state, provider) { state.isLogin = true; state.userInfo = provider; state.token=provider.token; //自己平台的用户基础信息 uni.setStorageSync('userInfo', JSON.stringify(provider)) //这里不能用stringify处理字符串,不然会多出双引号 uni.setStorageSync('token', provider.token) //alert(provider.token); },3.封装uni.request
config.js
export default{ baseUrl:"http://localhost:8087/RecordLife/", //自己架设的返回oss签名的服务 ossServer:"http://localhost:9089" }tokenRequest.js
这里封装了一个普通方法和传token的,实际中只用传token的就行,就算首页不需要登录即可访问的接口token自动传空
在封装方法中,会判断返回结果状态码,如果返回token认证失败相关状态码,直接跳登录界面就行了
import config from '../resources/config.js' // 定义基础请求路径(后端服务器地址) const baseRequest = (opts, data) => { let method='post' if(opts.method){ method=opts.method } let baseDefaultOpts = { url: config.baseUrl+opts.url, // 请求接口地址 data: data, // 传入请求参数 method: method, // 配置请求类型 header: method.toLowerCase() == 'get' ? {'X-Requested-With': 'XMLHttpRequest',"Accept": "application/json","Content-Type": "application/json; charset=UTF-8"} : {'X-Requested-With': 'XMLHttpRequest','Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}, // 配置请求头 dataType: 'json', } let promise = new Promise(function(resolve, reject) { uni.request(baseDefaultOpts).then( (res) => { if(res[1].data.code == '0' || res[1].data.code == 0){ // 后端返回的状态码100为成功状态,成功则返回请求结果,在app调试时可以通过console.log(JSON.stringify(res[1].data))来查看返回值(以项目实际情况为准) resolve(res[1].data) } if(res[1].data.code == '-1' || res[1].data.state == "-10"){ // 后端返回状态码为105则为未登录状态(以项目实际情况为准) uni.showToast({ icon:'none', title: res[1].data.msg, duration: 2000 }); // 尚未登录的逻辑处理 return false } } ).catch( (response) => { reject(response) } ) }) return promise }; //带Token请求 const TokenRequest = (opts, data) => { //此token是登录成功后后台返回保存在storage中的 let token = ""; if(uni.getStorageSync('token')){ token = uni.getStorageSync('token'); } //设置默认请求方式 let method='post' if(opts.method){ method=opts.method } let promise; //配置一下请求参数 let DefaultOpts = { url: config.baseUrl+opts.url, data: data, method: method, header: method.toLowerCase() == 'get' ? {'token': token,'X-Requested-With': 'XMLHttpRequest',"Accept": "application/json","Content-Type": "application/json; charset=UTF-8"} : {'token': token,'X-Requested-With': 'XMLHttpRequest','Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'}, dataType: 'json', } //if(token){ //我这里前台不验证token了, 放在后端验证,后台会对需要验证的接口进行配置,因为不是每个接口都要token认证的 promise = new Promise(function(resolve, reject) { uni.request(DefaultOpts).then( (res) => { console.log(JSON.stringify(res[1].data)) if(res[1].data.code == '0' || res[1].data.code == 0){ // 后端返回的状态码100为成功状态,成功则返回请求结果,在app调试时可以通过console.log(JSON.stringify(res[1].data))来查看返回值(以项目实际情况为准) resolve(res[1].data) } if(res[1].data.code == '-1' || res[1].data.state == "-10"){ // 后端返回状态码为105则为未登录状态(以项目实际情况为准) uni.showToast({ icon:'none', title: res[1].data.msg, duration: 2000 }); // 尚未登录的逻辑处理 return false } if(res[1].data.code == '-100'){ // 后端返回状态码为105则为未登录状态(以项目实际情况为准) uni.showToast({ icon:'none', title: '尚未登录,请重新登录', duration: 2000 }); setTimeout(function(){ uni.navigateTo({ url: '../login/login' }) },2000) } } ).catch( (response) => { reject(response) } ) }) /* }else{ uni.showToast({ title: '尚未登录,请重新登录', duration: 2000 }); setTimeout(function(){ uni.navigateTo({ url: '../login/login' }) },2000) } */ return promise } // 将对象导出外部引入使用 export default { baseRequest, TokenRequest }4.调用接口时使用封装后的
建议直接在main.js中放入全局变量中
main.js
import request from './common/tokenRequest.js'; //自己封装的request,可以自己注入并验证token Vue.prototype.$request=request调用 封装后的就比较简洁了:
var self = this; var data={ "userId": self.userInfo.userId, "nickname": nickname } self.$request.TokenRequest({url:"system/user/updateUser"}, data).then(res => { //console.log(JSON.stringify(res)); self.$store.commit("updateUser", data); uni.navigateBack({ delta: 1 }) //打印请求返回的数据 },error => {console.log(error);})二.后台 srpingboot+JWT
1.maven:
<!--token认证--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency>2.配置拦截器
这只是案例而已,springboot配置拦截器可单独查询
@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${server.servlet.context-path}") private String contextPath; @Value("${basePath}") private String basePath; //不注册这个在过滤器中 service将报空 @Bean public LoginInterceptor loginInterceptor(){ return new LoginInterceptor(); } //添加拦截器方法 @Override public void addInterceptors(InterceptorRegistry registry) { //添加拦截路径 String[] addPathPatters={ "/**" }; //添加不拦截路径 String[] excludePathPatters={ "/", "/login/login", "/login/loginPage","/login/register", "/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg", "/**/*.gif", "/**/fonts/*", "/**/*.svg", "/**/*.ttf","/**/*.woff","/**/*.eot","/**/*.otf","/**/*.woff2" }; //注册登录拦截器 registry.addInterceptor(loginInterceptor()).addPathPatterns(addPathPatters).excludePathPatterns(excludePathPatters); //如果多条拦截器则增加多条 } //添加放行静态资源 @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { //文件磁盘图片url 映射 //配置server虚拟路径,handler为前台访问的目录,locations为files相对应的本地路径 registry.addResourceHandler("/attachments/**").addResourceLocations("file:"+basePath+"attachments/"); //配置静态文件路径,与上面并不冲突 registry.addResourceHandler("/**").addResourceLocations("classpath:/META-INF/resources/"); } }3.新建两个注解 用于标识请求是否需要进行Token 验证
/*** * 用来跳过验证的 PassToken * @author MRC * @date 2019年4月4日 下午7:01:25 */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface PassToken { boolean required() default true; }/** * 用于登录后才能操作 * @author MRC * @date 2019年4月4日 下午7:02:00 */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface UserLoginToken { boolean required() default true; }
4.拦截器:
public class LoginInterceptor implements HandlerInterceptor { @Autowired private UserService userService; private String[] deviceArray = new String[]{"android","mac os","windows phone"}; //进入controller之前进入这个方法 @Override //这个方法是在访问接口之前执行的,我们只需要在这里写验证登陆状态的业务逻辑,就可以在用户调用指定接口之前验证登陆状态了 public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse response, Object object) throws Exception { String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token // 如果不是映射到方法直接通过 if(!(object instanceof HandlerMethod)){ return true; } HandlerMethod handlerMethod=(HandlerMethod)object; Method method=handlerMethod.getMethod(); String userAgent=httpServletRequest.getHeader("USER-AGENT").toLowerCase(); Boolean isMoileDevice=false; for(int i=0;i<deviceArray.length;i++){ if(userAgent.indexOf(deviceArray[i])>0){ isMoileDevice= true; } } //判断是pc端还是移动端请求,因为pc端涉及到一个过期返回登录页面的问题,移动端无法控制 if(isMoileDevice){ //检查是否有passtoken注释,有则跳过认证 if (method.isAnnotationPresent(PassToken.class)) { PassToken passToken = method.getAnnotation(PassToken.class); if (passToken.required()) { return true; } } //检查有没有需要用户权限的注解 if (method.isAnnotationPresent(UserLoginToken.class)) { UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class); if (userLoginToken.required()) { // 执行认证 if (token == null) { returnJson(response,"无token,请重新登录"); return false; //throw new RuntimeException("无token,请重新登录"); } // 获取 token 中的 user id String userId; try { userId = JWT.decode(token).getAudience().get(0); } catch (JWTDecodeException j) { //throw new RuntimeException("401"); returnJson(response,"获取用户认证发生错误"); return false; } SysUser user = userService.getUserById(userId); if (user == null) { returnJson(response,"用户不存在,请重新登录"); return false; //throw new RuntimeException("用户不存在,请重新登录"); } // 验证 token,因为我生成signature的时候加密的是用户密码,所以这里也需要用用户密码验证 JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getLoginPwd())).build(); try { jwtVerifier.verify(token); } catch (JWTVerificationException e) { e.printStackTrace(); returnJson(response,"认证失败,请重新登录"); return false; //throw new RuntimeException("401"); } return true; } } }else { HttpSession session = httpServletRequest.getSession(); //这里的User是登陆时放入session的 SysUser user = (SysUser) session.getAttribute("USER"); //如果session中没有user,表示没登陆 if (user == null){ //这个方法返回false表示忽略当前请求,如果一个用户调用了需要登陆才能使用的接口,如果他没有登陆这里会直接忽略掉 //当然你可以利用response给用户返回一些提示信息,告诉他没登陆 response.sendRedirect(httpServletRequest.getContextPath()+ "/login/loginPage"); return false; }else { return true; //如果session里有user,表示该用户已经登陆,放行,用户即可继续调用自己需要的接口 } } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } private void returnJson(HttpServletResponse response,String message){ PrintWriter writer = null; response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); try { writer = response.getWriter(); RetBase ret=new RetBase(); ret.setSuccess(false); ret.setCode("-100"); ret.setMsg(message); writer.print(JSON.toJSONString(ret, SerializerFeature.WriteMapNullValue)); } catch (IOException e){ //LoggerUtil.logError(ECInterceptor.class, "拦截器输出流异常"+e); } finally { if(writer != null){ writer.close(); } } } }5.拦截器配完了,现在需要在登录时调用生成token
登录方法:
@RequestMapping("login") @ResponseBody //密码登录 public Object login(ServletRequest req, HttpSession session,@RequestParam Map<String, Object> params){ RetBase ret = new RetBase(); List<SysUser> list = new ArrayList<>(); try { params.put("loginPwd", DigestUtils.md5DigestAsHex(params.get("loginPwd").toString().getBytes())); List<SysUser> list1=userService.getUserList(params); if(list1!=null && list1.size()>0){ list=loginService.login(params); }else{ this.register1(params); list=loginService.login(params); } if(list!=null && list.size()>0){ SysUser USER=list.get(0); String token = tokenService.getToken(USER); USER.setToken(token); //我这里是直接把token放进USER对象里面去了 ret.setData(USER); ret.setCode("0"); ret.setMsg("登录成功"); ret.setSuccess(true); }else{ ret.setCode("-1"); ret.setMsg("登录名或密码错误"); ret.setSuccess(false); } } catch (Exception e) { e.printStackTrace(); ret.setCode("-10"); ret.setMsg("登录失败"+e.getMessage()); ret.setSuccess(false); } return ret; }增加一个发放token的service:
@Service("TokenService") public class TokenService { public String getToken(SysUser user) { Date start = new Date(); Date end = new Date(start.getTime()+(long)30 *24 * 60* 60 * 1000);//30天有效时间 String token = ""; //这里使用用户密码做加密签名 token = JWT.create().withAudience(user.getUserId()).withIssuedAt(start).withExpiresAt(end) .sign(Algorithm.HMAC256(user.getLoginPwd())); return token; } }6.新建一个工具类从token中获取userId(选择使用,不用也没事)
public class TokenUtil { public static String getTokenUserId() { String token = getRequest().getHeader("token");// 从 http 请求头中取出 token String userId = JWT.decode(token).getAudience().get(0); return userId; } /** * 获取request * * @return */ public static HttpServletRequest getRequest() { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder .getRequestAttributes(); return requestAttributes == null ? null : requestAttributes.getRequest(); } }7.实际方法中调用
就是加一个注解完事,@userLoginToken,在拦截器判断是的有这个注解的才认证token,只需要将所有需要认证的方法加上这个注解就行了,不是每个方法都需要认证的,比如浏览新闻,商品列表啥的。
//更新用户 @UserLoginToken @ResponseBody @RequestMapping(value = "updateUser") public Object updateUser(@RequestParam Map<String, Object> params) { RetBase ret = new RetBase(); int count=0; try { count=userService.updateUser(params); if(count>0){ List<SysUser> list = userService.getUserList(params); if(list!=null && list.size()>0){ ret.setData(list.get(0)); } ret.setSuccess(true); ret.setCode("0"); ret.setMsg("修改成功"); }else{ ret.setSuccess(false); ret.setCode("-1"); ret.setMsg("修改失败"); } } catch (Exception e) { e.printStackTrace(); ret.setSuccess(false); ret.setCode("-10"); ret.setMsg("修改失败"); } return ret; }参数文献:https://blog.csdn.net/weixin_45532734/article/details/105137010
https://www.cnblogs.com/ChromeT/p/10932202.html