Java實(shí)現(xiàn)Token登錄驗(yàn)證的項(xiàng)目實(shí)踐
一、JWT是什么?
在介紹JWT之前,我們先來回顧一下利用token進(jìn)行用戶身份驗(yàn)證的流程:
1、客戶端使用用戶名和密碼請(qǐng)求登錄
2、服務(wù)端收到請(qǐng)求,驗(yàn)證用戶名和密碼
3、驗(yàn)證成功后,服務(wù)端會(huì)簽發(fā)一個(gè)token,再把這個(gè)token返回給客戶端
4、客戶端收到token后可以把它存儲(chǔ)起來,比如放到cookie中
5、客戶端每次向服務(wù)端請(qǐng)求資源時(shí)需要攜帶服務(wù)端簽發(fā)的token,可以在cookie或者h(yuǎn)eader中攜帶
6、服務(wù)端收到請(qǐng)求,然后去驗(yàn)證客戶端請(qǐng)求里面帶著的token,如果驗(yàn)證成功,就向客戶端返回請(qǐng)求數(shù)據(jù)
這種基于token的認(rèn)證方式相比傳統(tǒng)的session認(rèn)證方式更節(jié)約服務(wù)器資源,并且對(duì)移動(dòng)端和分布式更加友好。其優(yōu)點(diǎn)如下:
支持跨域訪問:cookie是無法跨域的,而token由于沒有用到cookie(前提是將token放到請(qǐng)求頭中),所以跨域后不會(huì)存在信息丟失問題
無狀態(tài):token機(jī)制在服務(wù)端不需要存儲(chǔ)session信息,因?yàn)閠oken自身包含了所有登錄用戶的信息,所以可以減輕服務(wù)端壓力
更適用CDN:可以通過內(nèi)容分發(fā)網(wǎng)絡(luò)請(qǐng)求服務(wù)端的所有資料
更適用于移動(dòng)端:當(dāng)客戶端是非瀏覽器平臺(tái)時(shí),cookie是不被支持的,此時(shí)采用token認(rèn)證方式會(huì)簡單很多
無需考慮CSRF:由于不再依賴cookie,所以采用token認(rèn)證方式不會(huì)發(fā)生CSRF,所以也就無需考慮CSRF的防御
而JWT就是上述流程當(dāng)中token的一種具體實(shí)現(xiàn)方式,其全稱是JSON Web Token,官網(wǎng)地址:https://jwt.io/
通俗地說,JWT的本質(zhì)就是一個(gè)字符串,它是將用戶信息保存到一個(gè)Json字符串中,然后進(jìn)行編碼后得到一個(gè)JWT token,并且這個(gè)JWT token帶有簽名信息,接收后可以校驗(yàn)是否被篡改,所以可以用于在各方之間安全地將信息作為Json對(duì)象傳輸。JWT的認(rèn)證流程如下:
1、首先,前端通過Web表單將自己的用戶名和密碼發(fā)送到后端的接口,這個(gè)過程一般是一個(gè)POST請(qǐng)求。建議的方式是通過SSL加密的傳輸(HTTPS),從而避免敏感信息被嗅探
2、后端核對(duì)用戶名和密碼成功后,將包含用戶信息的數(shù)據(jù)作為JWT的Payload,將其與JWT Header分別進(jìn)行Base64編碼拼接后簽名,形成一個(gè)JWT Token,形成的JWT Token就是一個(gè)如同lll.zzz.xxx的字符串
3、后端將JWT Token字符串作為登錄成功的結(jié)果返回給前端。前端可以將返回的結(jié)果保存在瀏覽器中,退出登錄時(shí)刪除保存的JWT Token即可
4、前端在每次請(qǐng)求時(shí)將JWT Token放入HTTP請(qǐng)求頭中的Authorization屬性中(解決XSS和XSRF問題)
5、后端檢查前端傳過來的JWT Token,驗(yàn)證其有效性,比如檢查簽名是否正確、是否過期、token的接收方是否是自己等等
6、驗(yàn)證通過后,后端解析出JWT Token中包含的用戶信息,進(jìn)行其他邏輯操作(一般是根據(jù)用戶信息得到權(quán)限等),返回結(jié)果
最后:說白了,JWT:JSON Web Token,其實(shí)token就是一段字符串,由三部分組成:Header,Payload,Signature
二、使用步驟
1.項(xiàng)目結(jié)構(gòu)
2.相關(guān)依賴
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.22</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.21</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.79</version> </dependency> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.9.4</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.22</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.11.0</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.1.2</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>2.3.8.RELEASE</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.18.3</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> </dependencies>
3.數(shù)據(jù)庫
這里進(jìn)行測(cè)試,所以用戶類只有用戶名密碼,自行創(chuàng)建
4.相關(guān)代碼
1、annotation包
PassToken:
package com.geesun.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author :Mr.ZJW * @date :Created 2022/2/28 10:26 * @description:用來跳過驗(yàn)證的 PassToken */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface PassToken { boolean required() default true; }
UserLoginToken:
package com.geesun.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author :Mr.ZJW * @date :Created 2022/2/28 10:26 * @description:用于登錄后才能操作的token */ /*RetentionPolicy.RUNTIME:這種類型的Annotations將被JVM保留, 所以他們能在運(yùn)行時(shí)被JVM或其他使用反射機(jī)制的代碼所讀取和使用。*/ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface UserLoginToken { boolean required() default true; }
2、common包
CodeMsg:
package com.geesun.common; /** * @author :Mr.ZJW * @date :Created 2022/2/28 10:26 * @description:返回提示 */ public class CodeMsg { private int retCode; private String message; // 按照模塊定義CodeMsg // 通用異常 public static CodeMsg SUCCESS = new CodeMsg(0,"success"); public static CodeMsg SERVER_EXCEPTION = new CodeMsg(500100,"服務(wù)端異常"); public static CodeMsg PARAMETER_ISNULL = new CodeMsg(500101,"輸入?yún)?shù)為空"); // 業(yè)務(wù)異常 public static CodeMsg USER_NOT_EXSIST = new CodeMsg(500102,"用戶不存在"); public static CodeMsg ONLINE_USER_OVER = new CodeMsg(500103,"在線用戶數(shù)超出允許登錄的最大用戶限制。"); public static CodeMsg SESSION_NOT_EXSIST = new CodeMsg(500104,"不存在離線session數(shù)據(jù)"); public static CodeMsg NOT_FIND_DATA = new CodeMsg(500105,"查找不到對(duì)應(yīng)數(shù)據(jù)"); public static CodeMsg USER_OR_PASS_ERROR = new CodeMsg(500102,"賬號(hào)或者密碼錯(cuò)誤,請(qǐng)重試!"); private CodeMsg(int retCode, String message) { this.retCode = retCode; this.message = message; } public int getRetCode() { return retCode; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
Result:
package com.geesun.common; /** * @author :Mr.ZJW * @date :Created 2022/2/28 10:26 * @description:返回統(tǒng)一結(jié)果集 */ public class Result<T> { private String message; private int retCode; private T data; private Result(T data) { this.retCode = 200; this.message = "成功"; this.data = data; } private Result(CodeMsg cm) { if(cm == null){ return; } this.retCode = cm.getRetCode(); this.message = cm.getMessage(); } /** * 成功時(shí)候的調(diào)用 * @return */ public static <T> Result<T> success(T data){ return new Result<T>(data); } /** * 成功,不需要傳入?yún)?shù) * @return */ @SuppressWarnings("unchecked") public static <T> Result<T> success(){ return (Result<T>) success(""); } /** * 失敗時(shí)候的調(diào)用 * @return */ public static <T> Result<T> error(CodeMsg cm){ return new Result<T>(cm); } /** * 失敗時(shí)候的調(diào)用,擴(kuò)展消息參數(shù) * @param cm * @param msg * @return */ public static <T> Result<T> error(CodeMsg cm,String msg){ cm.setMessage(cm.getMessage()+"--"+msg); return new Result<T>(cm); } public T getData() { return data; } public String getMessage() { return message; } public int getRetCode() { return retCode; } }
3、config包
InterceptorConfig:
package com.geesun.config; import com.geesun.Interceptor.AuthenticationInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.format.FormatterRegistry; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.config.annotation.*; import java.util.List; /** * @author :Mr.ZJW * @date :Created 2022/2/28 10:25 * @description:新建Token攔截器 */ @Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor()) .addPathPatterns("/**"); // 攔截所有請(qǐng)求,通過判斷是否有 @LoginRequired 注解 決定是否需要登錄 } @Bean public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); } @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> arg0) { // TODO Auto-generated method stub } @Override public void addCorsMappings(CorsRegistry arg0) { // TODO Auto-generated method stub } @Override public void addFormatters(FormatterRegistry arg0) { // TODO Auto-generated method stub } @Override public void addResourceHandlers(ResourceHandlerRegistry arg0) { // TODO Auto-generated method stub } @Override public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> arg0) { // TODO Auto-generated method stub } @Override public void addViewControllers(ViewControllerRegistry arg0) { // TODO Auto-generated method stub } @Override public void configureAsyncSupport(AsyncSupportConfigurer arg0) { // TODO Auto-generated method stub } @Override public void configureContentNegotiation(ContentNegotiationConfigurer arg0) { // TODO Auto-generated method stub } @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer arg0) { // TODO Auto-generated method stub } @Override public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> arg0) { // TODO Auto-generated method stub } @Override public void configureMessageConverters(List<HttpMessageConverter<?>> arg0) { // TODO Auto-generated method stub } @Override public void configurePathMatch(PathMatchConfigurer arg0) { // TODO Auto-generated method stub } @Override public void configureViewResolvers(ViewResolverRegistry arg0) { // TODO Auto-generated method stub } @Override public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> arg0) { // TODO Auto-generated method stub } @Override public void extendMessageConverters(List<HttpMessageConverter<?>> arg0) { // TODO Auto-generated method stub } @Override public MessageCodesResolver getMessageCodesResolver() { // TODO Auto-generated method stub return null; } @Override public Validator getValidator() { // TODO Auto-generated method stub return null; } }
4、Interceptor包
AuthenticationInterceptor:
package com.geesun.Interceptor; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTDecodeException; import com.auth0.jwt.exceptions.JWTVerificationException; import com.geesun.annotation.PassToken; import com.geesun.annotation.UserLoginToken; import com.geesun.pojo.User; import com.geesun.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; /** * @author :Mr.ZJW * @date :Created 2022/2/28 10:24 * @description:攔截器 */ public class AuthenticationInterceptor implements HandlerInterceptor { @Autowired UserService userService; @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception { String token = httpServletRequest.getHeader("token");// 從 http 請(qǐng)求頭中取出 token // 如果不是映射到方法直接通過 if(!(object instanceof HandlerMethod)){ return true; } HandlerMethod handlerMethod=(HandlerMethod)object; Method method=handlerMethod.getMethod(); //檢查是否有passtoken注釋,有則跳過認(rèn)證 if (method.isAnnotationPresent(PassToken.class)) { PassToken passToken = method.getAnnotation(PassToken.class); if (passToken.required()) { return true; } } //檢查有沒有需要用戶權(quán)限的注解 if (method.isAnnotationPresent(UserLoginToken.class)) { UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class); if (userLoginToken.required()) { // 執(zhí)行認(rèn)證 if (token == null) { throw new RuntimeException("無token,請(qǐng)重新登錄"); } // 獲取 token 中的 user id String userId; try { userId = JWT.decode(token).getAudience().get(0); } catch (JWTDecodeException j) { throw new RuntimeException("401"); } User user = userService.findUserById(userId); if (user == null) { throw new RuntimeException("用戶不存在,請(qǐng)重新登錄"); } // 驗(yàn)證 token JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build(); try { jwtVerifier.verify(token); } catch (JWTVerificationException e) { throw new RuntimeException("401"); } return true; } } return true; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } }
5、utils包
TokenUtil:
package com.geesun.utils; import com.auth0.jwt.JWT; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; /** * @author :Mr.ZJW * @date :Created 2022/2/28 10:24 * @description: */ public class TokenUtil { public static String getTokenUserId() { String token = getRequest().getHeader("token");// 從 http 請(qǐng)求頭中取出 token String userId = JWT.decode(token).getAudience().get(0); return userId; } /** * 獲取request * @return */ public static HttpServletRequest getRequest() { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder .getRequestAttributes(); return requestAttributes == null ? null : requestAttributes.getRequest(); } }
6、pojo包
User:
package com.geesun.pojo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; @Data @AllArgsConstructor @NoArgsConstructor @TableName(value = "`user`") public class User implements Serializable { @TableId(value = "id", type = IdType.NONE) private String id; @TableField(value = "username") private String username; @TableField(value = "password") private String password; private static final long serialVersionUID = 1L; }
7、controller包
UserController:
package com.geesun.controller; import cn.hutool.json.JSONObject; import com.geesun.annotation.UserLoginToken; import com.geesun.common.CodeMsg; import com.geesun.common.Result; import com.geesun.pojo.User; import com.geesun.service.UserService; import com.geesun.service.impl.TokenService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; /** * @author :Mr.ZJW * @date :Created 2022/2/26 10:47 * @description: */ @RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @Autowired private TokenService tokenService; /** * 查詢用戶信息 * @return */ @UserLoginToken @GetMapping("/list") public Result<Object> list(){ return Result.success(userService.list()); } /** * 登錄驗(yàn)證 * @param user * @param response * @return */ @RequestMapping(value = "/login" ,method = RequestMethod.GET) public Result<Object> login(User user, HttpServletResponse response) { JSONObject jsonObject = new JSONObject(); //獲取用戶賬號(hào)密碼 User userForBase = new User(); userForBase.setId(userService.findByUsername(user).getId()); userForBase.setUsername(userService.findByUsername(user).getUsername()); userForBase.setPassword(userService.findByUsername(user).getPassword()); //判斷賬號(hào)或密碼是否正確 if (!userForBase.getPassword().equals(user.getPassword())) { return Result.error(CodeMsg.USER_OR_PASS_ERROR); } else { String token = tokenService.getToken(userForBase); jsonObject.put("token", token); Cookie cookie = new Cookie("token", token); cookie.setPath("/"); response.addCookie(cookie); return Result.success(jsonObject); } } }
8、service包
UserService接口:
package com.geesun.service; import com.baomidou.mybatisplus.extension.service.IService; import com.geesun.pojo.User; public interface UserService extends IService<User> { int deleteByIds(Long[] ids); int addUser(User user); User findByUsername(User user); User findUserById(String userId); }
UserServiceImpl實(shí)現(xiàn)類:
package com.geesun.service.impl; import cn.hutool.core.util.ArrayUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.geesun.mapper.UserMapper; import com.geesun.pojo.User; import com.geesun.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Arrays; @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Autowired private UserMapper userMapper; /** * 判斷用戶名 * @param user * @return */ public User findByUsername(User user){ return userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUsername,user.getUsername())); } public User findUserById(String userId) { return userMapper.selectById(userId); } }
TokenService:
package com.geesun.service.impl; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import com.geesun.pojo.User; import org.springframework.stereotype.Service; import java.util.Date; /** * @author :Mr.ZJW * @date :Created 2022/2/28 10:20 * @description: */ @Service public class TokenService { public String getToken(User user) { Date start = new Date(); long currentTime = System.currentTimeMillis() + 60* 60 * 1000;//一小時(shí)有效時(shí)間 Date end = new Date(currentTime); String token = ""; token = JWT.create().withAudience(user.getId()).withIssuedAt(start).withExpiresAt(end) .sign(Algorithm.HMAC256(user.getPassword())); return token; } }
9、mapper包
package com.geesun.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.geesun.pojo.User; public interface UserMapper extends BaseMapper<User> { }
三、測(cè)試結(jié)果
1、登錄驗(yàn)證
2、查詢用戶信息
這個(gè)方法加上了@UserLoginToken,所以要token才能查詢
3、不加上Token進(jìn)行測(cè)試就會(huì)出錯(cuò)提示
出錯(cuò)提示:
到此這篇關(guān)于Java實(shí)現(xiàn)Token登錄驗(yàn)證的項(xiàng)目實(shí)踐的文章就介紹到這了,更多相關(guān)Java Token登錄驗(yàn)證內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
通過spring boot 設(shè)置tomcat解決 post參數(shù)限制問題
這篇文章主要介紹了通過spring boot 設(shè)置tomcat解決 post參數(shù)限制問題,需要的朋友可以參考下2019-05-05Java kafka如何實(shí)現(xiàn)自定義分區(qū)類和攔截器
這篇文章主要介紹了Java kafka如何實(shí)現(xiàn)自定義分區(qū)類和攔截器,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06spring框架下@value注解屬性static無法獲取值問題
這篇文章主要介紹了spring框架下@value注解屬性static無法獲取值問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11SpringBoot使用AOP實(shí)現(xiàn)統(tǒng)一角色權(quán)限校驗(yàn)
這篇文章主要介紹了SpringBoot如何使用AOP實(shí)現(xiàn) 統(tǒng)一角色權(quán)限校驗(yàn),文中有詳細(xì)的代碼示例講解和操作流程,具有一定的參考價(jià)值,需要的朋友可以參考下2023-07-07java實(shí)現(xiàn)Excel轉(zhuǎn)換為圖片
在實(shí)際開發(fā)過程中,經(jīng)常會(huì)有這樣的需求,需要將Excel表格或特定區(qū)域轉(zhuǎn)換為圖片,所以小編今天就來為大家介紹一下如何使用Java將Excel轉(zhuǎn)化為圖片吧2023-10-10