注意:登錄是在白名單(直接放行的接口)。生成的token攜帶賬號信息。
1.用戶每次登錄生成token時,將賬號當成key,token當成value,以token的過期時間存入redis中。
2.用戶訪問的時候,在攔截器解析token,獲取賬號,拿賬號去redis中獲取value,如果是value的token與當前用戶攜帶過來的token一致就放行。如果不一致,則告訴前端重復登錄,讓前端清除token,跳轉到登錄頁面。
3.用戶在另一台設備登錄時,也是存入redis,這樣就刷新了token的值和redis的過期時間。
這是我的一個實現思路,有沒有大佬可以指點,互相學習一下。
寫了個小demo,主要代碼如下:
1.攔截器代碼:
package com.yblue.config; import com.yblue.dto.Payload; import com.yblue.dto.UserInfo; import net.minidev.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.http.HttpMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.List; /** * @author: JiaXinMa * @description: 解決跨域和權限 * @date: 2021/3/26 */ public class AuthInterceptor implements HandlerInterceptor { @Autowired FilterProperties filterProperties; @Autowired JwtProperties jwtProps; @Autowired StringRedisTemplate redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //表示接受任意域名的請求,也可以指定域名 response.setHeader("Access-Control-Allow-Origin", request.getHeader("origin")); //該字段可選,是個布爾值,表示是否可以攜帶cookie response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS"); response.setHeader("Access-Control-Allow-Headers", "*"); //預請求,這里可以不加,但是其他語言開發的話記得處理options請求 // 如:異步請求的時候前端發兩次請求,其中一次是測試通不通 if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) { return true; } //獲取當前訪問的URL接口后的路徑 String uri = request.getRequestURI(); //如:/api/user/xxx //判斷uri地址,如果為 /error 則可能是請求方式、參數、訪問路徑不對 if (uri.equals("/error")) { this.responseError(response, 405, "請求方式、參數類型、訪問地址錯誤、數據庫方面異常!"); return false; } //1.判斷當前訪問路徑是否在白名單列表中 List<String> allowPaths = filterProperties.getAllowPaths(); for (String allowPath : allowPaths) { if (uri.contains(allowPath)) { //如果在,直接放行了 return true; } } //2.校驗token合法性 Payload<UserInfo> payload = null; try { String token = request.getHeader(jwtProps.getCookie().getCookieName()); // String token = CookieUtils.getCookieValue(request, jwtProps.getCookie().getCookieName()); payload = JwtUtils.getInfoFromToken(token, jwtProps.getPublicKey(), UserInfo.class); String redisToken = redisTemplate.opsForValue().get(payload.getInfo().getUsername()); if (token.equals(redisToken)) { return true; } else { this.responseError(response, 401, "您的賬號在另一台設備上登錄,請及時修改密碼!"); } } catch (Exception e) { this.responseError(response, 401, "登錄失效或未登錄!"); return false; } //3.如果你的想校驗權限、角色,可以在你的userInfo里封裝,然后在下面驗證,不過我現在的userInfo沒放 // UserInfo userInfo = payload.getInfo(); // List<Module> modules = userInfo.getModules(); // for (Module module : modules) { // if (uri.equals(module.getUrl())) {//判斷用戶是否有訪問路徑的權限 // return true; // } else { // this.responseError(response, 403, "權限不足!"); // } // } return false; } /** * @author: JiaXinMa * @description: 響應錯誤代碼 處理響應錯誤信息的方法,可以拿去用 * @date: 2021/8/23 */ public void responseError(HttpServletResponse response, Integer code, String returnMessage) { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); JSONObject jsonObject = new JSONObject(); try { jsonObject.put("code", code); jsonObject.put("msg", returnMessage); response.getWriter().append(jsonObject.toString()); } catch (Exception e) { e.printStackTrace(); } } @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 { } }
2.業務層代碼:
package com.yblue.service; import com.yblue.config.JwtProperties; import com.yblue.config.JwtUtils; import com.yblue.domain.User; import com.yblue.dto.Payload; import com.yblue.dto.UserInfo; import com.yblue.exception.pojo.ExceptionEnum; import com.yblue.exception.pojo.MdException; import org.joda.time.DateTime; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; /** * @author: JiaXinMa * @description: 驗證授權 Service層 * @date: 2021/3/25 */ @Service @Transactional public class AuthService { @Autowired UserService userService; @Autowired private JwtProperties jwtProps; @Autowired private StringRedisTemplate redisTemplate; /** * @author: JiaXinMa * @description: 登錄 * @date: 2021/5/28 */ public Map login(String username, String password) { try { Map<String, Object> map = new HashMap();//存放token和用戶信息 //1. 判斷用戶名和密碼是否正確 User loginUser = userService.findUserByNameAndPwd(username, password); UserInfo userInfo = new UserInfo(loginUser.getUserId(), loginUser.getName(), loginUser.getUsername()); //2.生成token String token = this.generateToken(userInfo); map.put(jwtProps.getCookie().getCookieName(), token); map.put("userInfo", userInfo); //3.將token放進redis中 redisTemplate.opsForValue().set(loginUser.getUsername(), token, 60, TimeUnit.MINUTES); return map; } catch (Exception e) { throw new MdException(ExceptionEnum.INVALID_USERNAME_PASSWORD); } } /** * @author: JiaXinMa * @description: 生成token * @date: 2021/4/6 */ public String generateToken(UserInfo info) { //利用JwtUtils+私鑰生成加密token //以前是的工具類是通過cookies帶過去了,然后現在他們說要放在請求頭上,所以那些類名字沒改,不妨礙業務的使用 return JwtUtils.generateTokenExpireInMinutes(info, jwtProps.getPrivateKey(), jwtProps.getCookie().getExpire()); } }
3.控制層代碼
package com.yblue.controller; import com.yblue.domain.User; import com.yblue.service.AuthService; import com.yblue.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.Map; /** * @author: JiaXinMa * @description: 驗證 控制器 * @date: 2021/5/28 */ @Slf4j @RestController @RequestMapping("/api") public class AuthController { @Autowired AuthService authService; @Autowired UserService userService; @PostMapping("/auth/login") public Map login(@RequestBody User user) { log.info("/api/auth/login:{}", user); return authService.login(user.getUsername(), user.getPassword()); } @GetMapping("/user/findByUserId") public User findByUserId(@RequestParam("userId") Integer userId) { log.info("/api/user/findByUserId:{}", userId); return userService.findByUserId(userId); } }
效果如下圖:
第一次:
第二次:
如果有些小伙伴有更好的解決方案或者覺得該方案有什么不足的,可以提出來一起討論交流。
想看更多精彩內容,可以關注我的CSDN