文章目录
- TGAM-springboot入门练手项目
- 一、Token是什么?
- 1.为什么需要token?
- 2.大致使用流程
- 二、JWT使用步骤
- 1.引入依赖
- 2.编写工具类生成TOKEN
- 3.配置注解、拦截器
- 4.测试使用
一、Token是什么?
Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。
1.为什么需要token?
在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token便应运而生。刚开始写springboot时,可能就只会简单地去实现登录和增删改查、但是试想一下,如果没有权限去管理这些API的使用,那么登录也没有什么意义,直接访问即可,又有人说此时可以采用前端或者后端拦截器啊?但在现在的前后分离情况下,别人知道了你的API地址,然后直接向你的数据暴力插数据,那么你是不是就凉凉了,此时你可能又会考虑后端控制层去判断,这样又回到了最初的背景。
因此,Token的主要目的为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。
2.大致使用流程
(1)客户端使用用户名跟密码请求登录
(2)服务端收到请求,去验证用户名与密码
(3)验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
(4)客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
(5)客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
(6)APP登录的时候发送加密的用户名和密码到服务器,服务器验证用户名和密码,如果成功,以某种方式比如随机生成32位的字符串作为token,存储到服务器中,并返回token到APP,以后APP请求时,
(7)凡是需要验证的地方都要带上该token,然后服务器端验证token,成功返回所需要的结果,失败返回错误信息,让他重新登录。其中服务器上token设置一个有效期,每次APP请求的时候都验证token和有效期。
3.JWT是什么
JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为JSON对象。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对对JWT进行签名。
header头文件信息、 payload 荷载、signature加密验证
(1)用户请求时携带此token(分为三部分,header密文,payload密文,签名)到服务端,服务端解析第一部分(header密文),用Base64解密,可以知道用了什么算法进行签名,此处解析发现是HS256。
(2)服务端使用原来的秘钥与密文(header密文+"."+payload密文)同样进行HS256运算,然后用生成的签名与token携带的签名进行对比,若一致说明token合法,不一致说明原文被修改。
(3)判断是否过期,客户端通过用Base64解密第二部分(payload密文),可以知道荷载中授权时间,以及有效期。通过这个与当前时间对比发现token是否过期。
官网:https://jwt.io/introduction
二、JWT使用步骤
1.引入依赖
pom.xml加入
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.3.0</version>
</dependency>
2.编写工具类生成TOKEN
package com.lyf.utils.authority;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.junit.Test;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtUtil {
//密钥
public static final String SECRET ="";//自己任意取一个字符串
//过期时间:秒
public static final int EXPIRE = 500;// 设置token时效
/**
* 生成Token
* @param password
* @param userName
* @return
* @throws Exception
*/
public static String createToken(String userName, String password) throws Exception {
// 现在时间
Calendar nowTime = Calendar.getInstance();
nowTime.add(Calendar.SECOND, EXPIRE);
// 希望的日期,也就失效的日期
Date expireDate = nowTime.getTime();
System.out.println(nowTime.getTime());
Map<String, Object> map = new HashMap<>();
// header
map.put("alg", "HS256");
map.put("typ", "JWT");
String token = JWT.create()
.withHeader(map)//头
.withClaim("userName", userName)
.withClaim("password", password)
.withSubject("TGAM验证")//
.withIssuedAt(new Date())//签名时间
.withExpiresAt(expireDate)//过期时间
.sign(Algorithm.HMAC256(SECRET));//签名
return token;
}
/**
* 验证Token
* @param token
* @return
* @throws Exception
*/
//
public static Map<String, Claim> verifyToken(String token)throws Exception{
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
DecodedJWT jwt = null;
try {
jwt = verifier.verify(token);
}catch (Exception e){
//return "凭证已过期,请重新登录";
throw new RuntimeException("凭证已过期,请重新登录");
}
return jwt.getClaims();
// return jwt.getToken();
}
/**
* 解析Token
* @param token
* @return
*/
public static Map<String, Claim> parseToken(String token){
DecodedJWT decodedJWT = JWT.decode(token);
return decodedJWT.getClaims();
}
@Test
public void test() throws Exception {
// String token = JwtUtil.createToken("3213","Tom22");
// System.out.println("token:"+token);
// System.out.println(JwtUtil.verifyToken(token));
System.out.println(JwtUtil.parseToken("efyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJUR0FN6aqM6K-BIiwidXNlck5hbWUiOiJUb20yMiIsImV4cCI6MTYxMjAxMDgwMCwidXNlcklkIjoiMzIxMyIsImlhdCI6MTYxMjAxMDMwMH0.INDJ9KS53uTwbU25Gys2aK7UWYb0Xd2fpyp3h6DcvRU").get("userId").asString());
}
}
3.配置注解、拦截器
(1)配置注解、并在相应的方法注解(在需要token验证(尤其是数据安地方)的地方注解LoginToken,不需要的地方(如登录接口)注解PassToken)
/**
* @AUTHOR LYF
* @DATE 2021-01-31
* @DESC跳过验证的PassToken
*/
@Target({ElementType.METHOD,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required()default true;
}
/**
* @AUTHOR LYF
* @DATE 2021-01-30
* @DESC 需要登录才能进行的操作
*
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginToken {
boolean required()default true;
}
(2)配置拦截器
注入拦截配置
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**");// 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
}
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}
实现拦截处理器:在preHandle中进行token的认证(判断header中是否带token=>判断token是否合法、有无权限=>判断token是否失效)
相应的处理之后,进行异常抛出,在一个统一异常处理类中进行数据返回。
package com.lyf.interceptor;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.Claim;
import com.google.gson.JsonObject;
import com.lyf.controller.ErrorController;
import com.lyf.dao.mapper.UserMapper;
import com.lyf.sercurity.LoginToken;
import com.lyf.sercurity.PassToken;
import com.lyf.utils.authority.JwtUtil;
import com.lyf.utils.result.Result;
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;
import java.util.Map;
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
UserMapper userMapper;
@Autowired
ErrorController errorController;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// Enumeration<String> token = request.getHeaders("token");
System.out.println("path"+request.getContextPath());
System.out.println("token"+request.getHeader("token"));
String token = request.getHeader("token");//从header中获取token
// handler??
if(!(handler instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod =(HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 检查是否有PassToken注解,有则跳过
if(method.isAnnotationPresent(PassToken.class)){
PassToken passToken = method.getAnnotation(PassToken.class);
if(passToken.required()){
return true;
}
}
// 检查有无需要用户权限的注解
if(method.isAnnotationPresent(LoginToken.class)){
LoginToken loginToken = method.getAnnotation(LoginToken.class);
// 执行认证
if(loginToken.required()){
if (token == null){
// return resp;
throw new RuntimeException("无token,请重新登录");
}
try{
Claim username = JwtUtil.parseToken(token).get("userName");
Claim password = JwtUtil.parseToken(token).get("password");
}catch (JWTDecodeException e){
throw new RuntimeException("无权限");
}
try {
JwtUtil.verifyToken(token);
}catch (Exception e){
throw new RuntimeException("凭证已过期,请重新登录");
}
return true;
}
}
return true;
}
@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 {
}
}
异常处理类
@org.springframework.web.bind.annotation.ExceptionHandler
@ResponseBody
public JSONObject RuntimeHandler(RuntimeException e){
logger.error("token验证拦截",e.getMessage());
JSONObject resp;
resp = Result.ERROR();
resp.put("detail",e.getMessage());
return resp;
}
更多关于拦截器的配置详细介绍,请参考:
4.测试使用
无token登录
登录获取token
带token登录(若失效则出现以下情况,未失效则可以正常获取相应接口数据,失效自己根据需要设置)
在使用JWT时,一个让人纠结的问题就是“Token的时限多长才合适?”。对此,Stormpath的这篇文章给出了一个可供参考的建议:
(1)面对极度敏感的信息,如钱或银行数据,那就根本不要在本地存放Token,只存放在内存中。这样,随着App关闭,Token也就没有了。
此外,将Token的时限设置成较短的时间(如1小时)。
(2)对于那些虽然敏感但跟钱没关系,如健身App的进度,这个时间可以设置得长一点,如1个月。
(3)对于像游戏或社交类App,时间可以更长些,半年或1年。
<font size=3
并且,文章还建议增加一个“Token吊销”过程来应对Token被盗的情形,类似于当发现银行卡或电话卡丢失,用户主动挂失的过程。
关于“Token吊销”的实现,文章建议个方式如下:
在DB中记录用户对应的Token
实现一个Api Endpoint,负责将指定用户的Token从DB中删除