一、登录校验
- 问题:在未登录情况下,我们也可以直接访问部门管理、员工管理等功能。
- 由于浏览器与web服务器中的数据交互是通过HTTP协议的,而HTTP协议是无状态的–即每个页面中的请求和响应都是独立的,没有状态存在。
- 所以我们需要进行登录校验:
1.登录校验
- 每次访问页面的时候可以用
if...else...
来进行判断用户是否登录。但过程较为繁琐,所以我们设置统一拦截。
二、统一拦截
1.登录标记
(1).会话技术:
- 用户登录成功之后,每一次请求中,都可以获取到该标记。
- 会话:
- 用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束在一次会话中可以包含多次请求和响应。
- 会话跟踪:
- 一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。
(2).会话跟踪方案:
- 客户端Cookie(传统)
- 客户端会话跟踪技术:
Cookie
。
import com.mannor.pojo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@RestController
public class SessionController {
//设置cookie
@GetMapping("/c1")
public Result cookie(HttpServletResponse response) {
response.addCookie(new Cookie("login_username", "mannor"));//设置Cookie/响应Cookie
return Result.success();
}
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();//获取所有的Cookie
for (Cookie cookie : cookies) {
if (cookie.getName().equals("login_username")) {//输出name为 login_username 的cookie
System.out.println("login_username: " + cookie.getValue());
}
}
return Result.success();
}
}
优点:HTTP协议中支持
缺点:1.移动端APP无法使用cookie不安全,2.用户可以自己禁用,3.Cookiecookie不能跨域。
- 服务端Session(传统)
- 服务端会话跟踪技术:
Session
,基于cookie开发
import com.mannor.pojo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Slf4j
@RestController
public class SessionController {
//往HttpSession中存储值
@GetMapping("/s1")
public Result session1(HttpSession session) {
log.info("attpSession-s1: {}", session.hashCode());
session.setAttribute("loginUser", "tom");//往session中存储数据
return Result.success();
}
//从HttpSession中获取值
@GetMapping("/s2")
public Result session2(HttpServletRequest request) {
HttpSession session = request.getSession();
log.info("HttpSession-s2:{}", session.hashCode());
Object loginUser = session.getAttribute("loginUser");//从session中获取数据
log.info("loginUser: {}", loginUser);
return Result.success(loginUser);
}
}
优点:存储在服务器,安全性高
缺点:1.在服务器集群的情况下无法直接使用Session; 2.Cookie的缺点(基于Cookie开发)。
- JWT令牌技术 (主流)
//具体相关内容在下详细介绍
优点:1.支持PC端和移动端 2.解决了集群环境下的认证问题 3.减轻了服务器端存储的压力(不用存储)。
缺点:需要自己实现。
(3). JWT令牌技术
- 简介:
- 全称:JSON Web Token 官网:https://jwt.io/
- 定义了一种简洁的、自包含的格式,用于在通信双方以
json
数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
- 组成:
- 第一部分: Header(头),记录令牌类型、签名算法等。例如:
{"alg":"HS256", "type":"JwT"}
- 第二部分: Payload(有效载荷),携带一些自定义信息、默认信息等。例如:
{"id":"1","username":"Tom"}
- 第三部分: Signature(签名),防止
Token
被篡改、确保安全性。将header
、payload
,并加入指定秘钥,通过指定签名算法计算而来。
生成JWT令牌时要对JSON格式的数据进行Base64编码:一种基于64个可打印字符(A-Z a-Z 0-9 + /)来表示二进制数据的编码方式。
//原始的JWT令牌:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
//1.HEADER:ALGORITHM & TOKEN TYPE (头)
{
"alg": "HS256",
"typ": "JWT"
}
//2.PAYLOAD:DATA (有效载荷)
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
//3.VERIFY SIGNATURE (数字签名)--通过前面的数据,执行签名算法
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
)
- 应用场景
- 场景:登录认证。
- 登录成功后,生成令牌
- 后续每个请求,都要携带JWT令牌,系统在每次请求处理之前,先校验令牌,通过后,再处理
- JWT-生成
- 首先需要引入
pom.xml
依赖:
<!-- JWT令牌-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
- 创建一个测试类
/**
* 生成JWT令牌
*/
@Test
public void testGenJwt() {
//利用Map集合来整合需要存储的数据
Map<String, Object> claims = new HashMap<>();
claims.put("id", 1);
claims.put("name", "tom");
//链式编程
String jwt = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, "mannor")//签名算法 解析密钥
.setClaims(claims)//自定义内容(载荷)
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))//设置有效期为1h
.compact();
System.out.println(jwt);
//控制台输出:eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTY4NzQ4OTkxOH0.FmNSF7KDVZ9SaGoVJb6jNDq_oqlgSeUcFBskGNgy0UE
}
- 把输出的JWT令牌发放到官网的Encoded下可以看见原始数据
- JWT-解析
/**
* 解析JwT
*/
@Test
public void testParseJwt() {
Claims claims = Jwts.parser()
.setSigningKey("mannor") //解析密钥
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTY4NzQ5MTIwN30.Tjzr4CKzwJcGtYwiRCo7d71z6WNugMMefu09hUp5H3o")
.getBody();
System.out.println(claims);
}
- JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的。
- 如果JWT令牌解析校验时报错,则说明JWT令牌被篡改或失效了,令牌非法。
(4).JWT的登录校验实现(登录-生成令牌)
- 步骤:
- 引入JWT令牌操作工具类。
- 登录完成后,调用工具类生成JWT令牌,并返回。
- 编写一个Util工具类:
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtils {
private static String signKey = "mannor"; //登录令牌
private static Long expire = 43200000L; //十二个小时后过期
/**
* 生成JWT令牌
* @param claims JWT第二部分负载 payload 中存储的内容
* @return
*/
public static String generateJwt(Map<String, Object> claims){
String jwt = Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, signKey)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
return jwt;
}
/**
* 解析JWT令牌
* @param jwt JWT令牌
* @return JWT第二部分负载 payload 中存储的内容
*/
public static Claims parseJWT(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
- 重新编写Controller层代码:
import com.mannor.Service.EmpService;
import com.mannor.pojo.Emp;
import com.mannor.pojo.Result;
import com.mannor.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp) {
log.info("登录的查询参数:{},{}", emp.getUsername(), emp.getPassword());
Emp e = empService.loginSelect(emp);
//登陆成功生成令牌,下发令牌
if (e != null) {
Map<String, Object> claim = new HashMap<>(); //存如我们需要的信息,ID,name、username
claim.put("id", e.getId());
claim.put("name", e.getName());
claim.put("username", e.getUsername());
String jwt = JwtUtils.generateJwt(claim);//jwt中包含了员工当前的登录信息
return Result.success(jwt);
}
//登录失败,返回错误信息
return Result.error("用户名或密码错误,请检查重新输入");
}
}
- 剩下的就需要拦截器来处理
2.统一拦截–过滤器Filter
1. 概述
- 概念: Filter过滤器,是JavaWeb三大组件(Servlet、Filter、Listener)之一。
- 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。
- 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。
2. Filter
的快速入门
- 1.定义
Filter
:定义一个类,实现Filter
接口,并重写其所有方法。
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(urlPatterns = "/*") //这是JavaWeb中的组件,springboot中没有,还需要在启动类上加上@ServletComponentScan注解
public class demoFilter implements Filter {
@Override //初始化、只调用一次 项目启动自动调用
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init初始化方法");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("拦截到了请求");
//拦截放行,在路径资源访问之后
filterChain.doFilter(servletRequest,servletResponse);
}
@Override //销毁方法,也只调用一次 项目结束自动调用
public void destroy() {
System.out.println("destroy初始化方法");
}
}
- 2.配置
Filter
:Filter
类上加@WebFilter
注解,配置拦截资源的路径。启动类上加@ServletComponentScan
开启Servlet
组件支持。
3. Filter
详解
- 1.Filter可以根据需求,配置不同的拦截资源路径:
拦截路径 | urlPatterns值 | 含义 |
---|---|---|
拦截具体路径 | /login | 只有访问/login 路径时,才会被拦截 |
目录拦截 | /emps/* | 访问/emps下的所有资源,都会被拦截 |
拦截所有 | /* | 访问所有资源,都会被拦截 |
4. 过滤器链
- 介绍:一个web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链。
- 执行流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wHREKRhw-1687578908342)(18_files/1.jpg)] - 执行顺序:注解配置的
Filter
,优先级是按照过滤器类名(字符串)的自然排序。
5. 利用Filter过滤器实现登录校验
- 流程
- 获取请求url。
- 判断请求url中是否包含login,如果包含,说明是登录操作,放行。
- 获取请求头中的令牌(token) 。
- 判断令牌是否存在,如果不存在,返回错误结果(未登录)。
- 解析token,如果解析失败,返回错误结果(未登录)。
- 放行。
- 代码实现:(具体细节请看注释)
import com.alibaba.fastjson.JSONObject;
import com.mannor.pojo.Result;
import com.mannor.utils.JwtUtils;
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 doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;
//1. 获取请求url。
String requestURI = req.getRequestURI();
log.info("请求的url:{}", requestURI);
//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行。
if (requestURI.contains("login")) {
log.info("登录操作");
filterChain.doFilter(servletRequest, servletResponse);
return;
}
//3.获取请求头中的令牌(token)。
String jwt = req.getHeader("token");
//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)。
if (!StringUtils.hasLength(jwt)) {
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");//前端需要响应一个json格式的数据
//手动将对象转换为json格式的数据-------》使用阿里巴巴fastJSON的工具包(需要引入依赖)
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);//使用HttpServletResponse对象返回错误的JSON数据
return;
}
//5.解析token(校验jwt令牌),如果解析失败,返回错误结果(未登录)。如果错误会报错,所以使用try...catch...
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {//程序执行到这里说明程序执行失败--jwt令牌错误
e.printStackTrace();
log.info("解析令牌失败,返回未登录的错误信息");
Result error = Result.error("NOT_LOGIN");//前端需要响应一个json格式的数据
//手动将对象转换为json格式的数据-------》使用阿里巴巴fastJSON的工具包(需要引入依赖)
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);//使用HttpServletResponse对象返回错误的JSON数据
return;
}
//6.放行。
log.info("令牌合法,直接放行");
filterChain.doFilter(servletRequest,servletResponse);
}
}
3.统一拦截–拦截器Interceptor
1.概述
- 概念:是一种动态拦截方法调用的机制,类似于过滤器。Spring框架中提供的,用来动态拦截控制器方法的执行。
- 作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。
- 与过滤器Filter的使用差不多。
2.快速入门
- 定义拦截器,实现
HandlerInterceptor
接口,并重写其所有方法。
package com.mannor.Interceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class LoinCheckInterceptor implements HandlerInterceptor {
@Override //目标资源方法运行前运行,返回true:放行;false:不放行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle...运行了");
return true;
}
@Override //目标资源方法运行后运行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle...运行了");
}
@Override //视图渲染完毕后运行,最后运行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion...运行了");
}
}
- 注册配置拦截器
package com.mannor.Config;
import com.mannor.Interceptor.LoinCheckInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration //声明为配置类
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoinCheckInterceptor loinCheckInterceptor;
@Override //用来注册拦截器
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loinCheckInterceptor).addPathPatterns("/**");
}
}
3.拦截路径
- 拦截器可以根据需求,配置不同的拦截路径:
拦截路径 | 含义 | 举例 |
---|---|---|
/* | 一级路径 | 能匹配/depts,/emps,/login,不能匹配/depts/1 |
/** | 任意级路径 | 能匹配/depts,/depts/1 ,/depts/1/2 |
/depts/* | /depts下的一级路径 | 能匹配/depts/1,不能匹配/depts/1/2,/depts |
/depts/** | /depts下的任意级路径 | 能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1 |
addPathPatterns
:拦截的路径。excludePathPatterns
:不拦截的路径。
@Configuration //声明为配置类
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoinCheckInterceptor loinCheckInterceptor;
@Override //用来注册拦截器
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loinCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
}
}
4.拦截器-执行流程
- 过滤器和拦截器的执行先后流程:
5.Filter与Interceptor
- 接口规范不同:过滤器需要实现
Filter
接口,而拦截器需要实现HandlerInterceptor
接口。 - 拦截范围不同:过滤器
Filter
会拦截所有的资源,而Interceptor
只会拦截Spring环境中的资源
6.登录校验的实现–Interceptor
- 步骤(与
Filte
r一样)
- 获取请求url。
- 判断请求url中是否包含login,如果包含,说明是登录操作,放行。
- 获取请求头中的令牌( token) 。
- 判断令牌是否存在,如果不存在,返回错误结果(未登录)。
- 解析token,如果解析失败,返回错误结果(未登录)。
- 放行。
package com.mannor.Interceptor;
import com.alibaba.fastjson.JSONObject;
import com.mannor.pojo.Result;
import com.mannor.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@Component
public class LoinCheckInterceptor implements HandlerInterceptor {
@Override //目标资源方法(Controller方法)运行前运行,返回true:放行;false:不放行
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
System.out.println("preHandle...运行了");
//1. 获取请求url。
String requestURI = req.getRequestURI();
log.info("请求的url:{}", requestURI);
//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行。
if (requestURI.contains("login")) {
log.info("登录操作");
return true;
}
//3.获取请求头中的令牌(token)。
String jwt = req.getHeader("token");
//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)。
if (!StringUtils.hasLength(jwt)) {
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");//前端需要响应一个json格式的数据
//手动将对象转换为json格式的数据-------》使用阿里巴巴fastJSON的工具包(需要引入依赖)
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);//使用HttpServletResponse对象返回错误的JSON数据
return false;
}
//5.解析token(校验jwt令牌),如果解析失败,返回错误结果(未登录)。如果错误会报错,所以使用try...catch...
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {//程序执行到这里说明程序执行失败--jwt令牌错误
e.printStackTrace();
log.info("解析令牌失败,返回未登录的错误信息");
Result error = Result.error("NOT_LOGIN");//前端需要响应一个json格式的数据
//手动将对象转换为json格式的数据-------》使用阿里巴巴fastJSON的工具包(需要引入依赖)
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);//使用HttpServletResponse对象返回错误的JSON数据
return false;
}
//6.放行。
log.info("令牌合法,直接放行");
return true;
}
@Override //目标资源方法运行后运行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle...运行了");
}
@Override //视图渲染完毕后运行,最后运行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion...运行了");
}
}
除此之外,还需要在配置类
WebConfig.java
中使用excludePathPatterns()
方法对css
、vue
,js
等静态资源放行,不然Interceptor会产生拦截