app端或者前后端分离的项目,保持用户登录认证最普遍的方法是token认证。

后端一般使用JWT进行token发放和认证,前端登录时拿到token,并在每次请求时都带上token,后端收到请求拿到token就可以认证用户信息和用户登录有效时间。

前台后台都需要设置拦截器。前台封装request方法,自动填入token和解析认证情况,后台根据配置判断token是否能通过认证

关于过期或者失效问题:我觉得应该让后台来控制认证问题,如果认证过期,返回过期即可;如果是前台主动下线(清除token),由于无状态连接的特性,无法让后台也主动下线,只需要后台判断请求token传空时返回前台让它跳转登录页面即可。

一.uniapp端

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

 

 

 

 

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐