1、登录功能分析
需求:

首先需要有一个对象可以存放以下登陆数据:

两种方法:一个是在emp实体类中再添加一个属性,另一个是新建一个类封装以上属性
本文选用第二种,在pojo包中新建一个登录的实体类:
package com.itheima.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginInfo {
private Integer id;
private String username;
private String name;
private String token;
}
三层架构如下:

同样的新建一个控制层类并添加如下代码:
package com.itheima.controller;
import com.itheima.mapper.EmpMapper;
import com.itheima.pojo.Emp;
import com.itheima.pojo.LoginInfo;
import com.itheima.pojo.Result;
import com.itheima.service.EmpService;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empservice;
@PostMapping("/login")
public Result login(@RequestBody Emp emp){
log.info("员工登录:{}",emp);
LoginInfo logininfo=empservice.Login(emp);
if(logininfo!=null){//判断返回值是否存在数据!!!
return Result.success(logininfo);
}
return Result.error("用户名或密码错误!!!");
}
}
使用了if进行判断,如果为空代表数据库中并没有该用户名和密码的数据,如果有值才代表登陆成功!
之后调用emoservice层:
LoginInfo Login(Emp emp);
对应的实现类添加如下代码,此处将token先赋值为空字符!
@Override
public LoginInfo Login(Emp emp) {
Emp e=empMapper.LoginDatabyup(emp);
if(e!=null){
log.info("员工信息:{}",e);
return new LoginInfo(e.getId(),e.getUsername(),e.getName(),"");
}
return null;
}
最后在empmapper新增查询功能:
@Select("select id,username,name from emp where username=#{username} and password=#{password}")
Emp LoginDatabyup(Emp emp);
通过用户名和密码实现员工信息查询!
2、登录校验
登录功能实现之后,发现不论是否进行登录,都可以通过网址进入到主页面并进行部门和员工的增删改查,这意味登录的作用没有任何意义,因此需要进行校验!!!


(1)会话技术

首先是方案一:cookie
虽然在http协议中没有cookie,但是它是http所支持的技术,并且带了请求头和响应头,从而可以帮助其自动进行获取信息并校验!

简单来说:第一次访问c1时,将数据存储在cookie中
之后访问c2时,将cookie存储的值拿出来使用

移动端譬如安卓ios不能使用
存储在cookie中,客户端可以自己删除并且浏览器也可以禁用第三方的cookie

由于前后端是分开开发的,服务器有两台,但是访问前端页面时将信息存储,后端的服务器是没有的,因此返回报错!

总结:

方案二:session
这种方法主要使用了集群的方法,中间的服务器是负载均衡服务器,第一次登录请求之后,浏览器返回负载服务器,之后随机分给一台服务器a。当第二次登录的时候,负载服务器可能将这个相应分配给服务器b,以此类推,在集群情况下,无法使用session!!!

总结:

方案三:令牌(主流)

a.JWT令牌
在第二部分末尾如果出现"=",表示是一个补位符号!!!
令牌长度不固定!!!

首先在pom文件中引入依赖:
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
之后在test包下继续新建一个类:JwtTest:

在属signWith的时候选择第三个,参数为字符串!

以下是完整的测试代码:
@Test
public void testGenerateJwt(){
Map<String, Object> dataMap=new HashMap<>();
dataMap.put("id",1);
dataMap.put("username","admin");//使用dataMap存储自定义信息
String jet=Jwts.builder().signWith(SignatureAlgorithm.HS256, "itheima")//指定令牌签名的算法和签名的密钥(密钥如果使用二进制编码,可以先将密钥使用base64编码输入即可)
.addClaims(dataMap)//添加自定义信息
.setExpiration(new Date(System.currentTimeMillis()+1000*60*60))//指定令牌有限期为1小时,从当前时间+3600000毫秒后过期
.compact();//构建令牌
System.out.println(jet);
}
运行程序如下所示:

可以通过Base64进行解码:

其中exp表示令牌的有效时间:

之后添加解析令牌的代码进行验证,一定要在令牌的有限期内解析令牌,不然也会报错!
@Test
public void testParaseJwt(){//解析令牌
//输入上面生成的令牌
String token="eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsImV4cCI6MTc2ODU0ODA1M30.aZ_bc3ZK84UjkNpLZshlRJoMLJG9nRu90I2uj2zQ3lk";
Claims claims=Jwts.parser().setSigningKey("itheima")//将密钥输入!
.parseClaimsJws(token)
.getBody();
System.out.println(claims);
}
结果如下:

总结:

之后生成令牌:

之后在工具类utils中新建一个类并导入以下内容:
package com.itheima.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 = "itheima";//密钥
private static long expire = 60*60*1000*12;//12小时后令牌过期
/**
* 生成JWT令牌
* @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;
}
}
之后对控制层中的代码进行更改,如下所示,主要增加了jwt生成代码以及相应数据:
@Override
public LoginInfo Login(Emp emp) {
Emp e=empMapper.LoginDatabyup(emp);
if(e!=null){
log.info("员工信息:{}",e);
Map<String,Object> claims=new HashMap<>();
claims.put("id",e.getId());
claims.put("username",e.getUsername());
String jwt=JwtUtils.generateJwt(claims);
return new LoginInfo(e.getId(),e.getUsername(),e.getName(),jwt);
}
return null;
}
运行项目之后,在apifox发送信息,响应数据:

在项目中实践登录功能,登陆成功后显示 宋江

b.Filter过滤器
目前过滤器基本只用Filter

只有Filter校验通过请求才能通过,不然直接拦截!!!

开启过滤器的步骤:

首先新建一个软件包:filter并在包下新创建一个类:

之后引入filter的标准接口,是servlet下的filter!!!

之后重写接口中引入的三个方法(官方提供的接口中init和destroy是空参实现的,一般可以自己不定义),对于入门程序来说这里三个方法都实现:
在类中的代码如下所示:
package com.itheima.filter;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
@WebFilter(urlPatterns = "/*")//拦截到所有请求
@Slf4j
public class DemoFilter implements Filter {
//初始化方法,只会在web服务器启动时,执行一次!
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("初始化过滤器...");
// Filter.super.init(filterConfig);
}
//过滤方法,拦截到请求,都会执行此方法
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("拦截到请求...");
}
//销毁方法,只会在web服务器关闭时,执行一次!
@Override
public void destroy() {
log.info("销毁过滤器...");
Filter.super.destroy();
}
}
之后在启动类中添加以下注解:
@ServletComponentScan//开启springboot对servlet组件的扫描
之后启动项目,在apifox发送登录请求,拦截成功:

但是目前代码中只是拦截到请求,之后需要"放行"
在doFilter中继续添加以下代码:
//放行
filterChain.doFilter(servletRequest,servletResponse);
之后登录成功相应数据:

停止项目之后自动调用dertroy方法:

总结:

登录注意事项:

由于目前系统还没有涉及到注册功能,因此目前只有一个例外:登录!

当请求路径中有/login,表示是登录请求直接放行,否则需要对其携带的令牌进行检验!!!

之后在filter软件包中新建一个类:TokenFilter:
根据以上内容进行编写代码:
package com.itheima.filter;
import com.itheima.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 java.io.IOException;
@Slf4j
@WebFilter(urlPatterns = "/*")//拦截所有请求进行判断
public class TokenFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info("拦截到请求...");
HttpServletRequest request=(HttpServletRequest) servletRequest;
HttpServletResponse response=(HttpServletResponse) servletResponse;
//获取请求路径
String url=request.getRequestURI();
//判断请求路径是否为登录请求
if(url.contains("/login")){
filterChain.doFilter(request,response);
return ;
}
//获取请求头的token
String token=request.getHeader("token");
//判断token是否存在,不存在返回401状态码
if(token==null||token.isEmpty()){
response.setStatus(401);
return ;
}
//检验令牌,如果不通过响应401
try{
JwtUtils.parseJWT(token);
}catch(Exception e){
response.setStatus(401);
return ;
}
//校验通过则放行
log.info("令牌校验通过");
filterChain.doFilter(request,response);
}
}
之后启动项目在项目中进行实践,如果输入页面内的网址会显示以下操作(除了index页面,这可能和前端代码有关,不过就算进入index页面,一旦点击任何与数据相关的页面都会直接退出并显示如下内容)

详解:

拦截路径:

过滤器链:命名排序越靠前表示过滤器越先执行!:譬如一个是AbcFilter,一个是DFilter,这样第一个过滤器就会先执行!!!

之后复制DemoFilter到同样的软件包下,命名为AbcFilter
之后启动项目随机选取一个接口发送,结果如下所示:类似于先拦截后放行



总结:

c.interceptor

步骤:其中/** 表示拦截所有!

新建一个软件包:interceptor,并在包中新建一个类:DemoInterceptor(入门程序,都实验一次)
之后实现接口HandlerInterceptor,并使用ctrl+o重写这个类的所有实现方法(3个):
package com.itheima.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.Interceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Slf4j
@Component
public class DemoInterceptor implements HandlerInterceptor {
@Override
//在目标方法执行之前执行,true表示放行,false表示不放行
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("preHandle...");
return true;
}
@Override
//在目标方法执行之后执行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle...");
}
@Override
//在目标方法执行完成之后,视图渲染后执行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion...");
}
}
之后新建一个软件包:config,并新建一个类为:WebConfig
package com.itheima.config;
import com.itheima.interceptor.DemoInterceptor;
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 DemoInterceptor demoInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(demoInterceptor).addPathPatterns("/**");//指定拦截路径
}
}
在启动项目之前要确保所有的过滤器已经注释:

之后启动项目,从控制台可得:


总结:

之后在实际中,需要通过令牌检验interceptor,在interceptor包下新建一个类:TokenInterceptor
实现接口HandlerInterceptor,只需要重写第一个方法即可!
以下是这个类的代码:
package com.itheima.interceptor;
import com.itheima.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.web.servlet.HandlerInterceptor;
@Slf4j
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Override//只重写这一个方法
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("拦截到请求...");
//获取请求路径
String url=request.getRequestURI();
//判断请求路径是否为登录请求
if(url.contains("/login")){
return true;
}
//获取请求头的token
String token=request.getHeader("token");
//判断token是否存在,不存在返回401状态码
if(token==null||token.isEmpty()){
response.setStatus(401);
return false;
}
//检验令牌,如果不通过响应401
try{
JwtUtils.parseJWT(token);
}catch(Exception e){
response.setStatus(401);
return false;
}
//校验通过则放行
log.info("令牌校验通过");
return true;
}
}
之后对webconfig进行修改:
package com.itheima.config;
import com.itheima.interceptor.DemoInterceptor;
import com.itheima.interceptor.TokenInterceptor;
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 DemoInterceptor demoInterceptor;
@Autowired
private TokenInterceptor tokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor).addPathPatterns("/**");//指定拦截路径
}
}
之后启动项目,不登录直接访问里边的某一页网址(除了index),出现如下效果:

拦截路径中关于/*和/**的区别:

整体流程:

之后实践,将demofilter(打开
)、demoInterceptor和webconfig(将拦截器进行更改
)进行配置。
启动项目,使用apifox发送任意一个接口请求,控制台结果如下:

总结:

之后只要让令牌生效的过滤器或者拦截器某一个打开即可,其余的都关闭!
这里保留了TokenFilter,其余均关闭!!!(都打开也可以,不过性能会降低!)