登陆功能是每个系统的最基本功能,在SSM技术栈中,登陆状态验证一般会使用服务端的session,但是session并没有想象中的那么好用,经常会出现由于sessionid不一致导致的信息丢失,更好的解决方案就是使用JWT的Token生成。
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息,该信息可以被验证和信任,因为它是数字签名的,常用于单点登录。
JWT-token
JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是:头部,载荷,签名
本次使用jwt的token生成来实现一个登陆状态的验证。
本次使用mybatis-plus的一键生成项目,具体的步骤可以查看我之前的文章mybatis-plus详解http://t.csdn.cn/F0QcR
引入依赖
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-implartifactId>
<version>0.11.2version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-apiartifactId>
<version>0.11.2version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-jacksonartifactId>
<version>0.11.2version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
<version>2.7.0version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-textartifactId>
<version>1.8version>
dependency>
生成token的工具类:
package com.lzl.utils;
import io.jsonwebtoken.*;
import org.apache.commons.lang3.time.DateUtils;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Date;
//jwt工具类
public class JwtHelper {
// 生成Jwt
public static String jwsWithHS(SignatureAlgorithm signatureAlgorithm, String userInfo, int num, String secret) {
Key key = new SecretKeySpec(secret.getBytes(), signatureAlgorithm.getJcaName());
Claims claims = Jwts.claims();
claims.setSubject(userInfo); // jwt的信息的本身
claims.setExpiration(DateUtils.addSeconds(new Date(), num)); //设置jwt多少秒过期
String jws = Jwts.builder()
.setClaims(claims).signWith(key, signatureAlgorithm).compact();
return jws;
}
// 校验Jwt
public static Jwt verifySign(String jws, String secret, SignatureAlgorithm signatureAlgorithm) {
Key key = new SecretKeySpec(secret.getBytes(), signatureAlgorithm.getJcaName());
Jwt jwt = Jwts.parserBuilder().setSigningKey(key).build().parse(jws);
return jwt;
}
}
本次使用的secret:
由于我们需要在controller中使用,所以把两个参数,密钥和加密方式配置在yml文件中
这个secret可以去https://jwt.io/网站生成
上边选择加密方法,下边输入你想加密的信息,左侧会显示出加密后的信息。
package com.lzl.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.lzl.pojo.User;
import com.lzl.service.UserService;
import com.lzl.utils.JwtHelper;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* --效率,是成功的核心关键--
*
* @Author lzl
* @Date 2023/3/21 08:21
*/
@RestController
@RequestMapping("/login")
public class LoginController {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.signature-algorithm}")
private String algorithm;
@Autowired
private UserService service;
@RequestMapping("/verify")
public Map<String,Object> loginVerify(User user){
Map<String, Object> map = new HashMap<>();
//去库中查询
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username",user.getUsername())
.eq("password",user.getPassword());
User one = service.getOne(wrapper);
if(one != null){
map.put("username",one.getUsername());
//登录成功,生成token
String token = JwtHelper.jwsWithHS(
SignatureAlgorithm.forName(algorithm),
user.getUsername(),
3600,
secret
);
map.put("token",token);
}
return map;
}
}
package com.lzl.controller;
import com.lzl.pojo.User;
import com.lzl.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
*
* 前端控制器
*
*
* @author zhenLong
* @since 2023-03-20
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService service;
@RequestMapping("/getAll")
public List<User> getUser(){
return service.list();
}
}
package com.lzl.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* --效率,是成功的核心关键--
* 拦截器,用于处理跨域请求
*
* @Author lzl
* @Date 2022/10/1 10:20
*/
public class CrossOriginInterceptor implements HandlerInterceptor {
//主要逻辑:在handler之前执行:抽取handler中的冗余代码
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
String origin = request.getHeader("Origin");
// 允许的跨域
response.setHeader("Access-Control-Allow-Origin",origin);
// 允许携带Cookie
response.setHeader("Access-Control-Allow-Credentials","true");
// 允许的请求头 预检请求需要这个设置
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers,Authorization,Access-Token,token");
response.setHeader("Access-Control-Expose-Headers", "*");//响应客户端的头部 允许携带Token 等等
response.setHeader("Access-Control-Max-Age", "3600"); // 预检请求的结果缓存时间
if (request.getMethod().equals("OPTIONS")){
return false;
}
return true;
}
//在handler之后执行:进一步的响应定制
@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 {
}
}
package com.lzl.interceptor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
* --效率,是成功的核心关键--
* token验证拦截器
* @Author lzl
* @Date 2023/3/21 08:19
*/
public class VerifyTokenInterceptor implements HandlerInterceptor {
//在controller之前执行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
/*
请求走到这,证明是非login方法(login方法已经被放行),验证token是否存在,不存在直接拦截
存在?验证是否有效,有效放行,无效拦截
*/
if (token == null){
response.setStatus(500);
Map<String, Object> map = new HashMap<>();
response.setContentType("application/json;charset=utf-8");
//is null or token unavailable
map.put("msg","登陆状态已过期");
map.put("code",500);
response.getWriter().write(new ObjectMapper().writeValueAsString(map));
return false;
}
return true;
}
}
在springboot中配置拦截器需要对拦截器进行注册
package com.lzl.config;
import com.lzl.interceptor.CrossOriginInterceptor;
import com.lzl.interceptor.VerifyTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* --效率,是成功的核心关键--
* 拦截器配置
*
* @Author lzl
* @Date 2023/1/26 14:12
*/
@Configuration
public class GlobalInterceptorConfig implements WebMvcConfigurer {
@Override //拦截器配置
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CrossOriginInterceptor()) //拦截器跨域注册对象
.addPathPatterns("/**"); //指定要拦截的请求
registry.addInterceptor(new VerifyTokenInterceptor())//token验证拦截器
.addPathPatterns("/**")
.excludePathPatterns("/login/*"); //排除请求
}
}
登陆状态验证的业务逻辑很简单,主要流程如下:
首先,用户没有登陆的情况下,用户无法访问除了login路径下的接口,以外的任何一个接口,访问的时候直接将页面重定向到登录页,若是前后端分离项目,则给前端返回错误状态码,让前端将用户跳转到登陆页。用户通过login接口输入正确的帐号和密码,后端生成一个token,并返回给前端,可以设置在请求头,或者以参数的形式传递,当前端拿到这个token时就可以访问登陆以外的页面了。
当没有登陆状态时
访问login方法登陆,传回来一个token
携带token访问后端方法
这里只是对token简单的使用,在微服务架构中,token一般用于单点登陆验证,即登陆完成后,将token传到redis中存储,当访问除了登陆以外的其它服务时,去redis中查找。