一、Token介绍
代替Session,就是一个加密的字符串。
Token是当用户第一次访问服务端,由服务端生成的一串加密字符串,以作后续客户端进行请求的一个通行令牌。当第一次登录后,服务器生成一个Token字符串,并将此字符串返回给客户端,以后客户端请求需要带上这个Token发送服务器,进行请求数据即可,无需再次带上用户名和密码。
类似于城主发放的路引,进城需要出示路引做身份证明。
使用场景:
- 接口使用限制(聚合数据,天行数据,阿里云接口等);
- 登录场景(客户端登录后,服务器签发token返回客户端);
- 有时效的url链接控制(密保邮箱找回密码;邮箱激活账号);
二、jjwt——生成Token的组件
生成解析token字符串的常用组件有auth0,jjwt,这里选用jjwt进行学习
JJWT旨在成为最易于使用和理解的库,用于在jvm和Android上创建和验证JSON Web Token(JWT)。对JWT进行加密签名后,称为JWS。
官网:https://github.com/jwtk/jjwt
JWT表示形式是一个字符串,该字符串包含三个部分,每个部分之间都用.进行分隔,每个部分都是Base64URL编码的。如下:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.ipevRNuRP6HflG8cFKnmUPtypruRC4fb1DWtoLL62SY
第一部分:header标头,需要指定用于签署JWT的算法
{
"alg": "HS256"
}
第二部分:body身体,jwt中需要包含的Claims认证数据,claims分为标准cliams与自定义。
{
"sub": "Joe"
}
第三部分:密文,它是通过将标头和正文的组合通过标头中指定的算法加密来计算的。起到鉴伪作用。
三、使用示例
1.引入依赖
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.2</version>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.2</version><scope>runtime</scope>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred --><version>0.11.2</version><scope>runtime</scope>
</dependency>
2.Token工具类
public class TokenUtil {// 服务端密钥,混淆的字符串,一旦确定,就不能改变,如果密钥改变,所有的token会集体失效private static final String keystr = "1PFRmsM8buvtsMVVIRz6tARYhioOWXL6AJPrlgsVoeQ=";// 服务端ID 加密使用private static final String UID = "root";// 加密的字符串,生效多长时间private static final Integer expTime = 100 * 60 * 60 * 24 * 15;// token头public static final String TOKEN_HEADER = "Authorization";// 生成秘钥private static void generateKey() {// 指定算法SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);// 生产加密字符串String encode = Encoders.BASE64.encode(key.getEncoded());System.out.println(encode);}// 加密,生成Tokenpublic static String generateToken(String uid) {Map<String, Object> map = new HashMap<>();map.put(UID, uid);// 因为加密的字符串有有效期,获取当前的时间戳Date time = new Date();// 过期时间Date lastTime = new Date(time.getTime() + expTime);// 生成keyKey secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(keystr));String token = Jwts.builder().setClaims(map)// 自定义签名.setIssuedAt(time)// 标准签名.setExpiration(lastTime)// 过期时间.signWith(secretKey)// 加密的密钥.compact();return token;}//解密,还原Tokenpublic static String parse(String token) {Key secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(keystr));try {Jws<Claims> jws = Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);Claims body = jws.getBody();return body.get(UID, String.class);} catch (Exception e) {throw new JavasmException(JavasmExceptionEnum.SessionExpired);}}public static void main(String[] args) {// 测试生成密钥
// generateKey();// 测试生成token
// String token = TokenUtil.generateToken("1001");
// System.out.println(token);// eyJhbGciOiJIUzI1NiJ9.eyJyb290IjoiMTAwMSIsImlhdCI6MTczNjMyNTI1MCwiZXhwIjoxNzM2NDU0ODUwfQ.mMpS6SBntWJDDyOOucJyZ_8IlKPea4rCPCGGyCsb6Hg// 测试还原tokenString uid = TokenUtil.parse("eyJhbGciOiJIUzI1NiJ9" +".eyJyb290IjoiMTAwMSIsImlhdCI6MTczNjMyNTI1MCwiZXhwIjoxNzM2NDU0ODUwfQ" +".mMpS6SBntWJDDyOOucJyZ_8IlKPea4rCPCGGyCsb6Hg");System.out.println(uid); // 1001}
}
3.后端接口中引入Token影响登录
登录的逻辑:
- 如果用户传入了用户名密码,按照用户名密码登录
- 登录成功之后,生成token,返回token给用户
- 如果用户没有传入用户名密码,检查是否有token
- 如果token有数据,返回用户信息,
- 如果token 没有数据,返回错误信息,
- 如果token、用户名、密码都空,返回错误信息
代码:
@PostMapping("/doUnamePwdTokenLogin")
public ResponseEntity<R> doUnamePwdTokenLogin(String username, String password) {WebUser webUser = null;if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) {// 说明传入了用户名密码webUser = loginService.doUnameLogin(username, password);} else {// 没有传入用户名密码,检查是否有token// 从header中获取前端传入的tokenString clientToken = request.getHeader(TokenUtil.TOKEN_HEADER);// 如果token为空,说明没有传入用户名或密码,返回错误信息if (StringUtils.isEmpty(clientToken)) {throw new JavasmException(JavasmExceptionEnum.ParameterNull);} else {// 走到这里说明至少是传入了token的,需要校验tokenString uid = TokenUtil.parse(clientToken);webUser = loginService.queryByUid(uid); }}if (webUser != null && webUser.getUid() != null) {// 生成token信息String uid = webUser.getUid().toString();String token = TokenUtil.generateToken(uid);// 将token返回给用户// token不会跟随body返回,所以要放到header中HttpHeaders httpHeaders = new HttpHeaders();// 把token存入header中httpHeaders.add(TokenUtil.TOKEN_HEADER, token);// 默认情况下,浏览器的js中,只会处理系统自带的header属性,自定义的属性是无法通过js获取的// 需要设置一下,特殊处理的属性是哪个httpHeaders.add("Access-Control-Expose-Headers", TokenUtil.TOKEN_HEADER);return new ResponseEntity<>(R.ok(webUser), httpHeaders, HttpStatus.OK);}return ResponseEntity.ok(R.ok());
}
4.前端接口中引入Token实现首页自动登录
src\stores\TokenStore.js:
import { ref, computed } from "vue";
import { defineStore } from "pinia";export default defineStore("tokenStore",() => {const token = ref("");return { token };},{persist: {storage: localStorage, // 默认是localStoragepaths: ["token"], // 将token的属性存入localStorage},}
);
src\plugins\axios.js:
//请求
axios.interceptors.request.use((config) => {// 这里表示每次请求的时候,都会携带tokenlet token = tokenStore().token;console.log("token请求:" +token);if (token != null && token != undefined && token !== "") {config.headers.Authorization = token;}return config;},(error) => {return Promise.reject(error);}
);
//响应
axios.interceptors.response.use(response => {let token = response.headers.authorization; // 注意这里要小写 console.log("token响应:" + token);if (token != null && token != undefined && token !== "") {tokenStore().token = token;}if (response.status === 200) {//服务器 给前端的内容return Promise.resolve(response);} else {return Promise.reject(response);}},(error) => {alert(`异常请求:${JSON.stringify(error.message)}`);}
);
src\views\Home.vue:
let user;
let isLogin = ref(false)
let checkLogin = () => {// 如果未登录,跳转到登录页面user = loginUser().userModelconsole.log(user.uid);if (user.uid !== -1) {isLogin.value = true} else {// 自动登录axios.post('/login/doUnamePwdTokenLogin').then(result => {if (result.code === 200) {// 登录成功loginUser().userModel = result.data;isLogin.value = trueuser = loginUser().userModel} else {// 登录失败,跳转到登录页面router.push('/login')}})}
}
onMounted(() => {checkLogin();
});
四、拦截器
给某一些接口添加加密功能:
只有登录的用户才能访问指定接口,接口需要加密,即加上指定的注解。
com.javaplay.playPal.common.annoation.Auth:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auth {
}
com.javaplay.playPal.common.interceptor.LoginInterceptor:
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request,HttpServletResponse response,Object handler) throws Exception {// 获取被访问的方法对象HandlerMethod handlerMethod = (HandlerMethod) handler;Method method = handlerMethod.getMethod();// 获取被访问的方法上的注解对象Auth annotation = method.getAnnotation(Auth.class);if (annotation == null) {// 没有注解,说明不加密,放行return true;}// 有注解,说明需要加密,判断是否登录String token = request.getHeader(TokenUtil.TOKEN_HEADER);if (StringUtils.isEmpty(token)){// 没有token,说明没有登录,返回错误信息throw new JavasmException(JavasmExceptionEnum.PermissionDenied);}// 解析tokenString uid = TokenUtil.parse(token);return true;}
}
com.javaplay.playPal.common.interceptor.PlayWebConfig:
@Component
public class PlayWebConfig implements WebMvcConfigurer {@Resourceprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/login/**");}
}
加了注解的请求方法必须要登录以后才能看见:
/*** 分页查询所有数据*/
@GetMapping("/page")
@Auth
public R selectAll(@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,@RequestParam(value = "pageSize", defaultValue = "3") Integer pageSize) {PageInfo<Dynamics> pageInfo = dynamicsService.getPageInfo(pageNum, pageSize);return R.ok(pageInfo);
}