SpringBoot+JWT實現(xiàn)注冊、登錄、狀態(tài)續(xù)簽流程分析
一、實現(xiàn)流程
1.注冊
2.登錄
3.登錄保持【狀態(tài)續(xù)簽】
二、實現(xiàn)方法
項目結(jié)構(gòu)
1.引入依賴
<!-- spring-web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- mysql連接器 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- Mybatis Plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3.4</version> </dependency> <!-- druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.8</version> </dependency> <!-- jwt --> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.10.3</version> </dependency>
2.application配置文件
spring: application: name: jwtLogin # 應(yīng)用名稱 datasource: druid: url: jdbc:mysql://192.168.0.111:3306/login_test?useSSL=false&serverTimezone=UTC username: samon password: 123456 driver-class-name: com.mysql.jdbc.Driver server: port: 8848 # 應(yīng)用服務(wù) WEB 訪問端口
3.mysql建表
用戶表
4.Bean
1.bean/user.java
用戶bean
package com.cxstar.bean; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data; import java.util.Date; @Data public class User { // 自增長id @TableId(type = IdType.AUTO) private Integer id; private String userName; private String passWord; private Date createTime; private Date lastLogin; public User() {} public User(Integer id, String userName) { this.id = id; this.userName = userName; } }
2.bean/ServiceRes.java
統(tǒng)一Service返回類
package com.cxstar.bean; import lombok.Data; @Data public class ServiceRes { private Integer code; private String msg; private String jwt; private ServiceRes() {} public ServiceRes(Integer code, String msg) { this.code = code; this.msg = msg; } public ServiceRes(Integer code, String msg, String jwt) { this.code = code; this.msg = msg; this.jwt = jwt; } }
3.bean/ControllerRes.java
統(tǒng)一Controller返回類
package com.cxstar.bean; import lombok.Data; @Data public class ControllerRes { private Integer code; private String msg; public ControllerRes(Integer code, String msg) { this.code = code; this.msg = msg; } }
5.Mapper mapper/UserMapper.java
繼承MP
package com.cxstar.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.cxstar.bean.User; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserMapper extends BaseMapper<User> { }
6.service service.userService.interface
登錄、注冊、改密業(yè)務(wù)【狀態(tài)續(xù)簽測試】
package com.cxstar.service; import com.cxstar.bean.ServiceRes; import com.cxstar.bean.User; public interface userService { // 注冊 ServiceRes register(User user); // 登錄 ServiceRes login(User user); // 改密【帶權(quán)限業(yè)務(wù),用于狀態(tài)續(xù)簽測試】 ServiceRes changePassWord(User user); }
service.impl.UserServiceImpl.java
登錄、注冊、改密業(yè)務(wù)【狀態(tài)續(xù)簽測試】
package com.cxstar.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.cxstar.bean.ServiceRes; import com.cxstar.bean.User; import com.cxstar.mapper.UserMapper; import com.cxstar.service.userService; import com.cxstar.utils.JwtUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.DigestUtils; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @Slf4j @Service public class UserServiceImpl implements userService { @Autowired UserMapper userMapper; /** * 注冊 * @param user 用戶類 * @return ServiceRes */ @Override public ServiceRes register(User user) { // 判斷用戶名是否唯一 if(this.checkUserNameIsUnique(user)) { // 判斷用戶名密碼是否合法 if(this.checkUserNameAndPassword(user)) { // 密碼MD5加密 user.setPassWord(this.MD5Code(user.getPassWord())); // 加入創(chuàng)建時間 user.setCreateTime(new Date()); // 入庫 userMapper.insert(user); return new ServiceRes(1, "注冊成功"); } else return new ServiceRes(-1, "用戶名或密碼不合法"); } else return new ServiceRes(-1, "用戶名已存在"); } /** * 登錄 * @param user 用戶類 * @return ServiceRes */ @Override public ServiceRes login(User user) { // 判斷用戶名密碼是否合法 if(this.checkUserNameAndPassword(user)) { // 密碼MD5加密 user.setPassWord(this.MD5Code(user.getPassWord())); // 檢查用戶是否存在 User curUser = this.checkUserIsExit(user); if(curUser!=null) { // 更新用戶最后登錄時間 curUser.setLastLogin(new Date()); userMapper.updateById(curUser); // 生成jwt Map<String, String> payload = new HashMap<>(); payload.put("userId", curUser.getId().toString()); // 加入一些非敏感的用戶信息 payload.put("userName", curUser.getUserName()); // 加入一些非敏感的用戶信息 String jwt = JwtUtil.generateToken(payload); return new ServiceRes(1, "登錄成功", jwt); } else return new ServiceRes(-1, "用戶名或密碼錯誤"); } else return new ServiceRes(-1, "用戶名或密碼不合法"); } /** * 改密業(yè)務(wù) * @return ServiceRes */ @Override public ServiceRes changePassWord(User user) { if(this.updatePassWord(user)) return new ServiceRes(1, "改密成功"); else return new ServiceRes(-1, "改密失敗"); } /** * 非對稱加密 * @param text 明文 * @return 密文 */ private String MD5Code(String text) { return DigestUtils.md5DigestAsHex(text.getBytes(StandardCharsets.UTF_8)); } /** * 修改密碼方法 * @param user 傳入用戶名和新密碼 * @return 改密成功返回 true 失敗返回 false */ private Boolean updatePassWord(User user) { // 密碼非對稱加密 user.setPassWord(this.MD5Code(user.getPassWord())); // 更新密碼 return userMapper.updateById(user)>0; } /** * 檢查用戶是否存在【用戶名密碼相同】 * @param user 用戶類 * @return 用戶存在返回 用戶對象 不存在返回 null */ private User checkUserIsExit(User user) { LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>(); lqw.eq(User::getUserName, user.getUserName()); lqw.eq(User::getPassWord, user.getPassWord()); return userMapper.selectOne(lqw); } /** * 判斷用戶名是否唯一 * @param user 用戶類 * @return 唯一返回 true 不唯一返回 false */ private Boolean checkUserNameIsUnique(User user) { LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<>(); lqw.eq(User::getUserName, user.getUserName()); List<User> userList = userMapper.selectList(lqw); return userList.size() == 0; } /** * 判斷用戶名密碼是否合法 * @param user 用戶類 * @return 滿足 【英文字母、數(shù)字、下劃線】 返回 true,否則返回 false */ private Boolean checkUserNameAndPassword(User user) { String regex = "^[_a-z0-9A-Z]+$"; return user.getUserName().matches(regex) && user.getPassWord().matches(regex); } }
6.Controller controller/UserController.java
登錄、注冊、改密業(yè)務(wù)【狀態(tài)續(xù)簽測試】
package com.cxstar.controller; import com.cxstar.bean.ControllerRes; import com.cxstar.bean.ServiceRes; import com.cxstar.bean.User; import com.cxstar.service.userService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Slf4j @Controller @ResponseBody @RequestMapping("/user") public class UserController { @Autowired userService userService; @PostMapping("/register") public ControllerRes register(User user) { // 注冊 ServiceRes serviceRes = userService.register(user); return new ControllerRes(serviceRes.getCode(), serviceRes.getMsg()); } @PostMapping("/login") public ControllerRes login(User user, HttpServletResponse response) { // 登錄 ServiceRes serviceRes = userService.login(user); // 登錄成功后往響應(yīng)頭插入jwt if(serviceRes.getJwt() != null) response.addHeader("access-token", serviceRes.getJwt()); return new ControllerRes(serviceRes.getCode(), serviceRes.getMsg()); } @PutMapping("/pwd") public ControllerRes changePassWord(User user, HttpServletRequest request) { // 取出jwt中的用戶 User jwtUser = (User)request.getAttribute("jwt-user"); // 合并jwt中用戶的用戶名與傳入用戶的新密碼 // 此處不能直接使用傳入的用戶名,防止惡意修改其他用戶的密碼 user.setId(jwtUser.getId()); // 改密 ServiceRes serviceRes = userService.changePassWord(user); return new ControllerRes(serviceRes.getCode(), serviceRes.getMsg()); } }
7.JWT工具類 utils/JwtUtil.java
生成和解析 token 的方法
package com.cxstar.utils; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTCreator; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.JWTVerifier; import java.util.Calendar; import java.util.Date; import java.util.Map; public class JwtUtil { // 簽名密鑰 private static final String SECRET = "hello JWT *%$#$&"; /** * 生成token * @param payload token攜帶的信息 * @return token字符串 */ public static String generateToken(Map<String,String> payload){ // 指定token過期時間 Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.HOUR, 24); // 24小時 JWTCreator.Builder builder = JWT.create(); // 構(gòu)建payload payload.forEach(builder::withClaim); // 指定簽發(fā)時間、過期時間 和 簽名算法,并返回token String token = builder.withIssuedAt(new Date()).withExpiresAt(calendar.getTime()).sign(Algorithm.HMAC256(SECRET)); return token; } /** * 解析token * @param token token字符串 * @return 解析后的token類 */ public static DecodedJWT decodeToken(String token){ JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET)).build(); DecodedJWT decodedJWT = jwtVerifier.verify(token); return decodedJWT; } }
8.HandlerInterceptor攔截器 interceptor.java
攔截器業(yè)務(wù)實現(xiàn)
package com.cxstar.interceptor; import com.alibaba.fastjson.JSONObject; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.TokenExpiredException; import com.auth0.jwt.interfaces.DecodedJWT; import com.cxstar.bean.ControllerRes; import com.cxstar.bean.User; import com.cxstar.utils.JwtUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * 攔截需要授權(quán)的接口 */ @Slf4j public class PermisssionInterceptor implements HandlerInterceptor { // 目標(biāo)方法執(zhí)行前調(diào)用 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { // 檢查用戶JWT String jwt = request.getHeader("access-token"); // 校驗并取出私有信息 try { // token 解碼 DecodedJWT dj = JwtUtil.decodeToken(jwt); // 取出基本用戶信息加入請求頭 -------------------------------------------------------------------------------- String userId = dj.getClaim("userId").asString(); String userName = dj.getClaim("userName").asString(); // jwt校驗合格的,將 jwt 中存的用戶信息加入請求頭,不合格的,請求頭存?zhèn)€空用戶 request.setAttribute("jwt-user", userId!=null?new User(Integer.valueOf(userId), userName):new User()); // ------------------------------------------------------------------------------------------------------- // 計算當(dāng)前時間是否超過過期時間的一半,如果是就幫用戶續(xù)簽 -------------------------- // 此處并不是永久續(xù)簽,只是為 大于過期時間的一半 且 小于過期時間 的 token 續(xù)簽 Long expTime = dj.getExpiresAt().getTime(); Long iatTime = dj.getIssuedAt().getTime(); Long nowTime = new Date().getTime(); if((nowTime-iatTime) > (expTime-iatTime)/2) { // 生成新的jwt Map<String, String> payload = new HashMap<>(); payload.put("userId", userId); // 加入一些非敏感的用戶信息 payload.put("userName", userName); // 加入一些非敏感的用戶信息 String newJwt = JwtUtil.generateToken(payload); // 加入返回頭 response.addHeader("access-token", newJwt); } // ----------------------------------------------------------------------- return true; } catch (JWTDecodeException e) { log.error("令牌錯誤"); addResBody(response, new ControllerRes(-1, "令牌錯誤")); // 新增返回體 return false; } catch (TokenExpiredException e) { log.error("令牌過期"); addResBody(response, new ControllerRes(-1, "令牌過期")); // 新增返回體 return false; } } // 目標(biāo)方法執(zhí)行后調(diào)用 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); } // 頁面渲染前調(diào)用 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { HandlerInterceptor.super.afterCompletion(request, response, handler, ex); } private void addResBody(HttpServletResponse response, ControllerRes res) throws IOException { response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 設(shè)置狀態(tài)碼 response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8"); PrintWriter out = response.getWriter(); out.write(JSONObject.toJSONString(res)); out.flush(); out.close(); } }
config/PermissionWebConfig.java
攔截器攔截規(guī)則
package com.cxstar.config; import com.cxstar.interceptor.PermisssionInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class PermissionWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new PermisssionInterceptor()) .addPathPatterns("/**") // 攔截哪些頁面 .excludePathPatterns("/user/login", "/user/register"); // 放行哪些頁面 } }
三、測試
1.注冊
注冊成功
數(shù)據(jù)入庫
2.登錄
登錄成功
查看登錄后返回的token
3.狀態(tài)續(xù)簽【登錄保持】
使用上一步登錄返回的 token 請求改密業(yè)務(wù)
當(dāng) JWT 存在時間小于 JWT 過期時間的一半時
業(yè)務(wù)會執(zhí)行成功
執(zhí)行業(yè)務(wù)不會返回續(xù)簽的 token
當(dāng) JWT 存在時間大于 JWT 過期時間的一半 且 小于過期時間 時
業(yè)務(wù)會執(zhí)行成功
執(zhí)行業(yè)務(wù)會返回續(xù)簽的 token,前端的下次請求需要使用新續(xù)簽的 token
當(dāng) JWT 存在時間大于 JWT 過期時間 時
業(yè)務(wù)會執(zhí)行失敗
執(zhí)行業(yè)務(wù)不會返回續(xù)簽的 token
到此這篇關(guān)于SpringBoot+JWT實現(xiàn)注冊、登錄、狀態(tài)續(xù)簽的實戰(zhàn)教程的文章就介紹到這了,更多相關(guān)SpringBoot JWT登錄狀態(tài)續(xù)簽內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java判斷數(shù)字位數(shù)的方法總結(jié)
本文給大家整理了Java判斷數(shù)字位數(shù)的兩種常用方法,對此有興趣的可以跟著小編一起學(xué)習(xí)下。2018-02-02Java并發(fā)編程中的synchronized關(guān)鍵字詳細(xì)解讀
這篇文章主要介紹了Java并發(fā)編程中的synchronized關(guān)鍵字詳細(xì)解讀,在Java早期版本中,synchronized 屬于 重量級鎖,效率低下,這是因為監(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的Mutex Lock來實現(xiàn)的,Java 的線程是映射到操作系統(tǒng)的原生線程之上的,需要的朋友可以參考下2023-12-12SpringBoot+layuimini實現(xiàn)左側(cè)菜單動態(tài)展示的示例代碼
Layuimini是Layui的升級版,它是專業(yè)做后臺頁面的框架,而且是適合PC端和移動端,以下地址可以在PC端顯示,也可以在手機上顯示,只不過會做自適應(yīng),本文將給大家介紹了SpringBoot+layuimini實現(xiàn)左側(cè)菜單動態(tài)展示的方法,需要的朋友可以參考下2024-04-04Spring Boot報錯:No session repository could be auto-configured
這篇文章主要給大家介紹了關(guān)于Spring Boot報錯:No session repository could be auto-configured, check your configuration的解決方法,文中給出了詳細(xì)的解決方法,對遇到這個問題的朋友們具有一定參考價值,需要的朋友下面來一起看看吧。2017-07-07java設(shè)計模式之裝飾器模式(Decorator)
這篇文章主要為大家詳細(xì)介紹了java設(shè)計模式之裝飾器模式Decorator,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-01-01一文搞懂如何實現(xiàn)Java,Spring動態(tài)啟停定時任務(wù)
定時任務(wù)的應(yīng)用場景十分廣泛,如定時清理文件、定時生成報表、定時數(shù)據(jù)同步備份等。本文將教你實現(xiàn)Java、Spring動態(tài)啟停定時任務(wù),感興趣的可以學(xué)習(xí)一下2022-06-06