第9章 SpringBootWeb登录认证
系统后台被直接访问是不安全的,实现登录认证,让用户必须登录后才能访问后台系统中的功能。
9.1 功能开发
登录功能分两步开发:
1、登录接口开发
2、登录校验开发
9.1.1 登录接口开发
LoginController
@RestController
public class LoginController {
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp){
Emp e = empService.login(emp);
return e != null ? Result.success():Result.error("用户名或密码错误");
}
}
EmpService
public interface EmpService {
/**
* 用户登录
* @param emp
* @return
*/
public Emp login(Emp emp);
//省略其他代码...
}
EmpServiceImpl
@Slf4j
@Service
public class EmpServiceImpl implements EmpService {
@Autowired
private EmpMapper empMapper;
@Override
public Emp login(Emp emp) {
//调用dao层功能:登录
Emp loginEmp = empMapper.getByUsernameAndPassword(emp);
//返回查询结果给Controller
return loginEmp;
}
//省略其他代码...
}
EmpMapper
@Mapper
public interface EmpMapper {
@Select("select id, username, password, name, gender, image, job, entrydate, dept_id, create_time, update_time " +
"from emp " +
"where username=#{username} and password =#{password}")
public Emp getByUsernameAndPassword(Emp emp);
//省略其他代码...
}
9.1.2 登录校验-生成JWT
我们要完成登录校验操作,会涉及到web开发中的两个技术:
-
会话跟踪技术
-
统一拦截技术
会话跟踪技术有两种:
-
Cookie(客户端会话跟踪技术)
- 数据存储在客户端浏览器当中
-
Session(服务端会话跟踪技术)
- 数据存储在储在服务端
-
令牌技术JWT**(推荐)**
-
带数字签名的可靠安全的加密的JSON字符串信息
-
存储用户的相关信息
-
统一拦截技术现实方案也有两种:
-
Servlet规范中的Filter过滤器
-
Spring提供的interceptor拦截器
JWT的组成: (JWT令牌由三个部分组成,三个部分之间使用英文的点来分割)
-
第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{"alg":"HS256","type":"JWT"}
-
第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{"id":"1","username":"Tom"}
-
第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。
签名的目的就是为了防jwt令牌被篡改,而正是因为jwt令牌最后一个部分数字签名的存在,所以整个jwt 令牌是非常安全可靠的。一旦jwt令牌当中任何一个部分、任何一个字符被篡改了,整个令牌在校验的时候都会失败,所以它是非常安全可靠的。

JWT是如何将原始的JSON格式数据,转变为字符串的呢?
其实在生成JWT令牌时,会对JSON格式的数据进行一次编码:进行base64编码
Base64:是一种基于64个可打印的字符来表示二进制数据的编码方式。既然能编码,那也就意味着也能解码。所使用的64个字符分别是A到Z、a到z、 0- 9,一个加号,一个斜杠,加起来就是64个字符。任何数据经过base64编码之后,最终就会通过这64个字符来表示。当然还有一个符号,那就是等号。等号它是一个补位的符号
需要注意的是Base64是编码方式,而不是加密方式。
在JWT登录认证的场景中我们发现,整个流程当中涉及到两步操作:
-
在登录成功之后,要生成令牌。
-
每一次请求当中,要接收令牌并对令牌进行校验。
9.1.2.1 生成和校验JWT(测试)
1.引入JWT的依赖:
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
调用现成的工具包,工具类:Jwts
2.测试生成JWT代码
实现:
@Test
public void genJwt(){
Map<String,Object> claims = new HashMap<>();
claims.put("id",1);
claims.put("username","Tom");
String jwt = Jwts.builder()
.setClaims(claims) //自定义内容(载荷)
.signWith(SignatureAlgorithm.HS256, "itheima") //签名算法
.setExpiration(new Date(System.currentTimeMillis() + 24*3600*1000)) //有效期
.compact();
System.out.println(jwt);
}
运行测试方法:
eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjcyNzI5NzMwfQ.fHi0Ub8npbyt71UqLXDdLyipptLgxBUg_mSuGJtXtBk
输出的结果就是生成的JWT令牌,,通过英文的点分割对三个部分进行分割,我们可以将生成的令牌复制一下,然后打开JWT的官网,将生成的令牌直接放在Encoded位置,此时就会自动的将令牌解析出来。

第一部分解析出来,看到JSON格式的原始数据,所使用的签名算法为HS256。
第二个部分是我们自定义的数据,之前我们自定义的数据就是id,还有一个exp代表的是我们所设置的过期时间。
由于前两个部分是base64编码,所以是可以直接解码出来。但最后一个部分并不是base64编码,是经过签名算法计算出来的,所以最后一个部分是不会解析的。
3.测试校验JWT代码
校验JWT令牌(解析生成的令牌):
@Test
public void parseJwt(){
Claims claims = Jwts.parser()
.setSigningKey("itheima")//指定签名密钥(必须保证和生成令牌时使用相同的签名密钥)
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjcyNzI5NzMwfQ.fHi0Ub8npbyt71UqLXDdLyipptLgxBUg_mSuGJtXtBk")
.getBody();
System.out.println(claims);
}
运行测试方法:
{id=1, exp=1672729730}
令牌解析后,我们可以看到id和过期时间,如果在解析的过程当中没有报错,就说明解析成功了。
9.1.2.2 生成JWT令牌
实现步骤:
-
引入JWT工具类
- 在项目工程下创建com.itheima.utils包,并把提供JWT工具类复制到该包下
-
登录完成后,调用工具类生成JWT令牌并返回
JWT工具类
public class JwtUtils {
private static String signKey = "itheima";//签名密钥
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)//指定令牌Token
.getBody();
return claims;
}
}
登录成功,生成JWT令牌并返回
@RestController
@Slf4j
public class LoginController {
//依赖业务层对象
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp) {
//调用业务层:登录功能
Emp loginEmp = empService.login(emp);
//判断:登录用户是否存在
if(loginEmp !=null ){
//自定义信息
Map<String , Object> claims = new HashMap<>();
claims.put("id", loginEmp.getId());
claims.put("username",loginEmp.getUsername());
claims.put("name",loginEmp.getName());
//使用JWT工具类,生成身份令牌
String token = JwtUtils.generateJwt(claims);
return Result.success(token);
}
return Result.error("用户名或密码错误");
}
}
重启服务,打开postman测试登录接口:

9.1.3 登录校验-拦截校验令牌
统一拦截的两种方式:
1、过滤器Filter
-
Filter表示过滤器,是 JavaWeb三大组件(Servlet、Filter、Listener)之一。
-
作用:访问web服务器上的资源,必须先经过滤器。
-
应用场景:登录校验、统一编码处理、敏感字符处理等。
2、拦截器Interceptor(Spring框架)
-
Interceptor是一种动态拦截方法调用的机制,类似于过滤器。
-
作用:拦截请求,在指定方法调用前后,根据业务需要执行预先设定的代码。
-
应用场景:同上。
9.1.3.1 过滤器Filter
过滤器的执行流程如下图:

@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("DemoFilter 放行前逻辑.....");
//放行请求
filterChain.doFilter(servletRequest,servletResponse);
System.out.println("DemoFilter 放行后逻辑.....");
}
@Override //销毁方法, 只调用一次
public void destroy() {
System.out.println("destroy 销毁方法执行了");
}
}
在启动类上加@ServletComponentScan 表示使用Servlet相关组件
Filter可以根据需求,配置不同的拦截资源路径:
| 拦截路径 | urlPatterns值 | 含义 |
|---|---|---|
| 拦截具体路径 | /login | 只有访问 /login 路径时,才会被拦截 |
| 目录拦截 | /emps/* | 访问/emps下的所有资源,都会被拦截 |
| 拦截所有 | /* | 访问所有资源,都会被拦截 |
过滤器链:在一个web应用程序当中,可以配置多个过滤器
默认情况下过滤器执行顺序按类名自动排序确定。
开发登录校验过滤器的流程步骤:

登录校验过滤器:LoginCheckFilter
@Slf4j
@WebFilter(urlPatterns = "/*") //拦截所有请求
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
//前置:强制转换为http协议的请求对象、响应对象 (转换原因:要使用子类中特有方法)
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//1.获取请求url
String url = request.getRequestURL().toString();
log.info("请求路径:{}", url); //请求路径:http://localhost:8080/login
//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
if(url.contains("/login")){
chain.doFilter(request, response);//放行请求
return;//结束当前方法的执行
}
//3.获取请求头中的令牌(token)
String token = request.getHeader("token");
log.info("从请求头中获取的令牌:{}",token);
//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
if(!StringUtils.hasLength(token)){
log.info("Token不存在");
Result responseResult = Result.error("NOT_LOGIN");
//把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
String json = JSONObject.toJSONString(responseResult);
response.setContentType("application/json;charset=utf-8");
//响应
response.getWriter().write(json);
return;
}
//5.解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtils.parseJWT(token);
}catch (Exception e){
log.info("令牌解析失败!");
Result responseResult = Result.error("NOT_LOGIN");
//把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
String json = JSONObject.toJSONString(responseResult);
response.setContentType("application/json;charset=utf-8");
//响应
response.getWriter().write(json);
return;
}
//6.放行
chain.doFilter(request, response);
}
}
在上述过滤器的功能实现中,我们使用到了一个第三方json处理的工具包fastjson。我们要想使用,需要引入如下依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
Filter不属于Spring框架,没有对请求体或响应体的JSON自动转换功能。
9.1.3.2 拦截器Interceptor
拦截器的执行流程如下图:

拦截器的使用步骤分两步:
-
定义拦截器
-
注册配置拦截器
**自定义拦截器:**实现HandlerInterceptor接口,并重写其所有方法
//自定义拦截器
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
//目标资源方法执行前执行。 返回true:放行 返回false:不放行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle .... ");
return true; //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 .... ");
}
}
注意:
preHandle方法:目标资源方法执行前执行。 返回true:放行 返回false:不放行
postHandle方法:目标资源方法执行后执行
afterCompletion方法:视图渲染完毕后执行(即响应已生成并发送给客户端)
注册配置拦截器:实现WebMvcConfigurer接口,并重写addInterceptors方法
@Configuration
public class WebConfig implements WebMvcConfigurer {
//自定义的拦截器对象
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表示拦截所有请求)
}
}
区别主要是两点:
接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。
拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。
常见拦截路径设置:
| 拦截路径 | 含义 | 举例 |
|---|---|---|
| /* | 一级路径 | 能匹配/depts,/emps,/login,不能匹配 /depts/1 |
| /** | 任意级路径 | 能匹配/depts,/depts/1,/depts/1/2 |
| /depts/* | /depts下的一级路径 | 能匹配/depts/1,不能匹配/depts/1/2,/depts |
| /depts/** | /depts下的任意级路径 | 能匹配/depts,/depts/1,/depts/1/2,不能匹配/emps/1 |
开发登录校验拦截器的流程步骤:
登录校验拦截器
//自定义拦截器
@Component //当前拦截器对象由Spring创建和管理
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
//前置方式
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle .... ");
//1.获取请求url
//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行
//3.获取请求头中的令牌(token)
String token = request.getHeader("token");
log.info("从请求头中获取的令牌:{}",token);
//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)
if(!StringUtils.hasLength(token)){
log.info("Token不存在");
//创建响应结果对象
Result responseResult = Result.error("NOT_LOGIN");
//把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
String json = JSONObject.toJSONString(responseResult);
//设置响应头(告知浏览器:响应的数据类型为json、响应的数据编码表为utf-8)
response.setContentType("application/json;charset=utf-8");
//响应
response.getWriter().write(json);
return false;//不放行
}
//5.解析token,如果解析失败,返回错误结果(未登录)
try {
JwtUtils.parseJWT(token);
}catch (Exception e){
log.info("令牌解析失败!");
//创建响应结果对象
Result responseResult = Result.error("NOT_LOGIN");
//把Result对象转换为JSON格式字符串 (fastjson是阿里巴巴提供的用于实现对象和json的转换工具类)
String json = JSONObject.toJSONString(responseResult);
//设置响应头
response.setContentType("application/json;charset=utf-8");
//响应
response.getWriter().write(json);
return false;
}
//6.放行
return true;
}
注册配置拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
//拦截器对象
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册自定义拦截器对象
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login");
}
}
9.2 全局异常处理器
将全局的异常信息统一的符合开发规范的异常格式。
全局异常处理器示意图如下:

9.2.1全局异常处理器开发分几步?
-
类上加 @RestControllerAdvice 注解标记这是一个全局异常处理器类。
-
通过方法上加 @ExceptionHandler 注解,并在当中的value属性来指定我们要捕获的是哪一类型的异常。
@RestControllerAdvice//表示当前类为全局异常处理器
public class GlobalExceptionHandler {
//处理异常
@ExceptionHandler(Exception.class) //指定能够处理的异常类型
public Result ex(Exception e){
e.printStackTrace();//打印堆栈中的异常信息
//捕获到异常之后,响应一个标准的Result
return Result.error("对不起,操作失败,请联系管理员");
}
}
@RestControllerAdvice = @ControllerAdvice + @ResponseBody
处理异常的方法返回值会转换为json后再响应给前端