1 登录
先创建一个新的 controller 层: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("error");
}
}
在 service 层实现
service 层调用 mapper 层,mapper 层操作数据库
测试:
2 登录校验
刚才的程序有 bug,即只要改一下 url,可以跳过登录直接进入员工管理界面,此时需要校验
2.1 会话技术
会话:
用户打开浏览器,访问 web 服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应
会话跟踪:
一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据
会话跟踪方案:
- 客户端会话跟踪技术:Cookie
- 服务端会话跟踪技术:Session
- 令牌技术
2.1.1 Cookie
Cookie 是一种小型数据存储,通常存储在用户浏览器中,可以由服务器在用户访问网站时发送给用户的浏览器,并由浏览器在随后的请求中发送回服务器
2.1.2 Session
Session 是另一种用于在用户和服务器之间保持状态的技术。与 Cookie 不同,Session 通常不直接存储在用户的浏览器中,而是存储在服务器端
2.1.3 令牌
JWT 令牌(JSON Web Token):
定义了一种简洁的、自包含 的格式,用于在通信双方以 json 数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的
自包含:JWT 包含所有必要的信息,不需要访问服务器来验证
JWT 令牌的组成:
- Header(头),记录令牌类型,签名算法等,例如{"alg":"HS256","type":"JWT"}
- Payload(有效载荷),携带一些自定义信息、默认信息等,例如{"id":"1","username":"Tom"}
- Signature(签名),防止 Token 被篡改、确保安全性。将 header、payload,并加入指定秘钥,通过指定签名算法计算而来
Header,Payload,Signature 字符串通过 Base64 编码变成 JWT 字符串
原理:
- 登录成功后,生成令牌
- 后续每个请求,都要携带 JWT 令牌,系统在每次请求处理之前,先校验令牌,通过后,再处理
生成 JWT 令牌:
1、引入依赖——在 pom.xml 中
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2、使用 jwts 库下的 jwt.builder() 方法
(在单元测试中写)
public void testJWT() {
Map<String, Object> claim = new HashMap<>();
claim.put("id", 1);
claim.put("name", "wyn");
String jwt = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, "mi_yao") // JWT 第一部分 Signature签名
.setClaims(claim) // JWT 第二部分 Payload载荷
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) // 设置令牌有限期为1h
.compact();// 生成 JWT 令牌(字符串)
System.out.println(jwt);
// print
// eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoid3luIiwiaWQiOjEsImV4cCI6MTcyMTM5NDcwMn0.prh9gbgvW5PTWvogGqkjPE2ofrXz-5FVGkYoDkEaNoI}
}
链式编程:
.signWith()
第一个参数是加密算法,第二个参数是密钥
.setClaims()
设置自定义的信息,参数是 map 类型
.setExpiration()
设置令牌有效时间
.compact()
最终生成 JWT 令牌的字符串
(注:每次生成的令牌不一样)
JWT 的解析:
@Test
public void testParseJWT() {
Map<String, Object> claims = Jwts.parser()
.setSigningKey("mi_yao") // 指定签名
// 解析 JWS(JSON Web Signature),返回一个 JWS 对象
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoid3luIiwiaWQiOjEsImV4cCI6MTcyMTM5NDcwMn0.prh9gbgvW5PTWvogGqkjPE2ofrXz-5FVGkYoDkEaNoI")
.getBody(); // 提取负载部分
System.out.println(claims); // print {name=wyn, id=1, exp=1721394702}
}
如果 JWT 输入不正确,就会报错
如果上面设置的 JWT 令牌有效时间过期了,也会报错
2.2 使用 JWT 令牌登录&校验
在 utils 软件包下导入一个工具类 JWTUtils:
package com.wyn.tlias.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 key = "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, key)
.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(key)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
2.2.1 登录
在 LoginControlller 里面直接调用 JWTUtils:
claim 信息自己添加给一个 map,然后作为一个 Result 统一响应结果返回
@RestController
public class LoginController {
@Autowired
private EmpService empService;
/**
* 登录,示例 username = "jinyong",password = 123456
* @param emp
* @return
*/
@PostMapping("/login")
public Result login(@RequestBody Emp emp) {
Emp e = empService.login(emp);
if (e != null) {
Map<String, Object> claims = new HashMap<>();
claims.put("id",emp.getId());
claims.put("username", emp.getUsername());
claims.put("name",emp.getName());
// claims.put("password", emp.getPassword()); // 注意不要封装密码
String jwt = JwtUtils.generateJwt(claims);
return Result.success(jwt);
}
return Result.error("error");
}
}
测试,返回了一个 JWT 令牌
2.2.2 校验
现在拿到令牌,需要进行 统一拦截,如果令牌存在,就可以继续;如果没有,就返回登录页面
统一拦截两种解决方案:
- 过滤器 Filter
- 拦截器 Interceptor
过滤器 Filter
JavaWeb 三大组件(Servlet、Filter、Listener)之一
Servlet 是一个运行在服务器端的 Java 小程序,它是 Web 应用程序的基石。Servlet 可以响应客户端的请求(通常是 HTTP 请求),并生成响应
Filter 是一个在 Servlet 之前或之后执行的程序,用于在请求到达 Servlet 之前或响应发送给客户端之前进行预处理或后处理
Listener 是一个监听特定事件的对象,它可以在事件发生时接收通知并执行相应的处理逻辑
过滤器可以把对资源的请求 拦截 下来,从而实现一些特殊的功能
过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等
使用方法:
1.定义 Filter:定义一个类,实现 Filter 接口,并重写其所有方法(就重写一个就行)
2.配置 Filter:Filter 类上加 @WebFilter 注解,配置拦截资源的路径。引导类上加@ServletcomponentScan 开启 Seflet 组件支持
在启动类里面加上注解
@ServletComponentScan
然后启动,发现原来的登录,发送 json 数据,已经没有响应了,被拦截(拦截了没有后续,所以没有响应),仅仅单纯拦截,而没有 放行
放行:
调用方法
filterChain.doFilter(servletRequest,servletResponse);
过滤器链
一个web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链
校验
测试:
1、访问 /login,获得一个 jwt 令牌
eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjpudWxsLCJpZCI6bnVsbCwidXNlcm5hbWUiOiJqaW55b25nIiwiZXhwIjoxNzIxNTE0NzI3fQ.vS6YA_M-9ptBG01ly0ELjti1QvWzs_tlqeHClhh6o-M
2、访问其他路径时,要 在请求头中携带令牌
如果不带:
携带:
拦截器 Interceptor
拦截器是一种动态拦截方法调用的机制,类似于过滤器。是 Spring 框架中提供的,用来动态拦截控制器方法的执行
作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码
使用方法:
1、定义拦截器,实现 Handlerlnterceptor 接口,并重写其所有方法
注意加上注解 @component
2、注册拦截器
流程
登录校验:
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override // 目标资源方法运行前运行,返回 true 代表放行;返回 false 代表不放行
// 登录校验在这里
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 注意形参直接是 HttpServletRequest 和 HttpServletResponse,不需要强制转换
// 获取 url,这段可以不用写,因为在配置文件里规定了不拦截/login
String url = request.getRequestURL().toString();
if (url.contains("login")){
// 登录,放行
return true;
}
// 获取令牌
String jwt = request.getHeader("token");
if (!StringUtils.hasLength(jwt)) {
// 令牌不存在,不放行
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return false;
}
// 验证令牌
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
// 令牌错误,不放行
Result error = Result.error("NOT_LOGIN");
String notLogin = JSONObject.toJSONString(error);
response.getWriter().write(notLogin);
return false;
}
// 令牌正确,放行
return true;
}
@Override // 目标资源方法运行后运行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override // 视图渲染完毕后运行,最后运行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
3 异常处理
使用全局异常处理器(GlobalExceptionHandler):