基础登录功能
要求输入用户名和密码然后从对应的数据库员工表当中查询是否存在对应员工:
查询成功
查看接口文档
响应数据中有一个JWT令牌。
实现思路
新建一个LoginController用于接收登录请求,然后调用EmpService中的借口进行查询操作。
三层架构的代码
Postman测试
成功输出
控制台成功输出
登录校验
问题引出:
上面的登录功能和其他的页面并没有关联,其他页面还是可以通过url直接访问
在登录成功后把网址复制下来然后退出登录,然后把访问复制的网址应该要重定向到登录页面。
概述:
在服务器端接收到浏览器发送的请求后要先进行校验,校验用户是否已经登录,是则执行业务操作,否则响应错误的结果然后跳转回登录页面。
浏览器和服务器之间的连接通过Http进行,是一种无状态的协议,每次请求都独立,因此无法判断是否已经登录。
实现思路
登录成功后存一个标记,在进行业务操作前先确认标记,失败返回错误信息,前端获取到错误信息自动跳转到登录页面。
每一个功能块都需要进行如此操作,过于繁琐,因此使用统一拦截的技术,拦截浏览器发来的所有请求并进行校验。
校验过程就是先获取登录标记,校验通过再分发到相应服务。
会话技术
如上图所示,有三个浏览器和服务器建立起了会话。
服务器会接收多个请求,需要判断是哪些浏览器所发出的,判断的过程就是会话跟踪。
共享数据的例子比如在一个后套管理网站上的登录操作,这是一次独立的请求,但是后面跳转到别的页面时,右上角的个人信息依然存在,这就是多个请求之间的共享数据实现的效果。
实现会话跟踪有三种技术:
Cookie Session 令牌技术
会话跟踪方案一 ——Cookie
在浏览器第一次登录时可以在服务器创建一个Cookie,服务器会自动将cookie响应给浏览器,然后浏览器会自动存储cookie到本地,此后浏览器每次访问都会自动携带着cookie,服务器会查询是否存在该Cookie
在服务器端设置如上两个方法,在浏览器访问第一个方法,返回的数据的响应头如下图所示,已经把cookie给传回来了。
访问c2的资源时的请求头如下图所示,存在刚刚得到的cookie
跨域:目前的开发模式都是前后端分离的开发模式,最终前端程序和后端程序都要分离部署
如下图所示,浏览器访问的前端页面在一个服务器上,在登录会向另一台服务器上发起登录请求,前端和后端的ip地址以及端口号都不一样,可以认为是跨域操作。
出现跨域时不能使用cookie。
会话跟踪方案二 ——Session
Session是服务器端会话跟踪技术,是存储在服务器端,底层是基于cookie实现。
浏览器第一次访问服务器时会在服务器生成一个拥有id的session,然后将sessionId响应给前端。后续浏览器向服务器的每次请求都会携带sessionId,然后服务器会在众多session中找到是否存在对应的session对象
访问第一个资源
下图把两个cookie都传了过去,访问第二个资源
会话跟踪方案三 ——令牌技术
浏览器成功登录后服务器给浏览器一个令牌,此后浏览器每次访问都带上令牌,服务器校验令牌是否有效,这里可以设定令牌有效期为14天,在14天之内就可以直接登录,不需要输入账号密码。
在同一次会话的多次请求之间共享数据就可以将数据存在令牌里面。
因为不需要一定把令牌存在cookie,所以既支持pc端也支持移动端。
登录校验——jwt令牌
简介:
jwt就是将原本的json格式数据进行了安全的封装
应用场景:登录
JWT——生成
引入相关依赖之后可以直接使用jwt提供的工具类jwts进行令牌生成。
令牌生成时需要指定签名算法alg,秘钥secret,还有在jwt令牌中要存储的一些自定义数据。
在官网中给出的一部分签名算法如下。
使用链式编程实现jwt生成,在测试类中定义如下测试方法。
@Test
public void testGenJWT(){
Map<String ,Object> claims=new HashMap<>();
claims.put("id",1);
claims.put("name","yhy");
String jwt=Jwts.builder()
.signWith(HS256,"yhy666") //签名算法
.setClaims(claims)//自定义内容(载荷)
.setExpiration(new Date(System.currentTimeMillis()+3600*1000)) //设置有效期为1小时
.compact(); //获取到生成的jwt令牌
System.out.println(jwt);
}
上面的setExpiration用于设置过期时间,直接传个new Date()相当于直接过期,所以再设置个3600*1000,前面再加上个时间转换毫秒值,相当于3600秒后过期,也就是一个小时
运行输出如下jwt
生成的令牌前面两个部分都可以直接使用base64进行阶解码,最后一个部分是进行数字签名之后的结果。
在官网进行jwt解析。
在java代码中解析JWT令牌
在test中使用如下代码
@Test
public void testParseJwt(){
Claims claims= Jwts.parser()
.setSigningKey("yhy666") //指定签名秘钥
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoieWh5IiwiaWQ" +
"iOjEsImV4cCI6MTY4MDQwMDExNH0.yOhUB_aHwwoOz9898p" +
"B9RSq3SmMXBVqk-eBvLrM8CS0") //传进jwt令牌
.getBody(); //获取令牌三个字段中的中间字段
System.out.println(claims);
}
控制台输出如下,成功获取中间字段
篡改令牌中的字符会爆异常,解析过期令牌也会爆异常
JWT——登录后下发令牌
查看接口文档
前端在以后的每一次登录中都会在请求头中携带jwt令牌,后端只需要在请求头中获取jwt令牌用于校验即可。
操作流程
在utils包下
引入如下工具类
package com.example.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 = "yhy666";
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;
}
}
在loginController当中
修改如下,增加令牌生成返回。
@RestController
@Slf4j
public class LoginController {
@Autowired
private EmpService empService;
@PostMapping("/login")
public Result login(@RequestBody Emp emp){
log.info("员工登录:{}",emp);
Emp e=empService.login(emp);
//登录成功,生成令牌,下发令牌
if(e!=null){
Map<String, Object> claims=new HashMap<>();
claims.put("id",e.getId());
claims.put("name",e.getName());
claims.put("username",e.getUsername());
String jwt=JwtUtils.generateJwt(claims); //将员工id,姓名,用户名都封装到claims中,并装进jwt令牌,有了当前登录的员工信息
return Result.success(jwt);
}
//登录失败,返回错误信息
return Result.error("同户名或者密码错误");
}
}
postman测试 成功输出如下
得到的令牌解析后
前后端联调
登录成功后控制台输出如下
令牌便自动存储到了浏览器本地
在后续的每一次请求当中都会将jwt令牌携带在请求头当中。
以上就是登录成功后生成jwt令牌的操作
登录校验——统一拦截校验
拦截校验有两种主流的技术方案:
过滤器Filter和拦截器Interceptor
在如下这篇文章中介绍,这里下去直接使用
JavaWeb——过滤器Filter和拦截器Interceptor_北岭山脚鼠鼠的博客-CSDN博客
登录校验Filter
结合上面的jwt令牌和过滤器Filter,可以在过滤器当中进行令牌的有效性校验操作,如果无效则由Filter返回一个错误信息.
在接口文档中
流程分析
在filter包下新建一个LoginCheckFilter类如下
@Slg4j用于日志记录
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//1.获取请求url。
//2.判断请求url中是否包含login,如果包含,说明是登录操作,放行。
//3.获取请求头中的令牌(token)。
//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)。
//5.解析token,如果解析失败,返回错误结果(未登录)。
//6.放行。
}
}
在Http请求中,请求参数都在HttpServlet,所以要将request先强转为HttpServletRequest,response也要强转。
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
1.获取url
String url = req.getRequestURL().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.判断令牌是否存在,如果不存在,返回错误结果(未登录)。
这里借助StringUtils工具类的方法hasLength判断字符串是否有长度
没有则说明没有令牌,需要返回规定格式的错误信息。
要响应的是一个json格式的数据,原本在Controller中响应时是可以借助一个Result的类进行实现,但是这里在过滤器当中就只能自己手动转换了
借助于阿里巴巴提供的fastJSON,
先引入fastJSON的依赖
<!--fastJSON-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
然后直接调用fastJson提供的api将一个对象转成json格式的字符串,然后就可以将该字符串写回响应数据流当中了。
if(!StringUtils.hasLength(jwt)){
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
5.解析token,如果解析失败,返回错误结果(未登录)。
这里如果解析失败了会报异常,所以使用一个try - catch用来捕获异常,返回未登录的错误信息。如果过解析成功就放行。
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {//jwt解析失败
e.printStackTrace();
log.info("解析令牌失败, 返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return;
}
6.放行
log.info("令牌合法, 放行");
chain.doFilter(request, response);
postman测试
登录操作测试
fliter直接放行
查询操作测试
将登录操作过后得到jwt令牌放到查询部门操作里面的请求头当中进行测试,可以看见后端服务器也成功让查询部门的请求通过了
前端测试
先把浏览器里面的令牌信息清空
然后刷新页面后会强制跳转登录界面
登录完成后又会得到一个新的令牌
服务器控制台输出如下。
到这里,使用Filter完成拦截校验的操作已经全部结束
拦截器Interceptor
相关入门在下面这里,这里直接使用
JavaWeb——过滤器Filter和拦截器Interceptor_北岭山脚鼠鼠的博客-CSDN博客
实现流程和过滤器的流程完全一致
在定义好的拦截器中的preHandle方法中实现登录校验,并决定返回值
修改后的preHandle方法如下
@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.判断请求url中是否包含login,如果包含,说明是登录操作,放行。
if(url.contains("login")){
log.info("登录操作, 放行...");
return true;
}
//3.获取请求头中的令牌(token)。
String jwt = req.getHeader("token");
//4.判断令牌是否存在,如果不存在,返回错误结果(未登录)。
if(!StringUtils.hasLength(jwt)){
log.info("请求头token为空,返回未登录的信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return false;
}
//5.解析token,如果解析失败,返回错误结果(未登录)。
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {//jwt解析失败
e.printStackTrace();
log.info("解析令牌失败, 返回未登录错误信息");
Result error = Result.error("NOT_LOGIN");
//手动转换 对象--json --------> 阿里巴巴fastJSON
String notLogin = JSONObject.toJSONString(error);
resp.getWriter().write(notLogin);
return false;
}
//6.放行。
log.info("令牌合法, 放行");
return true;
}
关闭所有的Filter只剩interceptor后做测试
Postman测试
在查询操作中带上token后成功通过拦截器
前后端联调
登录后所有页面的数据都可以正常显示
直接复制页面的地址后退出再访问就会自动跳转到登录页面
到这里位置,登录校验拦截器的开发完成。
异常处理
点击新建部门创建一个已经存在的部门后响应状态码为500,说明服务器端出现问题
这里报错是出现重复属性,因为建立部门表时为部门名字设置了唯一约束
响应的数据不是统一规范的result数据格式
在三层架构中mapper遇到异常会抛给service,service又会抛会给controller。
解决方案:
新建一个exception包下GlobalExceptionHandle 类
@RestControllerAdvice //自动将返回的Result数据格式转换成JOSN数据格式
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class) //表明需要捕获的异常,这里是所有的异常
public Result ex(Exception ex)
{
ex.printStackTrace(); //输出异常的堆栈信息
return Result.error("杂鱼杂鱼,你操作失败了涅");
}
}
重启项目测试
继续新建同名部门后页面的错误提示成功显示,响应数据的格式也对了