登录验证是开发过程中最重要的功能,在此总结一下登陆验证的方法,在开发过程中最常用也最安全的方法是JWT令牌验证的方法,本篇文章将会着重讲解JWT令牌验证的详细流程。
一、会话技术
会话:
用户打开浏览器访问web服务器,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。
会话跟踪:
一种维护浏览器状态的方法,服务器需要识别多次请求是否来自同一浏览器,以便再同一次会话的多次请求间共享数据。
因为HTTP协议是无状态的,每次向服务器发送的请求时独立的,服务器会将每次的请求视为新的请求,所以需要会话跟踪技术来实现会话内数据共享。
会话跟踪技术有两种:客户端会话跟踪技术以及服务器端会话跟踪技术。登陆验证一般使用客户端会话跟踪技术。
Cookie
@RestController
@Slf4j
@Api("用户登录")
@RequestMapping("/user/login")
public class LoginController {
//设置Cookie
@GetMapping("/1")
public Result cookie1(HttpServletResponse response){
//设置Cookie
response.addCookie(new Cookie("name","value1"));
return Result.success();
}
//获取Cookie
@GetMapping("/2")
public Result cookie2(HttpServletRequest request){
//获取所有的Cookie
Cookie[] cookies = request.getCookies();
for(Cookie cookie: cookies){
if(cookie.getName().equals("name")){
System.out.println("name"+cookie.getValue());
}
}
return Result.success();
}
}
在浏览器中发送请求1
便可看到响应头中有了Set-Cookie 值为name = value1并将此值存储在浏览器中,在下次发送请求时加入到请求头中。
再发送请求2
便可看到请求头中有了Cookie对应的值。
Cookie技术的优缺点
优点:
HTTP协议中支持的技术
缺点:
移动端APP无法使用Cookie
不安全,用户可以自己禁止Cookie
Cookie不能跨域
跨域区分为三个维度:协议、IP/域名、端口,前后端分离时跨域
Session
Session技术是基于Cookie技术实现的
@RestController
@Slf4j
@Api("用户登录")
@RequestMapping("/user/login")
public class LoginController {
/**
* 向HttpSession中存储值
* @param httpSession
* @return
*/
@GetMapping("/1")
public Result session1(HttpSession httpSession){
//向session中存储数据
httpSession.setAttribute("user","value1");
return Result.success();
}
/**
* 从HttpSession中获取值
* @param httpServletRequest
* @return
*/
@GetMapping("/2")
public Result session2(HttpServletRequest httpServletRequest){
HttpSession session = httpServletRequest.getSession();
//获取数据
Object login = session.getAttribute("user");
System.out.println(login);
return Result.success(login);
}
}
打开浏览器发送请求1
此时便可得到Set-Cookie 并得到服务器生成的session数据
发送请求2
浏览器已自动将session加入请求头中
Session优缺点
优点
因为session是存储在服务端,比较安全
缺点
现在软件系统不仅仅使用一台服务器,服务器集群环境下无法直接使用Session
具有Cookie的缺点
二、JWT令牌
JWT(JSON Web Token)
JWT令牌技术是一种开放的行业标准(RFC7591),用于实现端到端的安全验证。它提供了一种紧凑且自包含的方式,用于在各方之间安全地传输信息。JWT主要由三部分组成:头部(Header)、负载(Payload)和签名(Signature)。由于数字签名的存在,这些信息是可靠的。所以JWT令牌的应用场景主要是登陆验证。
JWT令牌的生成
要使用JWT令牌技术首先需要引入JWT的依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
引入依赖后方可使用JWT技术提供的API来实现登录验证功能。
在项目开发过程中一般会将生成JWT令牌与解析JWT令牌写为工具类
工具类
import io.jsonwebtoken.*;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtUtil {
private static final String SECRET_KEY = "mySecretKey"; // 你的密钥,请保持私密
private static final long JWT_EXPIRATION = 86400000; // 24小时
//将用户id作为参数传进来作为自定义内容
public static String createToken(String userId) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
//设置过期时间
Date expiration = new Date(System.currentTimeMillis() + JWT_EXPIRATION);
//返回生成的令牌
return Jwts.builder()
.setClaims(claims)
.setExpiration(expiration)
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
//解析令牌
public static Claims parseToken(String token) {
try {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
// JWT已过期
e.printStackTrace();
} catch (UnsupportedJwtException e) {
// 不支持的JWT
e.printStackTrace();
} catch (MalformedJwtException e) {
// JWT格式错误
e.printStackTrace();
} catch (SignatureException e) {
// JWT签名错误
e.printStackTrace();
} catch (IllegalArgumentException e) {
// JWT字符串为空
e.printStackTrace();
}
return null;
}
}
登录功能案例
@RestController
@Slf4j
@Api(tags = "用户登录")
@RequestMapping("/user/login")
public class LoginController {
@Autowired
private UserService userService;
@GetMapping("/login")
@ApiOperation("用户登录验证")
public Result userLogin(UserLoginDto userLoginDto) {
log.info("用户登录:{}",userLoginDto);
User user = userService.login(userLoginDto);
String token = JwtUtil.createToken(user.getId().toString());
UserLoginVo userLoginVo = UserLoginVo.builder()
.id(user.getId())
.account(user.getAccount())
.name(user.getNickname())
.token(token)
.build();
return Result.success(userLoginVo);
}
}
解析令牌一般使用拦截器拦截所有动态请求,从请求头中获取令牌进行校验,一般使用有两种方法,一种是过滤器Filter,一种是拦截器Interceptor
JWT令牌优缺点
优点:
- 安全性:JWT使用数字签名或加密机制来验证令牌的真实性,防止伪造和篡改。同时,JWT不需要在服务器上存储会话信息,降低了被攻击的风险。
- 跨平台兼容性:JWT是基于标准的JSON格式,可以在不同的平台和编程语言之间进行交互。即解决了Cookie和Session中不能应用与App端的缺点。
- 分布式和无状态:由于JWT包含了所有必要的信息,服务器不需要在数据库中存储会话信息,也无需在集群中共享会话状态。这使得应用程序可以轻松地进行水平扩展。
缺点:
- 可伪造性:尽管JWT包含签名,但如果密钥不够强大或者不够安全,仍然存在伪造的风险。
- 信息量过大:在某些情况下,JWT包含了很多声明和信息,导致传输的数据量较大。
- 过期时间管理:如果JWT的过期时间设置得太短,可能会导致频繁刷新令牌;而过期时间设置得太长可能会导致安全风险。
- 密钥管理:有效地管理JWT的密钥可能会带来挑战,尤其是在大规模的系统中。
- 无法处理会话管理:JWT是无状态的,因此无法有效处理会话管理,包括注销和跟踪用户状态。
三、过滤器(Filter)
Filter是JavaWeb三大件(Servelt、Filter、Listener)之一,过滤器可以将对资源的请求拦截,实现特殊的功能,过滤器一般完成一些通用功能的操作,比如登录校验、统一编码处理以及敏感字符处理等。
使用Filter需要做到:
1.先定义一个类,实现Filter接口,并重写所有方法。
2.配置Filter:Filter类上加上@WebFilter注解,配置拦截资源路径Springboot项目需要在启动类上加@ServletComponentScan注解开启Servlet组件支持。
为了实现登录校验功能,我们需要编写一个登录校验过滤器,拦截到请求时,如果令牌验证成功再放行,否则直接抛出异常,不需要继续执行代码。
import com.alibaba.fastjson.JSONObject;
import com.result.Result;
import com.utils.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//执行放行前操作
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
//获取请求url
String url = httpServletRequest.getRequestURL().toString();
//判断url中是否包含login,如果包含说明是登录请求,直接放行
if(url.contains("login")){
filterChain.doFilter(servletRequest,servletResponse);
return;//登陆后不需要执行外面代码,需要直接返回
}
//获取请求头中的令牌进行校验
String jwt = httpServletRequest.getHeader("token");
//未登录
if(!StringUtils.hasLength(jwt)){
log.info("请求头为空,返回未登录信息");
Result error = Result.error("未登录");
//需要返回json格式的数据返回给浏览器
String notLogin = JSONObject.toJSONString(error);
httpServletResponse.getWriter().write(notLogin);//将错误信息返回给浏览器
return;
}
//解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtil.parseToken(jwt);
} catch (Exception e) {
e.printStackTrace();
log.info("解析令牌失败,返回未登录信息");
Result error = Result.error("未登录");
//需要返回json格式的数据返回给浏览器
String notLogin = JSONObject.toJSONString(error);
httpServletResponse.getWriter().write(notLogin);//将错误信息返回给浏览器
return;
}
//放行
log.info("令牌合法,放行");
filterChain.doFilter(servletRequest,servletResponse);
}
}
需要注意的一点是过滤器拦截到请求之后执行完放行前操作后必须执行放行操作,否则不会访问到数据并返回前端请求的数据。放行后访问对应资源还会回到Filter中,直接执行放行后操作,不会执行放行前操作。
拦截路径:
1.拦截具体的路径(“/login”),只有访问具体的路径时才会被拦截
2.目录拦截(“/user/*") 访问user路径下的所有资源时都会被拦截
3.拦截所有("/*"),实例中配置的就是拦截所有,访问所有资源时都会被拦截
四、拦截器(Interceptor)
拦截器是一种动态拦截方法调用的机制,类似于过滤器,只不过拦截器是spring框架中提供的,用来动态拦截控制器方法的执行,它会根据拦截请求在指定的方法调用前后,根据业务需要执行预先设定的代码。
使用拦截器步骤和过滤器比较相似
1.定义拦截器类,实现HandlerInterceptor接口,并重写其所有方法,因为其中的方法都是默认实现的,所以只需要重写我们需要的方法即可。
其中有三个方法,preHandle():目标资源方法执行前放行,
postHandle()目标资源方法执行后执行,
afterCompletion()视图渲染完毕后执行,最后执行,
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler) throws Exception {
//获取请求url
String url = httpServletRequest.getRequestURL().toString();
//判断url中是否包含login,如果包含说明是登录请求,直接放行
if(url.contains("login")){
log.info("登录操作,放行...");
return true;
}
//获取请求头中的令牌进行校验
String jwt = httpServletRequest.getHeader("token");
//未登录
if(!StringUtils.hasLength(jwt)){
log.info("请求头为空,返回未登录信息");
Result error = Result.error("未登录");
//需要返回json格式的数据返回给浏览器
String notLogin = JSONObject.toJSONString(error);
httpServletResponse.getWriter().write(notLogin);//将错误信息返回给浏览器
return false;
}
//解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtil.parseToken(jwt);
} catch (Exception e) {
e.printStackTrace();
log.info("解析令牌失败,返回未登录信息");
Result error = Result.error("未登录");
//需要返回json格式的数据返回给浏览器
String notLogin = JSONObject.toJSONString(error);
httpServletResponse.getWriter().write(notLogin);//将错误信息返回给浏览器
return false;
}
//放行
log.info("令牌合法,放行");
return true;
}
}
}
2.配置拦截器
定义WebConfig类实现WebMvcConfigurer接口
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
/**
* 注册自定义拦截器
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
registry.addInterceptor(LoginCheckInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/user/login");
}
}