SpringBoot实现登录校验与日志记录

发布时间 2023-05-19 21:42:46作者: snob

1.系统登录实现

表示层:

点击查看代码

@RestController
public class LoginControlly {
    @Autowired
    private EmpService empService;
    @PostMapping("/login")
    public Result login (@RequestBody Map<String,String> map){
        String username = map.get("username");
        String password = map.get("password");
        Emp emp = empService.findByUsernameAndPassword(username,password);
        if (emp == null){
            return  Result.error("用户名不存在密码或错误");
        }else {
            return Result.success();
        }
    }
}

业务层:

点击查看代码
@Service
public class EmpServiceImpl implements EmpService {
    @Autowired
    private EmpMapper empMapper;

    @Override
    public Emp findByUsernameAndPassword(String username, String password) {
        return empMapper.findByUsernameAndPassword(username,password);
    }

持久层:

点击查看代码
@Select("select * from emp where username = #{username} and password=#{password}")
    Emp findByUsernameAndPassword(String username, String password);

2.登录校验

虽然实现了将用户输入的账号密码与数据库中的数据进行匹配登录,但依然可以不经过登录的情况下,直接输入员工页面地址就可以访问,这是非常不安全的。

  • 正确的流程应该是:当访问请求到达服务器后,服务器要校验当前用户是否已经登录过
    如果登录过,就放行请求
    如果未登录过,就禁止请求访问

  • 那如何知道用户是否已经登录过呢?这就需要在用户登录成功后,由服务器为其颁发一个token(身份标识)
    然后用户每次发送请求,都会携带着这个token
    而作为系统会对每次的请求进行拦截,校验token的合法性即可

2.1JWT

JWT介绍:

全称:JSON Web Token 用于对应用程序上的用户进行身份标记。

本质上就是一个经过加密处理与校验处理的字符串,它由三部分组成:

  • 头信息(Header):记录令牌类型和签名算法
  • 有效载荷(Payload):记录一些自定义能够区分身份的非敏感信息
  • 签名(Signature):用于保证Token在传输过程中不被篡改,它是header、payload,加入指定算法计算得来的

登录功能改进

@RestController
public class LoginControlly {
    @Autowired
    private EmpService empService;
    @PostMapping("/login")
    public Result login (@RequestBody Map<String,String> map){
        String username = map.get("username");
        String password = map.get("password");

        Emp emp = empService.findByUsernameAndPassword(username,password);
        if (emp == null){
            return  Result.error("用户名不存在密码或错误");
        }else {
            //配置载荷
            Map<String,Object> claims = new HashMap<>();
            claims.put("id",emp.getId());
            claims.put("username",emp.getUsername());
            claims.put("name",emp.getName());

            //生成token令牌
            String token = JwtUtils.generateJwt(claims);
            return Result.success(token);
        }
    }
}

修改目前的登录功能,当登录成功后,创建token并返回给客户端

2.2过滤器

当用户访问服务器资源时,过滤器将请求拦截下来,完成一些通用的功能,比如:登录校验、统一编码处理、敏感字符处理等

实现过滤器需要三步
1.定义Filter,定义一个类实现Filter接口,并重写其中的方法。

2.配置Filter: Filter类添加@WebFilter注解,并配置拦截资源的路径。

3.在启动类上加@ServletComponentScan 开启Servlet组件支持。

执行流程

1.客户端向服务器发起访问资源的请求。
2.Filter拦截请求,处理访问资源之前的逻辑。
3.Filter决定是否放行访问请求。
4.请求访问到相关资源,服务器给出响应。
5.Filter拦截响应,处理访问资源之后的逻辑。
6.服务器将响应返回给浏览器。

在代码实现之前,不妨先思考两个问题:

  1. 所有的请求,拦截到了之后,都需要校验令牌吗?
  2. 拦截到请求后,在满足什么条件下才可以放行?

对于登录请求来说显然不需要校验令牌,而其他请求就需要了。
拦截到请求后首先要看有没有令牌,有令牌并且令牌没有过期且内容真实就可以放行,否则不放行。

代码实现:

@WebFilter("/*")
public class LoginCheckFilter implements Filter {

    @Autowired
    private Gson gson;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //1.强转
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse rep = (HttpServletResponse) response;

重写Filter类中的doFilter方法,需要将参数request和response强转为HttpServletRequest类型,方便之后调用方法

        //2.处理业务逻辑
        //获取url
        String uri = req.getRequestURI();
        //判断是否含有login
        if ("/login".equals(uri)){
            //放行
            chain.doFilter(req,rep);
            return;
        }

首先要判断是不是登录请求,如果是则直接放行否则检验令牌,判断的方法为调用getRequestURI方法获取URI判断路径中是否含有"/login"
URI:相当于身份证号 URL:类似于身份证住址+姓名
因此这里使用URI就可以了

        //获取请求头中的令牌(token)
        String token = req.getHeader("token");
        if (token==null){
            Result result = Result.error("NOT_LOGIN");
            //String json = new ObjectMapper().writeValueAsString(result);
            //把java对象转为Json格式
            String json = gson.toJson(result);
            rep.setContentType("application/json;charset=utf=8");
            rep.getWriter().write(json);
            return ;
        }

如果不是登录就获取请求头中的令牌,通过在浏览器中F12调试可以发现令牌实在请求头中,因此这里调用getHeader方法,判断请求是否带有令牌,如果没有令牌就根据API文档向前端返回"NOT_LOGIN"(两种转换为json格式的方法,谷歌的gson需要导入依赖)

        //解析token,解析失败则返回结果
         try {
             JwtUtils.parseJWT(token);
         }catch (Exception e){
             //Log.info("token错误");
             Result result = Result.error("NOT_LOGIN");
             String json = gson.toJson(result);
             rep.setContentType("application/json;charset=utf=8");
             rep.getWriter().write(json);
             return ;
         }
        //3.放行
        chain.doFilter(req,rep);
    }
}

最后解析token(令牌),失败则返回结果,成功放行

2.3拦截器

拦截器介绍

拦截器是Spring提供的一种技术,它的功能似于过滤器,它会在进入controller之前,离开controller之后以及响应离开服务时进行拦截。

拦截路径

拦截器的路径写法相对简单,其实只有两个:/*表示一层路径 /**表示多层路径

拦截器实现访问校验

添加依赖

<dependency>    <groupId>com.google.code.gson</groupId>    <artifactId>gson</artifactId></dependency>

① 创建Interceptor
判断是否存在token和token是否过期,都没有则返回true放行

@Component
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
    @Autowired
    private Gson gson;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("token");
        if (token == null){
            Result result = Result.error("NOT_LOGIN");
            String json = gson.toJson(result);
            response.setContentType("application/json;charset-utf-8");
            response.getWriter().write(json);
            return false;
        }
        try {
            JwtUtils.parseJWT(token);
        } catch (Exception e) {
            log.info("token错误");
            String json = gson.toJson(Result.error("NOT_LOGIN"));
            response.setContentType("application/json;charset-utf-8");
            response.getWriter().write(json);
            return false;
        }
        return true;
    }

}

② 配置Interceptor
实现WebMvcConfigurer接口,重写addInterceptors方法,生成拦截器,调用三个方法
1.addInterceptor 2.addPathPatterns 3.excludePathPatterns
添加拦截器,添加拦截路径,排除的拦截路径

public class MVCConfig implements WebMvcConfigurer {
    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginCheckInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/login");
    }
}

3.异常处理

开发一个全局异常处理器来捕获系统中出现的异常,然后给前端一个统一报错

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public Result hanler(Exception e){
        e.printStackTrace();
        return Result.error("操作失败");
    }
}

4.日志记录

日志记录过程相对繁琐复杂,直接贴代码

@Component
@Aspect
public class LogAspect {
    @Pointcut("@annotation(com.itheima.anno.Anno)")
    public void pt(){}

    @Autowired
    //获取token
    private HttpServletRequest request;
    @Autowired
    private LogService logService;
    @Around("pt()")
    public Object log(ProceedingJoinPoint point) throws Throwable{
        Object obj = null;
        try {
            //1.收集信息
            String className = point.getTarget().getClass().getName();
            MethodSignature signature = (MethodSignature) point.getSignature();
            String methodName = signature.getMethod().getName();
            Object[] args = point.getArgs();
            //获得方法上的注解的属性值
            String methodDesc = signature.getMethod().getAnnotation(Anno.class).methodDesc();
            String token = request.getHeader("token");
            Claims claims = JwtUtils.parseJWT(token);
            Integer id = (Integer) claims.get("id");


            long startTime = System.currentTimeMillis();
            obj = point.proceed();
            long endTime = System.currentTimeMillis();


            OperateLog operateLog = new OperateLog();
            operateLog.setClassName(className);
            operateLog.setMethodName(methodName);
            operateLog.setMethodDesc(methodDesc);
            operateLog.setMethodParams(Arrays.toString(args));
            operateLog.setReturnValue(new ObjectMapper().writeValueAsString(obj));
            operateLog.setOperateUser(id);
            operateLog.setOperateTime(LocalDateTime.now());
            operateLog.setCostTime(endTime-startTime);

            //2.插入到表
            logService.addLog(operateLog);

        } catch (Throwable e) {
            e.printStackTrace();
            throw new Throwable(e);
        }
        return obj;
    }
}

JAVA注解(Annotation)也称为JAVA标注,是JDK 1.5 引进的一种注释机制。
使用@interface进行创建注解接口,进行注解接口的定义。注解是注解接口声明的简称。
元注解作用范围只适用于注解接口,用于描述注解接口的行为属性。其中比较常用的有4个,分别是@Target@Retention@Documented@Inherited
@Retention 表示注解接口的生存时间。
@Target 描述注解接口的作用域。
ElementType ElementType是一个enum类,其中的enum常量表示作用域范围。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Anno {
    String methodDesc();
}