在前后端分离开发中,用户登陆成功后一般会生成token,在前后端进行携带验证。使用jwt加密的方式,token将会被前端放置在请求头中(当然作为请求参数传递也是允许的,看前端开发者的心情。),后端通过request.getHeader("token")来获取到token并进行验证。在需要用户登录后才能访问的接口上加入自定义的注解,当用户发起请求会被拦截器拦截进行验证,验证通过则放行,验证失败则返回相应的信息给前台,提示用户先进行登录才允许访问。类似于sso单点登录的校验。最近的APP开发中刚好有这样的需求,特意做一个总结【高手可以直接忽略,不喜勿喷。同时也欢迎广大读者朋友提出你们的意见】
1.maven依赖【使用jwt加密需要导入的依赖】
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.41</version>
</dependency>
2.工具类
public class JwtUtil {
public static String encode(String key, Map<String, Object> param, String salt) {
if (salt != null) {
key += salt;
}
JwtBuilder jwtBuilder = Jwts.builder().signWith(SignatureAlgorithm.HS256, key);
jwtBuilder = jwtBuilder.setClaims(param);
String token = jwtBuilder.compact();
return token;
}
public static Map<String, Object> decode(String token, String key, String salt) {
Claims claims = null;
if (salt != null) {
key += salt;
}
try {
claims = Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
} catch (JwtException e) {
return null;
}
return claims;
}
}
HttpclientUtil 模拟浏览器发送请求的工具类
import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class HttpclientUtil {
public static String doGet(String url) {
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.createDefault();
// 创建http GET请求
HttpGet httpGet = new HttpGet(url);
CloseableHttpResponse response = null;
try {
// 执行请求
response = httpclient.execute(httpGet);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
HttpEntity entity = response.getEntity();
String result = EntityUtils.toString(entity, "UTF-8");
EntityUtils.consume(entity);
httpclient.close();
return result;
}
httpclient.close();
} catch (IOException e) {
e.printStackTrace();
return null;
}
return null;
}
public static String doPost(String url, Map<String, String> paramMap) {
// 创建Httpclient对象
CloseableHttpClient httpclient = HttpClients.createDefault();
// 创建http Post请求
HttpPost httpPost = new HttpPost(url);
CloseableHttpResponse response = null;
try {
List<BasicNameValuePair> list = new ArrayList<>();
for (Map.Entry<String, String> entry : paramMap.entrySet()) {
list.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
HttpEntity httpEntity = new UrlEncodedFormEntity(list, "utf-8");
httpPost.setEntity(httpEntity);
// 执行请求
response = httpclient.execute(httpPost);
// 判断返回状态是否为200
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
HttpEntity entity = response.getEntity();
String result = EntityUtils.toString(entity, "UTF-8");
EntityUtils.consume(entity);
httpclient.close();
return result;
}
httpclient.close();
} catch (IOException e) {
e.printStackTrace();
return null;
}
return null;
}
}
3.在用户登录成功后,使用jwt进行加密并传值到前台
用户登录验证部分省略,只展示使用jwt加密,并把信息存储在redis中的部分。
ResultMsg 是封装好的用于前后台传值的实体类
ResultMsg msg = new ResultMsg();
Map<String, Object> param =new HashMap<>();
param.put("uuid", user.getUuid());
param.put("phone", user.getPhone());
String salt = request.getRemoteAddr();
String encode = JwtUtil.encode("2020ms", param , salt);
HashMap<String,Object> map = new HashMap<String,Object>();
map.put("user", user);
//========暂时把用户的登录有效期设置为一天====修改====
redisUtil.hmset(phone, map, 1*24*60*60);
msg.setCode(200);
msg.setPublicStr(phone);
msg.setMsg("用户登陆成功!!!");
return msg;
4.验证token是否有效的方法
@RequestMapping("/verify")
@ResponseBody
public String verify(String token, String currentIp, HttpServletRequest request) {
System.out.println("开始校验token数据.........");
// 通过jwt校验token真假
Map<String, Object> map = new HashMap<>();
Map<String, Object> decode = JwtUtil.decode(token, "2020ms", currentIp);
try {
if (decode != null) {
Object uuid = decode.get("uuid");
Object phone = decode.get("phone");
System.out.println("检验phone="+phone);
System.out.println("检验uuid="+uuid);
Map<Object, Object> hmget = redisUtil.hmget(token);
//通过token取到redis中存储的数据
if(null != hmget && hmget.size()>0) {
User redis_user = (User) hmget.get("user");
//if(null != redis_user) {
map.put("status", "success");
System.out.println("校验成功.........");
System.err.println("token校验时的redis_user:"+redis_user);
// }else {
// map.put("status", "fail");
// System.out.println("token校验失败.........");
// }
}else {
map.put("status", "fail");
System.out.println("token校验失败.........");
}
} else {
map.put("status", "fail");
System.out.println("校验失败.........");
}
}catch (NullPointerException e){
map.put("status", "fail");
System.out.println("校验失败.........");
}
return JSON.toJSONString(map);
}
5.编写拦截器
@Component
public class AuthInterceptor extends HandlerInterceptorAdapter {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// registry.addResourceHandler("/upload/**").addResourceLocations("file:C:/work/temp/temp0/temp1/upload/");
//如果请求为静态资源请求时,类型转换会报错,类型不对应,所以应再请求时方法请求时再转换类型
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
// 拦截代码
// 判断被拦截的请求的访问的方法的注解(是否时需要拦截的)
HandlerMethod hm = (HandlerMethod) handler;
LoginRequired methodAnnotation = hm.getMethodAnnotation(LoginRequired.class);
StringBuffer url = request.getRequestURL();
System.out.println("url="+url);
// 是否拦截
if (methodAnnotation == null) {
return true;//没有注解的都放行
}
String token = request.getHeader("token");
System.out.println("拦截器前台传的token= "+token);
// 是否必须登录
boolean loginSuccess = methodAnnotation.loginSuccess();// 获得该请求是否必登录成功
System.out.println("该请求是否必须登录成功"+loginSuccess);
// 调用认证中心进行验证
String success = "fail";
Map<String,String> successMap = new HashMap<>();
if(StringUtils.isNotBlank(token)){//判断token是否为空 是的话不走该方法
// String ip = request.getHeader("x-forwarded-for");// 通过nginx转发的客户端ip
// if(StringUtils.isBlank(ip)){
// ip = request.getRemoteAddr();// 从request中获取ip
// if(StringUtils.isBlank(ip)){
// ip = "127.0.0.1";
// }
// }
// String ip ="127.0.0.1";
String ip =request.getRemoteAddr();
System.err.println("当前访问的用户IP为:"+ip);
String successJson = HttpclientUtil.doGet("http://127.0.0.1:88/verify?token=" + token+"¤tIp="+ip);
successMap = JSON.parseObject(successJson,Map.class);
success = successMap.get("status");
System.out.println("返回的校验后的状态为:success= "+success);
}
if (loginSuccess) {
// 必须登录成功才能使用
if (!success.equals("success")) {
System.out.println("IF判断里success= "+success);
//重定向会passport登录
StringBuffer requestURL = request.getRequestURL();
//tologin为自定义的方法,让用户去登录
response.sendRedirect("http://127.0.0.1:88/tologin");
return false;
}
// // 需要将token携带的用户信息写入
// request.setAttribute("memberId", successMap.get("memberId"));
// request.setAttribute("nickname", successMap.get("nickname"));
}
// else {
// 没有登录也能用,但是必须验证
// if (success.equals("success")) {
// // 需要将token携带的用户信息写入
// request.setAttribute("memberId", successMap.get("memberId"));
// request.setAttribute("nickname", successMap.get("nickname"));
//
//
//
// }
// }
return true;
}
}
6.自定义注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)//在方法范围内生效
@Retention(RetentionPolicy.RUNTIME)//在虚拟机范围内也生效
public @interface LoginRequired {
//通过注解的方式来标识具体的方法是否需要通过拦截器
boolean loginSuccess() default true;
}
7.配置拦截器的拦截路径,在springboot中它没有web.xml而我们刚刚自定义的拦截器相当于声明了一个组件,也可以理解为写了一个拦截方法,需要配置拦截路径方能生效,【本人层踩过这个坑,忽略了对拦截器的配置导致拦截器不生效。。。】
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.pf.bindDate.web.interceptor.AuthInterceptor;
@ControllerAdvice//切面类
@Configuration
/**
* 自定义资源映射
*通过addResourceHandler添加映射路径,然后通过addResourceLocations来指定路径。
*/
public class WebConfig extends WebMvcConfigurerAdapter {
@Autowired
AuthInterceptor authInterceptor;
@Value("${file-save-path}")
private String fileSavePath;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//指定拦截路径,类似于web.xml中的配置
registry.addInterceptor(authInterceptor).addPathPatterns("/**")
.excludePathPatterns("/error")
.excludePathPatterns("/pf_dd/uploads/**");
super.addInterceptors(registry);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
/**
* 配置资源映射
* 意思是:如果访问的资源路径是以“/images/”开头的,
* 就给我映射到本机的“E:/images/”这个文件夹内,去找你要的资源
* 注意:E:/images/ 后面的 “/”一定要带上
*/
registry.addResourceHandler("/pf_dd/uploads/**").addResourceLocations("file:"+fileSavePath);
registry.addResourceHandler("/statics/**").addResourceLocations("classpath:/statics/");
// 解决 SWAGGER 404报错
registry.addResourceHandler("/swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
}
}
8.接下来就可以在Controller中需要拦截的方法上面加@LoginRequired注解,达到我们对必须登录才能访问的接口的校验。
同样的需求可以有多种解决办法,我们要做的就是尽量写出安全可靠高效的程序。革命尚未成功,同志仍需努力。加油!!!