PHP做的彩票網(wǎng)站好用嗎電腦培訓(xùn)中心
前言
大家從b站大學(xué)學(xué)習(xí)的項目側(cè)重點好像都在基礎(chǔ)功能的實現(xiàn)上,反而一個項目最根本的登錄攔截請求接口都不會寫,怎么攔截?為什么攔截?只知道用戶登錄時我后端會返回一個token,這個token是怎么生成的,我把它返回給前端干什么用?前端怎么去處理這個token?這個是我在學(xué)習(xí)過程中一知半解的,等開始做自己的項目時才知道原來還有這么多不會,本文就來講解一下怎么去實現(xiàn)登錄攔截請求校驗的方法。
一、導(dǎo)入數(shù)據(jù)庫表依賴
這里有一張常用的用戶表作為本文的實戰(zhàn)測試
CREATE TABLE `user` (`id` int(11) NOT NULL AUTO_INCREMENT,`username` varchar(50) NOT NULL COMMENT '用戶名',`password` varchar(255) NOT NULL COMMENT '密碼',`email` varchar(100) DEFAULT NULL COMMENT '郵箱',`create_time` datetime DEFAULT NULL COMMENT '創(chuàng)建時間',`login_time` datetime DEFAULT NULL COMMENT '最后一次登錄時間',`avatar` varchar(255) DEFAULT NULL COMMENT '頭像',PRIMARY KEY (`id`),UNIQUE KEY `username` (`username`) USING BTREE,UNIQUE KEY `email` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=33 DEFAULT CHARSET=utf8mb4 COMMENT='用戶表';
運(yùn)行然后連接。
二、登陸接口實現(xiàn)
@Api(tags = "用戶相關(guān)接口")
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@Autowiredprivate JwtProperties jwtProperties;@ApiOperation("用戶登錄")@PostMapping("/login")public Result login(@RequestBody User user) {user = userService.login(user);//登錄成功后,生成jwt令牌Map<String, Object> claims = new HashMap<>();claims.put("userId", user.getId());String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(),jwtProperties.getUserTtl(),claims);UserLoginVo userLoginVo = UserLoginVo.builder().user(user).token(token).build();return Result.okResult(userLoginVo);}@ApiOperation("注冊用戶")@PostMappingpublic Result addUser(@RequestBody UserDto userDto) {userService.addUser(userDto);return Result.okResult();}@ApiOperation("更新用戶信息")@PostMapping("/update")public Result uploadAvatar(User user) {userService.uploadAvatar(user);return Result.okResult();}@GetMapping("/test")public Result test() {return Result.okResult("test");}
}
寫了幾個常用的用戶層接口用來測試,主要關(guān)注用戶登錄/login接口,其他的暫時無需理會。
配置JwtProperties 類
@Component
@ConfigurationProperties(prefix = "zwk.jwt")
@Data
public class JwtProperties {/*** 用戶生成jwt令牌相關(guān)配置*/private String userSecretKey;private long userTtl;private String userTokenName;}
JwtProperties 對應(yīng)的配置文件
zwk:jwt:# 設(shè)置jwt簽名加密時使用的秘鑰user-secret-key: zwkzwk# 設(shè)置jwt過期時間user-ttl: 7200000# 設(shè)置前端傳遞過來的令牌名稱user-token-name: token
配置UserService 類
public interface UserService {void addUser(UserDto userDto);User login(User user);void uploadAvatar(User user);
}
UserService 的實現(xiàn)類
@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;/*** 新增用戶* @param userDto*/public void addUser(UserDto userDto) {User user = new User();BeanUtils.copyProperties(userDto, user);//user.setEmail("123@qq.com");user.setCreateTime(new Date());user.setLoginTime(new Date());userMapper.insert(user);}public User login(User user) {String password = user.getPassword();final User user1 = userMapper.getUserByName(user.getUsername());if (user1 == null) {throw new RuntimeException("該用戶名不存在");}//對密碼進(jìn)行md5加密//password = DigestUtils.md5DigestAsHex(password.getBytes());if (!password.equals(user1.getPassword())){throw new RuntimeException("密碼錯誤");}return user1;}/*** 更新用戶信息* @param user* @return*/@Overridepublic void uploadAvatar(User user) {userMapper.updateById(user);}
}
這里主要是對用戶登錄時傳過來的用戶名和密碼進(jìn)行校驗,校驗通過后我們再重新回到控制層看看是怎么處理的。
@ApiOperation("用戶登錄")@PostMapping("/login")public Result login(@RequestBody User user) {user = userService.login(user);//登錄成功后,生成jwt令牌Map<String, Object> claims = new HashMap<>();claims.put("userId", user.getId());String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(),jwtProperties.getUserTtl(),claims);UserLoginVo userLoginVo = UserLoginVo.builder().user(user).token(token).build();return Result.okResult(userLoginVo);}
- 如果登錄成功,代碼將生成一個 JWT(JSON Web Token)令牌。JWT 是一種緊湊的、自包含的方式,用于在客戶端和服務(wù)器之間傳遞安全信息。在這個例子中,JWT 令牌包含了用戶的 ID 信息。
- claims 是一個 Map,用于存儲 JWT 中的聲明(Claims),這里存儲了用戶 ID。
- JwtUtil.createJWT 方法用于創(chuàng)建 JWT 令牌,它接收三個參數(shù):用戶的密鑰(jwtProperties.getUserSecretKey())、令牌的有效時間(jwtProperties.getUserTtl())和聲明信息(claims)。
導(dǎo)入User類
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {private static final long serialVersionUID = 1L;/*** 主鍵*/@TableIdprivate Long id;/*** 用戶名*/private String username;private String password;private String email;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")private Date createTime;@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")private Date LoginTime;/*** 頭像*/private String avatar;
}
導(dǎo)入UserLoginVo類
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserLoginVo {private String token;private User user;
}
編寫JwtUtil工具類,該類用來生成jwt令牌
public class JwtUtil {/*** 生成jwt* 使用Hs256算法, 私匙使用固定秘鑰** @param secretKey jwt秘鑰* @param ttlMillis jwt過期時間(毫秒)* @param claims 設(shè)置的信息* @return*/public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {// 指定簽名的時候使用的簽名算法,也就是header那部分SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;// 生成JWT的時間long expMillis = System.currentTimeMillis() + ttlMillis;Date exp = new Date(expMillis);// 設(shè)置jwt的bodyJwtBuilder builder = Jwts.builder()// 如果有私有聲明,一定要先設(shè)置這個自己創(chuàng)建的私有的聲明,這個是給builder的claim賦值,一旦寫在標(biāo)準(zhǔn)的聲明賦值之后,就是覆蓋了那些標(biāo)準(zhǔn)的聲明的.setClaims(claims)// 設(shè)置簽名使用的簽名算法和簽名使用的秘鑰.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))// 設(shè)置過期時間.setExpiration(exp);return builder.compact();}/*** Token解密** @param secretKey jwt秘鑰 此秘鑰一定要保留好在服務(wù)端, 不能暴露出去, 否則sign就可以被偽造, 如果對接多個客戶端建議改造成多個* @param token 加密后的token* @return*/public static Claims parseJWT(String secretKey, String token) {// 得到DefaultJwtParserClaims claims = Jwts.parser()// 設(shè)置簽名的秘鑰.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))// 設(shè)置需要解析的jwt.parseClaimsJws(token).getBody();return claims;}}
以上就是我們前期準(zhǔn)備工作,然后發(fā)現(xiàn)好像還是沒用,因為我們還沒有做自定義攔截處理。我們首先對除了/user/login接口進(jìn)行放行,其他接口全部攔截。
編寫JwtTokenAdminInterceptor 類重寫HandlerInterceptor方法
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {@Autowiredprivate JwtProperties jwtProperties;/*** 校驗jwt** @param request* @param response* @param handler* @return* @throws Exception*/public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//判斷當(dāng)前攔截到的是Controller的方法還是其他資源if (!(handler instanceof HandlerMethod)) {//當(dāng)前攔截到的不是動態(tài)方法,直接放行return true;}//1、從請求頭中獲取令牌String token = request.getHeader(jwtProperties.getUserTokenName());//2、校驗令牌try {log.info("jwt校驗:{}", token);Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);Long userId = Long.valueOf(claims.get("userId").toString());log.info("當(dāng)前用戶id:{}", userId);//3、通過,放行return true;} catch (Exception ex) {//4、不通過,響應(yīng)401狀態(tài)碼response.setStatus(401);return false;}}
}
自定義攔截器WebMvcConfiguration
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {@Autowiredprivate JwtTokenAdminInterceptor jwtTokenAdminInterceptor;/*** 注冊自定義攔截器* @param registry*/protected void addInterceptors(InterceptorRegistry registry) {log.info("開始注冊自定義攔截器...");registry.addInterceptor(jwtTokenAdminInterceptor).addPathPatterns("/user/**") //表示攔截所以前綴帶/user的請求.excludePathPatterns("/user/login"); //排除特定路徑:excludePathPatterns("/user/login") 方法用于排除某些路徑,//即使它們匹配前面指定的模式。在這個例子中,/user/login 路徑不會被 jwtTokenAdminInterceptor 攔截。}/*** 設(shè)置靜態(tài)資源映射* @param registry*/protected void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");}
}
然后對接口進(jìn)行登錄測試
登錄測試
{"code": 200,"msg": "操作成功","data": {"token": "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjA2MDI3NzIsInVzZXJJZCI6MX0.Cf1ew-rPOkRYup5tird7nVD9xiHblNhYHwtdFHGQqV0","user": {"id": 1,"username": "kkk","password": "kkk123","email": "2765314967@qq.com","createTime": "2024-07-10 10:44:36","LoginTime": "2024-07-10 10:44:42","avatar": null,"loginTime": "2024-07-10 10:44:42"}}
}
可以看見,登錄成功后我們成功向前端返回token令牌。
那么前端拿到了這個token令牌有什么用呢?
- 第一次登錄的時候,前端調(diào)用后端的登錄接口,發(fā)送用戶名和密碼
- 后端收到請求,驗證用戶名和密碼,驗證成功,就給前端返回一個token
- 前端拿到token,將token存儲到localStorage和vuex中,并跳轉(zhuǎn)路由頁面
- 前端每次跳轉(zhuǎn)路由,就判斷l(xiāng)ocalStorage中有無token,沒有就跳轉(zhuǎn)到登錄頁面,有則跳轉(zhuǎn)到對應(yīng)的路由頁面
- 每次調(diào)后端接口,都要在請求頭中加token
- 后端判斷請求頭中有無token,有token,就拿到token并驗證token,驗證成功就返回數(shù)據(jù),驗證失敗(例如:token過期)就返回403,請求頭中沒有token也返回403
- 如果前端拿到狀態(tài)碼為403,就清除token信息并跳轉(zhuǎn)到登錄頁面
這個時候我們再來測試其他接口,應(yīng)為我們剛剛只放行了/user/login接口,其他接口是一律攔截的,我們看看直接請求會發(fā)生什么。
可以發(fā)現(xiàn),當(dāng)我們請求這個測試接口時,返回狀態(tài)碼401,和我們預(yù)想的一樣,如圖,就是我們剛剛寫的JwtTokenAdminInterceptor類
然后發(fā)現(xiàn)控制臺的jwt為空,這就應(yīng)對了我們前面所說的,當(dāng)我們將token返回給前端之后,前端之后的每次請求都會把token攜帶到到請求頭header里面?zhèn)鹘o后端,我們后端就可以通過HttpServletRequest獲取請求頭token,如圖:
然后根據(jù)我們后端自定義的攔截器看看是否需要對這個請求頭進(jìn)行判斷,如果不需要判斷,直接放放行,否則進(jìn)行jwt校驗。
那我們再次回到剛剛/user/test接口,我們剛剛也是由前端對該接口進(jìn)行請求,但這個時候前端請求頭里面的token為空,我們后端又對這個接口進(jìn)行了攔截,所以校驗自然失敗,無法訪問,這個時候我們再把登錄時生成的token放在前端傳給侯丹的請求頭里,看看會發(fā)生什么.
可以看到,這個時候就能成功請求。再看看控制臺
可以發(fā)現(xiàn),后端拿到前端傳過來的token后校驗通過,并且還可以通過token獲取用戶id,我們再回過頭看看最開始的問題,這個token有什么用,這個通過token獲取用戶id就是最明顯的體現(xiàn)之一。
我們只需要通過
Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);Long userId = Long.valueOf(claims.get("userId").toString());
我講的也不是很清楚,建議大家細(xì)看JwtTokenAdminInterceptor和 WebMvcConfiguration這兩個類,方可大成。
等我我后續(xù)大成后再重新回來更新。