【JavaWeb】SpringBootWeb登录认证
? 本文由 程序喵正在路上 原创,CSDN首发!
? 系列专栏:JavaWeb开发
? 首发时间:2024年3月18日
? 欢迎关注?点赞?收藏?留言?
目录
- 登录功能
- 登录校验 (重点)
- 会话技术
- JWT令牌
- 过滤器Filter
- 拦截器Interceptor
- 异常处理
登录功能
需求
思路
实现
LoginController.java
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp) {
log.info("员工登录,{}", emp);
Emp e = empService.login(emp);
return e != null ? Result.success() : Result.error("用户名或密码错误");
}
}
EmpServiceImpl.java
@Override
public Emp login(Emp emp) {
return empMapper.getByUsernameAndPassword(emp);
}
EmpMapper.java
/**
* 根据用户名和密码查询员工
* @param emp
* @return
*/
@Select("select * from emp where username = #{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);
测试
前后端联调,先退出到登录界面:
失败的情况:
成功的情况:
登录校验 (重点)
问题分析
执行启动类,直接点击 http://localhost:90/#/system/emp 进入,然后我们就访问到了员工管理的页面,但是我们并没有登录,这显然是有问题的,这是因为我们在写查询等功能时没有考虑到去校验用户是否已登录的情况
解决思路
登录标记
- 用户登录成功之后,每一次请求中,都可以获取到该标记
统一拦截
- 过滤器Filter
- 拦截器Interceptor
会话技术
- 会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。
- 会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。
会话跟踪方案:
- 客户端会话跟踪技术:Cookie
- 服务端会话跟踪技术:Session
- 令牌技术
会话跟踪方案对比
JWT令牌
- 全称:JSON Web Token (https://jwt.io/)
- 定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
组成:
- 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:
{"alg":"HS256","type":"JWT"}
- 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:
{"id":"1","username":"Tom"}
- 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。
- 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:
令牌的前两部分通过 Base64 进行编码
- Base64:是一种基于64个可打印字符(A-Z a-z 0-9 + /)来表示二进制数据的编码方式。
典型应用场景:登录认证
- 登录成功后,生成令牌
- 后续每个请求,都要携带JWT令牌,系统在每次请求处理之前,先校验令牌,通过后,再处理
JWT-生成
在测试类中,我们来写 JWT 生成的程序,执行前可以先把 @SpringBootTest
注释掉,这样不用加载整个工程,因为整个测试程序和工程无关
/**
* 生成JWT
*/
@Test
public void testGenJwt() {
Map<String, Object> claims = new HashMap<>();
claims.put("id", 1);
claims.put("name", "Tom");
String jwt = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, "xixi") //签名算法
.setClaims(claims) //自定义内容(载荷)
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) //设置有效期为1小时
.compact();
System.out.println(jwt);
}
JWT-校验
/**
* 校验JWT
*/
@Test
public void testParseJwt() {
Claims claims = Jwts.parser()
.setSigningKey("xixi")
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiVG9tIiwiaWQiOjEsImV4cCI6MTcxMDIyODExNX0.bATiIimrJ_SD2Nbow30ErqBBtEDZgugbv96L0SXJtrs")
.getBody();
System.out.println(claims);
}
注意事项
- JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的。
- 如果JWT令牌解析校验时报错,则说明JWT令牌被篡改或失效了,令牌非法。
登录后下发令牌
令牌生成和校验思路
- 令牌生成:登录成功后,生成JWT令牌,并返回给前端。
- 令牌校验:在请求到达服务端后,对令牌进行统一拦截、校验。
登录-生成令牌步骤
- 引入JWT令牌操作工具类。
- 登录完成后,调用工具类生成JWT令牌,并返回。
工具类 JwtUtils
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 = "xixi";
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;
}
}
修改 LoginController.java
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;
@PostMapping("/login")
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("name", e.getName());
claims.put("username", e.getUsername());
String jwt = JwtUtils.generateJwt(claims);
return Result.success(jwt);
}
//登录失败,返回错误信息
return Result.error("用户名或密码错误");
}
}
测试:
过滤器Filter
- 概念:Filter 过滤器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一
- 过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能
- 过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等
快速入门
定义Filter:定义一个类,实现 Filter 接口,并重写其所有方法。
配置Filter:Filter类上加
@WebFilter
注解,配置拦截资源的路径。引导类上加@ServletComponentScan
开启Servlet组件支持。
详解
Filter执行流程
- 放行后,程序访问对应的资源,资源访问完成后,程序会回到 Filter 中继续执行放行后面的内容
Filter拦截路径
Filter 可以根据需求,配置不同的拦截资源路径:
过滤器链
- 介绍:一个web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链。
- 顺序:注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序。
登录校验-Filter
注意
- 所有的请求,拦截到了之后,除了登录操作外,都需要校验令牌
- 拦截到请求后,有令牌,且令牌校验通过(合法)才可以放行,执行业务操作;否则都返回未登录错误结果
步骤
- 获取请求url。
- 判断请求url中是否包含login,如果包含,说明是登录操作,放行。
- 获取请求头中的令牌(token)。
- 判断令牌是否存在,如果不存在,返回错误结果(未登录)。
- 解析token,如果解析失败,返回错误结果(未登录)。
- 放行。
创建一个 LoginCheckFilter.java
import com.alibaba.fastjson.JSONObject;
import com.xixi.pojo.Result;
import com.xixi.utils.JwtUtils;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
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 servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;
//获取请求url
String url = req.getRequestURL().toString();
log.info("请求的url:{}", url);
//判断请求url中是否包含login,如果包含,说明是登录操作,放行
if (url.contains("login")) {
log.info("登录操作,放行...");
filterChain.doFilter(servletRequest, servletResponse);
return;
}
//获取请求头中的令牌(token)
String jwt = req.getHeader("token");
//判断令牌是否存在,如果不存在,返回错误结果(未登录)
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;
}
//解析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;
}
//放行
log.info("令牌合法,放行");
filterChain.doFilter(servletRequest, servletResponse);
}
}
需要的依赖:
<!-- fastJSON-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
这时候,除了登录外的请求,我们都无法访问到信息:
先登录获取令牌:
发起其它请求时在请求头中将获取到的令牌传递过去,这样才能成功:
拦截器Interceptor
简介
- 概念:是一种动态拦截方法调用的机制,类似于过滤器。Spring框架中提供的,用来动态拦截控制器方法的执行。
- 作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。
快速入门
定义拦截器,实现HandlerInterceptor接口,并重写其所有方法
创建包 interceptor,在包下创建类 LoginCheckInterceptor,按住 Ctrl + O 可以选择实现接口的方法:
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.stereotype.Component;
@Component
public class LoginCheckInterceptor 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...");
}
}
注册拦截器
创建包 config,在包下创建类 WebConfig:
import com.xixi.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("/**"); //拦截路径为所有
}
}
详解
拦截器——拦截路径
拦截器可以根据需求,配置不同的拦截路径:
拦截器-执行流程
如果同时存在过滤器和拦截器,它们的执行顺序是这样的:
Filter 与 Interceptor 的不同点
- 接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。
- 拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。
登录校验- Interceptor
步骤
- 获取请求url
- 判断请求url中是否包含login,如果包含,说明是登录操作,放行
获取请求头中的令牌(token) - 判断令牌是否存在,如果不存在,返回错误结果(未登录)
- 解析token,如果解析失败,返回错误结果(未登录)
- 放行
因为只有 preHandle 方法是 Controller 执行之前执行的,所以我们的登录校验要在 preHandle 方法中进行
首先将过滤器注释掉:
因为登录校验的逻辑是一样的,所以我们可以复制前面使用过滤器登录校验的代码,再稍微修改一下即可:
import com.alibaba.fastjson.JSONObject;
import com.xixi.pojo.Result;
import com.xixi.utils.JwtUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
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;
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override //目标资源方法执行前执行,返回true: 放行;返回false: 不放行
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
//获取请求url
String url = req.getRequestURL().toString();
log.info("请求的url:{}", url);
//判断请求url中是否包含login,如果包含,说明是登录操作,放行
if (url.contains("login")) {
log.info("登录操作,放行...");
return true;
}
//获取请求头中的令牌(token)
String jwt = req.getHeader("token");
//判断令牌是否存在,如果不存在,返回错误结果(未登录)
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 false;
}
//解析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 false;
}
//放行
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...");
}
}
接下来我们进行测试,先测试登录功能,成功获取到令牌:
在不传递令牌的情况下查询部门:
给出令牌再次查询,成功:
控制台:
异常处理
程序开发过程中不可避免的会遇到类似下面的异常现象:
出现这种错误信息是因为我们没有对异常进行处理,异常一直向上层抛去,直到抛到 spring 框架,它给我们返回了这样一个错误信息:
那么出现这种异常,该如何处理?
- 方案一:在 Controller 的每个方法中进行 try…catch 处理(代码臃肿,不推荐)
- 方案二:使用全局异常处理器(简单、优雅、推荐)
全局异常处理器
创建包 exception,在包下创建类 GlobalExceptionHandler:
import com.xixi.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("对不起,操作失败,请联系管理员");
}
}
测试:
打开前端页面,添加一个已经存在的部门:
点击确定,会出现我们写的错误信息:
还没有评论,来说两句吧...