SpringBoot - 登录
会话技术
登录流程:
会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一起会话中包含多次请求和响应
会话跟踪: 一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据
会话跟踪方案:
- 客户端会话技术:Cookie
- 服务端会话跟踪技术: Session
- 令牌技术
传统方案:
为什么session 不适应于集群:
令牌技术:
Session 案例演示
服务端代码:
package com.chuangzhou.controller;
import com.chuangzhou.pojo.Result;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.net.http.HttpRequest;
@Slf4j
@RestController
public class TestController {
//往session 中存储值
@GetMapping("/s1")
public Result session1(HttpSession session) {
log.info("HttpSession - s1:{}", session.hashCode());
session.setAttribute("loginUser", "tome");
return Result.success();
}
//从session 中获取值
@GetMapping("/s2")
public Result session2(HttpServletRequest request) {
HttpSession session = request.getSession();
log.info("HttpSession - s2:{}", session.hashCode());
Object loginUser = session.getAttribute("loginUser");
log.info("loginUser:{}",loginUser);
return Result.success();
}
}
第一次访问服务端:
服务端Tomcat容器会判断浏览器是否第一次访问如果是第一次访问就会创建一个Session对象,并通过响应头的 Set-Cookie 将session 的id 返回给浏览器
第二次访问:
服务端输出:
2023-08-22T22:19:15.928+08:00 INFO 13696 --- [nio-8080-exec-1] c.chuangzhou.controller.TestController : HttpSession - s1:1010831818
2023-08-22T22:19:22.155+08:00 INFO 13696 --- [nio-8080-exec-2] c.chuangzhou.controller.TestController : HttpSession - s2:1010831818
2023-08-22T22:19:22.155+08:00 INFO 13696 --- [nio-8080-exec-2] c.chuangzhou.controller.TestController : loginUser:tome
2023-08-22T22:20:38.941+08:00 INFO 13696 --- [nio-8080-exec-5] c.chuangzhou.controller.TestController : HttpSession - s1:1010831818
JWT 令牌
JWT: JSON WEB Tokenks。定义了一种简洁的、自包含的格式,用于在通信双方以JSON数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠
组成:
- Header: 记录令牌类型、签名算法等。e.g:
- Payload:携带一些自定义信息、默认信息等。e.g:
- Signature: 防止Token被篡改、确保安全性。将header、payload合并然后加入指定密钥,通过指定签名算法计算而来
令牌生成代码:
@Test
public void testGenJWT(){
Map<String, Object> map = new HashMap<>();
map.put("id",1);
map.put("username","Tome");
String jwts = Jwts.builder() //SignatureAlgorithm.HS256:签名算法。chuangzhou: 密钥
.signWith(SignatureAlgorithm.HS256, "chuangzhou")
.setClaims(map) //载荷
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) //设置密钥的有效期为一个小时
.compact();
System.out.println(jwts);
}
eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjkyODA1MTM0LCJ1c2VybmFtZSI6IlRvbWUifQ.Uc2KYmPSo8jpK_0aRx2yeatLri0BZ-txl9HnVpox4Oc
可以在官网进行解码:
其中前两个部分也就是header 和 payload 部分是 BASE64 编码后,解码后就可以看到内容:
解析令牌:
@Test
public void testparseJwt(){
Claims chuangzhou = Jwts.parser()
.setSigningKey("chuangzhou")
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjkyODA1MTM0LCJ1c2VybmFtZSI6IlRvbWUifQ.Uc2KYmPSo8jpK_0aRx2yeatLri0BZ-txl9HnVpox4Oc")
.getBody();
System.out.println(chuangzhou);
}
解析出来的内容就为载荷:
{id=1, exp=1692805134, username=Tome}
exp 为令牌的有效期
注意事项:
- JWT校验时使用的签名密钥,必须和生成JWT令牌时使用的密钥是配套的
- 如果JWT令牌解析校验时报错,则说明JWT令牌被篡改或失效了,令牌非法
登录案例
引入JWT 工具类:
package com.chuangzhou.utils;
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 = "chuangzhou"; //密钥
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:
package com.chuangzhou.controller;
import com.chuangzhou.pojo.Emp;
import com.chuangzhou.pojo.Result;
import com.chuangzhou.serivce.EmpService;
import com.chuangzhou.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){
Emp e = empService.getEepByUsernamePassword(emp);
if (e != null){
//登录成功
//1.设置载荷
Map<String,Object> map = new HashMap<>();
map.put("id",e.getId());
map.put("username",e.getUsername());
map.put("name",e.getName());
//2.生成令牌
String jwt = JwtUtils.generateJwt(map);
//3.返回jwt令牌
return Result.success(jwt);
}
return Result.error("账号或密码错误");
}
}
接口成功返回jwt:
前端会将token 存储在浏览器的本地存储空间:
之后的请求前端需要将token 放入到 请求头 以便后端 解析校验
Filter
概念:Filter 过滤器,是JavaWeb 三大组件(Servlet、Filter、Litener) 之一
- 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能
- 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等
Springboot 中的使用步骤:
- Filter 实现类: 标注 @WebFilter
package com.chuangzhou.filter;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import java.io.IOException;
@WebFilter(urlPatterns = "/*") // 拦截所有请求
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("放行前 doFilter 执行逻辑");
filterChain.doFilter(servletRequest,servletResponse); // 放行请求
System.out.println("放行后 doFilter 执行逻辑");
}
@Override
public void destroy() { //项目结束时执行
System.out.println("destroy 方法执行了");
}
}
- 启动类上使用:@ServletComponentScan
@SpringBootApplication
@ServletComponentScan
public class BliasWebManagerApplication {
public static void main(String[] args) {
SpringApplication.run(BliasWebManagerApplication.class, args);
}
}
执行流程:
总结:放行后的资源会重新进入Fileter,执行 放行后的逻辑
拦截路径常见用法:
过滤器链
介绍:一个web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链
顺序:注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序。
利用Filter 实现登录校验案例
package com.chuangzhou.filter;
import com.alibaba.fastjson.JSONObject;
import com.chuangzhou.pojo.Result;
import com.chuangzhou.utils.JwtUtils;
import jakarta.servlet.*;
import jakarta.servlet.annotation.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.io.IOException;
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
//1. 判断是否是登录请求
StringBuffer requestURL = req.getRequestURL();
log.info("请求路径为:{}",requestURL);
//2.如果是登录请求放行
if (requestURL.toString().contains("login")){
chain.doFilter(request,response);
return;
}
//3.如果不是登录请求判断请求头是否有token
String token = req.getHeader("token");
if (!StringUtils.hasLength(token)){
log.info("请求未携带token:{}",token);
//4.如果没有返回错误
Result error = Result.error("NOT_LOGIN");
String respStr = JSONObject.toJSONString(error);
resp.getWriter().write(respStr);
return;
}
//5.如果有解析令牌
try{
JwtUtils.parseJWT(token);
}catch (Exception e){ //解析失败
e.printStackTrace();
log.info("解析令牌失败,返回未登录错误信息");
Result not_log = Result.error("NOT_LOG");
String s = JSONObject.toJSONString(not_log);
resp.getWriter().write(s);
return;
}
//6.解析令牌成功放行
log.info("令牌合法,放行");
chain.doFilter(request,response);
}
}
拦截器
入门案例
介绍: 是一种动态拦截方法调用的机制,类似于过滤器。Spring 框架中提供的,用来动态拦截控制器方法的执行。
作用: 拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码
- 编写拦截器:
package com.chuangzhou.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Component
public class CheckLoginInterceptor implements HandlerInterceptor {
@Override // controller 执行之前执行,return true:放行。 返回false 不放行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle ...");
return true;
}
@Override // controller 执行之后执行
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.chuangzhou.config;
import com.chuangzhou.interceptor.CheckLoginInterceptor;
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 CheckLoginInterceptor checkLoginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(checkLoginInterceptor).addPathPatterns("/**");
}
}
拦截路径配置
@Override
public void addInterceptors(InterceptorRegistry registry) {
// excludePathPatterns:指定了不拦截的请求
// /*: 可以拦截/depts、/emps, 但是无法拦截 /depts/1
registry.addInterceptor(checkLoginInterceptor).addPathPatterns("/*").excludePathPatterns("/login");
}
具体规则:
拦截器的执行流程 :
当filter 和 inteper 同时存在时:
Filter 和 Intercetor 的不同点:
- 接口规范不同:过滤器需要实现Filter 接口,而拦截器需要实现HandlerIntercetor 接口
- 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor 只会拦截Spring 环境中的资源
本文来自博客园,作者:chuangzhou,转载请注明原文链接:https://www.cnblogs.com/czzz/p/17644264.html