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