登录认证
登录接口
1.查看原型
2.查看接口
3.思路分析
登录核心就是根据用户名和密码查询用户信息,存在则登录成功, 不存在则登录失败
4.Controller
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;
/**
* 登录的方法
*
* @param emp
* @return
*/
@PostMapping("/login")
public Result login(@RequestBody Emp emp) {
log.info("员工登录:{}", emp);
Emp e = empService.login(emp);
return e != null ? Result.sucess():Result.error("用户名或密码错误");
}
}
5.Server
/**
* 员工管理
*/
public interface EmpService {
/**
* 员工登录
* @param emp
*/
Emp login(Emp emp);
}
@Service
public class EmpServiceImpl implements EmpService {
@Autowired
private EmpMapper empMapper;
/**
* 员工登录
* @param emp
* @return
*/
@Override
public Emp login(Emp emp) {
return empMapper.getByUsernameAndPassword(emp);
}
}
6.Mapper
/**
* 员工管理
*/
@Mapper
public interface EmpMapper {
/**
* 根据用户名和密码查询账户信息
* @param emp
* @return
*/
@Select("select * from emp where username = #{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);
}
7.接口测试
登录校验
前端的所有请求, 都需要经过登录判断, 登录了进行正常操作, 没登录则返回错误信息, 让用户去登录页
会话跟踪
会话: 用户打开浏览器,访问web服务器的资源,会话建立,直到一方断开连接,会话结束, 在一次会话中, 可以包含多次请求和响应
会话跟踪: 一种维护浏览器状态的方法, 服务器需要识别多次请求是否来自同一浏览器, 以便在同一次会话的多次请求间共享数据
Cookie
客户端会话跟踪技术
- 优点:
- HTTP协议支持的技术
- 服务器返回Cookie / 浏览器保存Cookie / 客户端携带Cookie 都是自动完成的
- 缺点:
- 移动端APP无法使用Cookie
- 不安全, 用户可以自己禁用Cookie
- Cookie不能跨域
- 当前地址和请求地址的 协议, 域名和端口 出现不同, 就形成了跨域
示例
@Slf4j
@RestController
public class SessionController {
//设置Cookie
@GetMapping("/c1")
public Result cookie(HttpServletResponse response){
//通过HttpServletResponse获取响应对象
//设置Cookit的name和value
response.addCookie(new Cookie("login_username","itheima"));
return Result.success();
}
//获取Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
//获取所有的Cookie
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
if(cookie.getName().equals("login_username")) {
//找到name为login_username的Cookie
System.out.println("login_username:" + cookie.getValue());
}
}
return Result.success();
}
}
- 后端只要设置了cookie, hppt就会自动在响应头的Set-Cookie属性中携带cookie信息
- 浏览器解析http时, 如果发现了Set-Cookie信息, 就会把信息自动储存在本地
- 当下次请求时, 就会在请求头的Cookie属性中自动携带该cookie信息
Session
服务端会话跟踪技术
- Session是基于Cookie实现的
- Cooike直接把数据存储在浏览器, 不安全
- Session是把Seeion ID存在在浏览器, 数据存储在服务器
- 优点: 数据存储在服务端, 安全
- 缺点:
- Session基于Cookie实现,也继承了其缺点
- 服务器集群环境下无法直接使用Session
示例
@Slf4j
@RestController
public class SessionController {
// 往HttpSession中存储值
@GetMapping("/s1")
public Result session1(HttpSession session) {
log.info("HttpSession-s1:{}", session.hashCode());
//往session中存储值
session.setAttribute("loginUser", "tom");
return Result.success();
}
// 从HttpSession中获取值
@GetMapping("/s2")
public Result session2(HttpServletRequest request) {
HttpSession session = request.getSession();
log.info("HttpSession-s2:{}", session.hashCode());
//从session中获取数据
Object loginUser = session.getAttribute("loginUser");
log.info("loginUser:{}", loginUser);
return Result.success(loginUser);
}
}
- 后端只要设置了Session, hppt就会自动在响应头的Set-Cookie属性中携带Session ID信息
- 浏览器解析http时, 如果发现了Set-Cookie信息, 就会把信息自动储存在本地
- 当下次请求时, 就会在请求头的Cookie属性中自动携带该Session ID信息
JWT令牌
介绍
全称 JSON Web Token, 定义了一种简洁的, 自包含的格式, 用于在通信双方以JSON数据格式安全的传输信息, 由于数字签名的存在, 这些信息是可靠的
- 官网: JSON Web Tokens - jwt.io
- 执行流程
- 登录成功后, 生成令牌, 响应给前端, 前端保存起来
- 前端的后续请求, 都携带JWT令牌
- 后端在处理请求之前, 先校验令牌, 再处理业务
- 优缺点
- 支持PC/移动端/小程序
- 解决集群环境下的认证问题
- 减轻服务器存储压力
- 需要自己实现
JWT令牌本质是JSON对象, JSON对象储存数据, 经过Base64编码后, 在拼接上数字签名就形成了JWT令牌
- Base64: 是一种基于64个可打印字符(A-Z a-z 0-9 + /) 来表示二进制数据的编码方式
生成与校验
// JWT依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
@Test
public void testGenJwt(){
Map<String,Object> claims = new HashMap<>();
claims.put("id",1);
claims.put("name","tom");
// 生成JWT令牌
String jwt = Jwts.builder()
.signWith(SignatureAlgorithm.HS256,"itheima") //指定签名算法
.setClaims(claims) //自定义内容(载荷)
.setExpiration(new Date(System.currentTimeMillis()+3600*1000)) //设置有效期为1H
.compact();
System.out.println(jwt);
}
@Test
public void testParseJWT(){
// 解析JWT令牌
Claims claims = Jwts.parser()
.setSigningKey("itheima")
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoidG9tIiwiaWQiOjEsImV4cCI6MTY5NjE0OTc2Nn0.7-rlCqZQ1XssTdNOM2os0s-mpuKHzUkwFWtsvMOlalY")
.getBody();
System.out.println(claims);
}
- JWT校验时使用的签名秘钥, 必须和生成JWT令牌时使用的秘钥配套
- 如果JWT令牌解析时报错, 说明JWT令牌被篡改 或者 失效了
登录校验-下发令牌
引入jwt依赖
// JWT依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
引入jwt工具类
public class JwtUtils {
//指定签名秘钥
private static String signKey = "itheima";
//指定过期时间(12h)
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)
.getBody();
return claims;
}
}
生成jwt令牌, 并返回给前端
@Slf4j
@RestController
public class LoginController {
@Autowired
private EmpService empService;
/**
* 登录的方法
*
* @param emp
* @return
*/
@PostMapping("/login")
public Result login(@RequestBody Emp emp) {
log.info("员工登录:{}", emp);
Emp e = empService.login(emp);
//登录成功,生成jwt令牌,并下发令牌
if(e!=null) {
//把用户信息存入Map集合
Map<String,Object> claims = new HashMap<>();
claims.put("id",e.getId());
claims.put("name",e.getName());
claims.put("username",e.getUsername());
//生成jwt令牌
String jwt = JwtUtils.generateJwt(claims); //jwt令牌包含了用户信息
// 下发令牌
return Result.success(jwt);
}
//登录失败,返回错误信息
return Result.error("用户名或密码错误");
}
}
接口测试
过滤器
快速入门
filter过滤器是 javaWeb 三大组件( Servler / Filter / Listener )之一
- 过滤器可以拦截请求, 从而实现特殊功能
- 过滤器一般完成一些通用操作, 比如登录校验, 统一编码处理, 敏感字处理等
定义过滤器: 定义一个类, 实现Filter接口, 重写其所有方法
// 设置拦截请求的路径
@WebFilter(urlPatterns = "/*")
public class DemoFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
//初始化的方法,服务器启动时只调用一次
System.out.println("服务器启动了");
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 拦截到请求之后调用,调用多次
System.out.println("拦截到请求了");
// 设置放行, 不放行前端拿不到响应结果
filterChain.doFilter(servletRequest,servletResponse);
// 放行后的代码执行完, 还会回到拦截器
System.out.println("放行后的逻辑");
}
@Override
public void destroy() {
//销毁的方法,服务器关闭时只调用一次
System.out.println("服务器关闭了");
Filter.super.destroy();
}
}
- 注意Filter是 javax.servlet 下的包
- 实际业务中, init 方法和 destroy方法不需要重写, 使用默认实现即可
配置过滤器: 过滤器是JavaWeb组件, 不是Springboot组件, 所以需在再启动类进行配置, 才能生效
//开启对servlet组件的支持
@ServletComponentScan
@SpringBootApplication
public class TliasWebManagementApplication {
public static void main(String[] args) {
SpringApplication.run(TliasWebManagementApplication.class, args);
}
}
详解
拦截器的执行流程
- 请求被拦截后, 先执行放行前的逻辑, 符合条件放行
- 方形后访问资源, 资源访问完成后还会回到过滤器中
- 回到过滤器后, 会执行过滤器放行后的逻辑
根据需求, 设置过滤器器的拦截路径
过滤器链: 一个web应用中, 可以配置多个过滤器,多个过滤器形成过滤器链
- 顺序: 定义多个过滤器之后, 过滤器生效的优先级, 是按照过滤器类名(字符串)的自然排序
登录校验-Filter
使用过滤器完成前端请求的登录校验
- 除了登录请求, 所有的请求都要进行令牌校验
- 拦截请求之后, token存在并且通过校验, 才能继续访问资源, 否则返回登录错误
登录校验实现的步骤
// 登录校验过滤器
@Slf4j
@WebFilter(urlPatterns = "/*")
public class loginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 1,获取url
String url = req.getRequestURI().toString();
log.info("请求到的url: {}", url);
// 2,判断url是否包含'login'
if (url.contains("login")) {
log.info("登录操作, 放行...");
chain.doFilter(request, response);
return;
}
// 3, 获取请求头中的token
String jwt = req.getHeader("token");
// 4, 判断令牌是否存在
// 使用springframework提供的工具类
if (!StringUtils.hasLength(jwt)) {
log.info("请求头token为空");
Result error = Result.error("NOT_LOGIN");
// 手动转换 把 对象-> JSON ------->使用阿里巴巴fastJSON插件
String notLogin = JSON.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
// 5, 解析token
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
// 解释失败
log.info("解释令牌失败");
Result error = Result.error("NOT_LOGIN");
String notLogin = JSON.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
// 6, 放行
log.info("令牌合法, 放行");
chain.doFilter(request, response);
}
}
- 在springbott中, 响应的数据会被自动封装为JSON对象
- 在过滤器中, 我们要手动把数据转成JSON, 才能响应给前端
// fastjson依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
接口测试
拦截器
快速入门
定义拦截器: 实现HandlerIntereptor接口, 并重写其所有方法
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override //目标资源方法运行前运行, 返回true放行,反回false不放行
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
System.out.println("目标资源方法运行前运行");
return true;
}
@Override //目标资源方法运行后运行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("目标资源方法运行后");
}
@Override //视图渲染完毕后运行,最后运行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("视图渲染完毕后");
}
}
- 拦截器的所有方法都有默认实现, 定义拦截器时按需实现接口的方法即可
注册拦截器
@Configuration //标明配置类
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器,并指定监听路径
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**")
}
}
详解
配置拦截路径: 拦截器可以根据需求, 配置不同的拦截路径
拦截器的执行流程
过滤器和拦截器的区别
登录校验-Interceptor
使用拦截器完成登录校验, 登录校验的代码的逻辑是不变的, 只是放行方式不同
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override //目标资源方法运行前运行, 返回true放行,反回false不放行
public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception {
//1,获取请求URL
String url = req.getRequestURL().toString();
log.info("请求的URL:{}", url);
//2,判断是否包含login,登录操作直接放行
if (url.contains("login")) {
log.info("登录操作,放行");
return true;
}
//3,获取请求头的令牌
String jwt = req.getHeader("token");
//4,判断令牌是否存在,如果不存在返回错误信息
if (!StringUtils.hasLength(jwt)) {
log.info("请求头token信息为空");
// 创建错误对象
Result error = Result.error("NOT_LOGIN");
// 手动转换 对象->JSON
String notLogin = JSONObject.toJSONString(error);
// 响应数据
resp.getWriter().write(notLogin);
// 阻止代码继续往下执行
return false;
}
//5,解析token,如果解析失败则token无效
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
// 输出错误日志
e.printStackTrace();
log.info("解析令牌失败,返回错误信息");
// 创建错误对象
Result error = Result.error("NOT_LOGIN");
// 手动转换 对象->JSON
String notLogin = JSONObject.toJSONString(error);
// 响应数据
resp.getWriter().write(notLogin);
// 阻止代码继续往下执行
return false;
}
//6,token合法,放行
log.info("token合法,放行");
return true;
}
}
@Configuration //标明配置类
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginCheckInterceptor loginCheckInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器,并指定监听路径,排除监听路径
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");
}
}