标签:令牌 第十二天 JavaWeb JWT 笔记 Filter Result import 拦截
SpringBootWeb案例(三)
登录功能
import com.zgg1h.pojo.Emp;
import com.zgg1h.pojo.Result;
import com.zgg1h.service.EmpService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/login")
public class LoginController {
@Autowired
private EmpService empService;
@PostMapping
public Result login(@RequestBody Emp emp) {
log.info("员工登录:{}", emp);
Emp e = empService.login(emp);
return e == null ? Result.error("用户名或密码错误") : Result.success();
}
}
Emp login(Emp emp);
@Override
public Emp login(Emp emp) {
return empMapper.getByUsernameAndPassword(emp);
}
@Select("select * from emp where username = #{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);
登录校验
在之前的程序中,用户不管有没有登陆,都可以直接进行业务操作,可以用以下技术来进行登录校验。
- 登陆标记(会话技术,JWT令牌):当用户登陆成功后,给用户打上一个标记,之后的每一次请求中,都可以获取到该标记。
- 统一拦截(过滤器Filter,拦截器Interceptor):在用户要进行业务操作时,先校验登陆标记,校验成功则进入正常业务执行,校验失败则给前端相应一个错误信息,前端页面再跳转到登录页面。
会话技术
- 会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。
- 会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。
- 会话跟踪方案:
- 客户端会话跟踪技术:Cookie
- 服务端会话跟踪技术:Session
- 令牌技术
会话跟踪方案对比
Cookie
执行流程
- 浏览器第一次向服务器发送请求后,服务器会在返回响应时自动加上Cookie。
- 浏览器接收到这次响应后,会将Cookie自动保存在本地。
- 之后,浏览器的每一次请求都会自动带上Cookie,这就实现了会话跟踪。
优点
缺点
- 移动端APP无法使用Cookie。
- 不安全,因为Cookie数据会保存在浏览器本地;且用户可以自己禁用Cookie。
- Cookie不能跨域。(跨域区分三个维度:协议、IP/域名、端口,三者之一有不同即为跨域)
Session
执行流程
- 浏览器第一次向服务器发送请求后,服务器会在本地创建一个会话对象Session,每一个Session对象都有一个id。服务器在返回响应时会将Session id通过Cookie相应给浏览器。
- 之后的流程与Cookie一致。
优点
缺点
- 服务器集群环境下无法直接使用Session。
- Cookie的其他缺点依然存在。
令牌技术
执行流程
- 浏览器第一次向服务器发送请求后,服务器会创建一个令牌并下发给浏览器。
- 之后,浏览器的每一次请求都会带上令牌,服务器收到令牌后,会统一拦截校验并校验令牌的有效性,如果有效,则放行,反之则拦截。
优点
- 支持PC端、移动端。
- 解决集群环境下的认证问题。
- 减轻服务器端存储压力。
缺点
JWT令牌
- JWT全称:JSON Web Token。
- JWT定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
- JWT令牌就是一段字符串,由标头Header、有效载荷(Payload)和签名(Signature)这三部分组成,用 . 拼接。在传输的时候,会将JWT的前两个部分分别进行Base64编码后用 . 进行连接,形成最终传输的字符串。
- 组成:
- 第一部分:Header(标头), 记录令牌类型、签名算法等。
- 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。
- 第三部分:Signature(签名),防止Token被篡改、确保安全性。融入header、payload,并加入指定秘钥,通过指定签名算法计算而来。
- Base64:是一种基于64个可打印字符(A-Z a-z 0-9 + /)来表示二进制数据的编码方式。(还有一个占位符=)
执行流程
- 登录成功后,生成令牌。
- 后续每个请求,都要携带JWT令牌,系统在每次请求处理之前,先校验令牌,通过后,再处理请求,否则拒绝处理请求。
JWT令牌的使用
引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
生成JWT
String jwt = Jwts.builder()
.setClaims(Map对象) //自定义内容(载荷)
.signWith(签名算法名, 密钥字符串) //签名算法
.setExpiration(Date对象) //有效期
.compact(); //压缩生成JWT
校验JWT
Claims claims = Jwts.parser()
.setSigningKey(密钥字符串) //指定签名秘钥
.parseClaimsJws(JWT令牌) //解析令牌
.getBody() //获取Map对象封装的令牌内容
注意事项
- JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的。
- 如果JWT令牌解析校验时报错,则说明 JWT令牌被篡改或失效了,令牌非法。
登录功能-生成令牌
@PostMapping
public Result login(@RequestBody Emp emp) {
log.info("员工登录:{}", emp);
Emp e = empService.login(emp);
if (e != null) {
Map<String, Object> claims = new HashMap<>();
claims.put("id", e.getId());
claims.put("username", e.getUsername());
claims.put("name", e.getName());
String jwt = JwtUtils.generateJwt(claims);
return Result.success(jwt);
}
return Result.error("用户名或密码错误");
}
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 = "zgg1h"; //密钥
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;
}
}
过滤器Filter
- Filter过滤器是 JavaWeb 三大组件(Servlet、Filter、Listener)之一。
- 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。
- 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。
使用步骤
- 定义Filter:定义一个类,实现 Filter 接口,并重写其所有方法。(init,doFilter,destroy,主要是doFilter)
- 配置Filter:Filter类上加 @WebFilter 注解,配置拦截资源的路径。由于Filter不是springboot的组件,所以还要在引导类上加上@ServletComponentScan以开启Servlet组件支持。
几点注意
方法 |
说明 |
init |
初始化方法,Web服务器启动,创建Filter时调用,只调用一次 |
doFilter |
拦截到请求时,调用该方法,可调用多次 |
destroy |
销毁方法,服务器关闭时调用,只调用一次 |
注解 |
说明 |
@WebFilter(urlPatterns="拦截路径") |
加在Filter类上,用于配置拦截资源的路径 |
@ServletComponentScan |
加在springboot引导类上,用于开启Servlet组件支持 |
执行流程
- 拦截请求。
- 执行放行前逻辑。
- 执行放行操作。
- 访问对应的web资源。
- 执行放行后逻辑。
拦截路径
- Filter 可以根据需求,配置不同的拦截资源路径:
拦截路径 |
urlPatterns值 |
含义 |
拦截具体路径 |
/login |
只有访问/login路径时,才会被拦截 |
目录拦截 |
/emps/* |
访问/emps下的所有资源,都会被拦截 |
拦截所有 |
/* |
访问所有资源,都会被拦截 |
过滤器链
- 介绍:一个web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链,放行后操作执行顺序与放行前操作相反。
- 顺序:注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序。
登录校验-Filter
import com.alibaba.fastjson.JSONObject;
import com.zgg1h.pojo.Result;
import com.zgg1h.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 request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
//1.获取请求url。
String url = req.getRequestURL().toString();
log.info("请求的url: {}",url);
//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行。
if(url.contains("login")){
log.info("登录操作, 放行...");
chain.doFilter(request,response);
return;
}
//3.获取请求头中的令牌(token)。
String jwt = req.getHeader("token");
//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)。
if(!StringUtils.hasLength(jwt)){
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
//5.解析token,如果解析失败,返回错误结果(未登录)。
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {//jwt解析失败
e.printStackTrace();
log.info("解析令牌失败, 返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
//6.放行。
log.info("令牌合法, 放行");
chain.doFilter(request, response);
}
}
- SpringbootExampleApplication:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@ServletComponentScan
@SpringBootApplication
public class SpringbootExampleApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootExampleApplication.class, args);
}
}
拦截器Interceptor
- 拦截器Interceptor是一种Spring框架中提供的动态拦截方法调用的机制,类似于过滤器,用来动态拦截控制器方法的执行。
- 作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。
使用步骤
- 定义拦截器,实现HandlerInterceptor接口,加上@Component注解,并按照需求重写其方法。
- 注册拦截器:定义一个配置类实现WebMvcConfigurer接口,加上@Configuration注解,并重写addInterceptors方法。
注意事项
方法 |
说明 |
preHandle |
目标资源方法执行前执行,返回true代表放行,返回false代表不放行 |
postHandle |
目标资源方法执行后执行 |
afterCompletion |
视图渲染完毕后执行,最后执行 |
拦截路径
InterceptorRegistry类的方法 |
说明 |
addPathPatterns("拦截路径") |
需要拦截哪些资源 |
excludePathPatterns("不拦截路径") |
不需要拦截哪些资源 |
拦截路径 |
含义 |
举例 |
/* |
一级路径 |
能匹配/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 |
执行流程
- 由于Interceptor是spring框架提供的,Tomcat无法识别,所以外部请求先进入DispatcherServlet,再由DispatcherServlet转给Interceptor。
- 请求进入Interceptor后,先执行preHandle逻辑,并根据执行结果决定是否放行。
- 如果放行,则访问对应的web资源。
- 然后执行postHandle逻辑。
- 最后执行afterCompletion逻辑。
- 如果同时使用Filter和Interceptor,则外部请求在进入DispatcherServlet前先被Filter拦截,并执行放行前逻辑,若放行,则外部请求进入DispatcherServlet,执行第1步,在第5步执行完后又回到Filter执行放行后逻辑。
Filter和Interceptor的区别
区别 |
说明 |
接口规范不同 |
过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口 |
拦截范围不同 |
过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源 |
登录校验- Interceptor
import com.alibaba.fastjson.JSONObject;
import com.zgg1h.pojo.Result;
import com.zgg1h.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.获取请求url。
String url = request.getRequestURL().toString();
log.info("请求的url: {}",url);
//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行。
if(url.contains("login")){
log.info("登录操作, 放行...");
return true;
}
//3.获取请求头中的令牌(token)。
String jwt = request.getHeader("token");
//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)。
if(!StringUtils.hasLength(jwt)){
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return false;
}
//5.解析token,如果解析失败,返回错误结果(未登录)。
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {//jwt解析失败
e.printStackTrace();
log.info("解析令牌失败, 返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return false;
}
//6.放行。
log.info("令牌合法, 放行");
return true;
}
}
import com.zgg1h.interceptor.LoginCheckInterceptor;
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 LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
}
}
异常处理
当出现异常时,默认返回的结果不符合规范,前端无法解析。
解决方案
方案 |
说明 |
评价 |
方案一 |
在Controller的方法中进行try…catch处理 |
代码臃肿,不推荐 |
方案二 |
使用全局异常处理器 |
简单、优雅,推荐 |
全局异常处理器
定义一个异常处理类,加上@RestControllerAdvice注解。在内部定义处理异常的方法,并加上@ExceptionHandler(异常类型)注解。
import com.zgg1h.pojo.Result;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result ex(Exception ex){
ex.printStackTrace();
return Result.error(" 对不起,操作失败,请联系管理员 ");
}
}
- @RestControllerAdvice = @ControllerAdvice + @ResponseBody,所以方法ex可以把对象封装成json格式返回。
标签:令牌,
第十二天,
JavaWeb,
JWT,
笔记,
Filter,
Result,
import,
拦截
From: https://www.cnblogs.com/zgg1h/p/18110342