在前後端分離開發中,用戶登陸成功後一般會生成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註解,達到我們對必須登錄才能訪問的接口的校驗。
同樣的需求可以有多種解決辦法,我們要做的就是儘量寫出安全可靠高效的程序。革命尚未成功,同志仍需努力。加油!!!