- 基础登录功能:根据提供的用户名和密码判断是否存在于数据库
LoginController.java
@RestController
@Slf4j
public class LoginController {
@Autowired
private UserService userService;
@PostMapping("/login")
public Result login(@RequestBody User user) {
log.info("user: {}", user);
User u = userService.login(user);
return u != null ? Result.success(u) : Result.error("用户名或密码错误");
}
}
UserController.java
@Service
public class UserServiceImpl implements com.diaryback.Service.UserService {
@Autowired
private UserMapper userMapper;
public User login(User user) {
return userMapper.getUserByIdAndPassword(user);
}
}
登录校验
在用户未登录情况下访问需要登录才能使用的业务,会跳转到登录界面
会话技术
- 会话:用户通过浏览器访问服务器资源时建立会话,直到一方断开连接会话结束。一次会话中可以包含多次请求和响应
- 会话跟踪:服务器需要识别多个请求是否来自同一个浏览器,以便在同一个会话的多个请求之间共享数据
Cookie
服务器端存储Cookie,响应时自动加上Cookie到客户端,客户端下次请求会自动加上cookie
不允许跨域请求
Session
Session技术基于cookie,首次访问服务端时创建session,并将sessionID附加到cookie
令牌
令牌中存储用户的身份信息以及需要共享的数据,存储在客户端
JWT令牌
jwt:JSON Web Token,将原始JSON数据进行安全封装
jwt三组成部分:
- Header:记录令牌类型,签名算法
- Payload:有效载荷,携带自定义信息,默认信息和有效期等
- Signature:签名,确保安全性。将header和payload加密计算而来
引入依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.12.3</version>
</dependency>
生成jwt字符串:需要指定签名算法、密钥以及自定义内容和过期时间
/**
* 测试生成JWT
*/
@Test
public void testGenJWT(){
Map<String, Object> claims = new HashMap<>();
claims.put("id", 1);
claims.put("name", "maria");
String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "AlexandarHamiltonWeAreWaitingInTheWingsForYou")//设置加密方式和加密密钥
.claims(claims)//设置自定义内容
.expiration(new Date(System.currentTimeMillis() + 3600 * 1000))//设置有效期为一个小时
.compact();//生成字符串
System.out.println(jwt);
}
解析时需要指定签名的密钥
/**
* 测试解析JWT
*/
@Test
public void testParseJWT() {
Claims claims = Jwts.parser().setSigningKey("AlexandarHamiltonWeAreWaitingInTheWingsForYou")//设置解密密钥
.build().parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoibWFyaWEiLCJpZCI6MSwiZXhwIjoxNzM3NzExMjQ3fQ.AD2hdUzHdzq9qA0ZulvOyWA857tuRUWChnEX2P1ebcI")
.getBody();
System.out.println(claims);
}
JWT工具类:jwtUtil
package com.diaryback.Utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtil {
private static String signKey = "AlexandarHamiltonWeAreWaitingInTheWingsForYou";
private static Long expire = 432000000L;
/**
* 生成jwt令牌
*/
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令牌
*/
public static Claims parseJwt(String jwt){
Claims claims = Jwts.parser()
.setSigningKey(signKey)
.build()
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
Filter过滤器
可以拦截对资源的请求,通常用于登录校验、统一编码等
- 定义Filter:定义一个类,实现Filter接口,重写所有方法(init(), destroy(), doFilter())
- 配置Filter:Filter类加上@WebFilter注解,配置拦截资源的路径,同时启动类加上@ServletComponentScan开启Servlet组件支持
拦截路径的配置:
过滤器链:一个web应用中可以配置多个过滤器行程过滤器链
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;
import javax.servlet.*;
import java.io.IOException;
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements jakarta.servlet.Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Filter.super.init(filterConfig);
System.out.println("初始化过滤器");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Demo 执行过滤操作...放行前逻辑");
//放行
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("Demo 执行过滤操作...放行后逻辑");
}
@Override
public void destroy() {
// Filter.super.destroy();
System.out.println("销毁过滤器");
}
}
登录校验的Filter流程:
LoginFilter.java
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONPObject;
import com.diaryback.Pojo.Result;
import com.diaryback.Utils.JwtUtil;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.http.HttpRequest;
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;//强制类型转换,将ServletRequest转换为其子类HttpServletRequest
HttpServletResponse resp = (HttpServletResponse) servletResponse;
String url = req.getRequestURL().toString();
log.info("请求的url:{}", url);
// 如果是登录请求则放行
if(url.contains("login")){
log.info("登录请求,放行");
filterChain.doFilter(servletRequest, servletResponse);
return;
}
//获取请求头中的令牌
String jwt = req.getHeader("token");
//判断请求头中是否有令牌
if(!StringUtils.hasLength(jwt)){
log.info("未登录,不允许访问");
Result error = Result.error("NOT_LOGIN");
//将Result转换为JSON格式传递给前端,使用fastJson包
String notLogin = JSON.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
//解析token校验令牌
try {
JwtUtil.parseJwt(jwt);
} catch (Exception e) {//jwt解析失败
e.printStackTrace();
log.info("解析失败,不允许访问");
Result error = Result.error("NOT_LOGIN");
String notLogin = JSON.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
//放行
log.info("已登录,放行");
filterChain.doFilter(servletRequest, servletResponse);
}
}
Interceptor拦截器
Interceptor:作用类似于Filter,拦截请求,在指定方法调用前后执行预先设定的代码
- 定义拦截器,实现HandlerInterceptor接口,并重写所有方法(preHandler, postHandle, afterCompletion)
- 注册拦截器,需要在配置类中注册
LoginCheckInterceptor.java
package com.diaryback.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 //将拦截器交给Spring容器管理
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override //请求处理前调用,返回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执行");
}
}
WebConfig.java
package com.diaryback.config;
import com.diaryback.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("/**");
}
}
拦截路径:addPathPatterns()定义需要拦截哪些资源,excludePathPatterns()定义不需要拦截哪些资源
拦截器执行流程:
登录校验拦截器
```java
package com.diaryback.Interceptor;
import com.alibaba.fastjson.JSONObject;
import com.diaryback.Pojo.Result;
import com.diaryback.Utils.JwtUtil;
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 //将拦截器交给Spring容器管理
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override //请求处理前调用,返回false则请求不会被处理
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String url = request.getRequestURL().toString();
log.info("preHandle执行,url: {}", url);
if(url.contains("login")){
log.info("放行");
return true;
}
String jwt = request.getHeader("token");
//检查是否有令牌
if(!StringUtils.hasLength(jwt)){
log.info("未登录");
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return false;
}
//解析令牌
try {
JwtUtil.parseJwt(jwt);
} catch (Exception e) {
e.printStackTrace();
log.info("令牌无效");
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error);
response.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执行");
}
}
异常处理
定义全局异常处理器
import com.diaryback.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("服务器异常");
}
}