SpringBootWeb登录认证
为什么在登陆时需要JWT令牌、过滤器Filter、拦截器Interceptor:看代码
登录功能
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 / EmpServiceImpl
EmpService:
/**
* 登录
* @param emp
*/
Emp login(Emp emp);
@Override
public Emp login(Emp emp) {
Emp e = empMapper.getByUsernameAndPassword(emp);
return e;
}
EmpServiceImpl:
//根据用户名及密码查询员工信息
@Select("select * from emp where username = #{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);
EmpServiceImpl:
//根据用户名及密码查询员工信息
@Select("select * from emp where username = #{username} and password = #{password}")
Emp getByUsernameAndPassword(Emp emp);
测试
功能开发完毕后,我们就可以启动服务,打开postman进行测试了。 发起POST请求,访问:
问题分析
在未登录的情况下,我们可以直接在浏览器地址栏访问部门管理、员工管理等功能。
因为我们在实现实现的代码逻辑中,我们根据用户名和密码查询用户,查询到了用户信息,就判定登录成功,我们并没有在服务器或客户端记录任何用户登录成功的标志,而http协议又是无状态协议,那在下一次请求时,我们也无法判断员工是否已经登录。
要解决上述问题,我们需要做两件事
在员工登录成功后,需要将用户登录成功的信息,存起来,记录用户登录成功的标记。
在浏览器发送请求时,放行请求,如果发现登录标记中没有成功的标记,则给前端返回错误的信息,如果有登录成功的信息,就说明用户登录成功,放行请求,如果发现登录标记中没有登录成功的标记则给前端返回错误的信息,跳转至登录界面。
其中:
统一拦截:可以使用两种技术实现,Filter过滤器以及Interceptor拦截器。
登录标记:就需要用户登录成功后,在每一次的请求中,都可以获取到该标记。
此标记需要在用户登录成功之后,每一个请求资源中,都可以获取到,也就是说可以在多次请求间共享。但是我们之前介绍过,HTTP是无状态的,不能在多次请求间共享数据,所以,此处需要使用会话跟踪技术来解决。
对于会话跟踪这四个词,我们需要拆开来进行解释,首先要理解什么是会话,然后再去理解什么是会话跟踪:
会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。
从浏览器发出请求到服务端,响应数据给前端之后,一次会话(在浏览器和服务器之间)就建立了。
会话被建立后,如果浏览器或服务端都没有被关闭,则会话就会持续建立着。
浏览器和服务器就可以继续使用该会话进行请求发送和响应,上述的整个过程就被称之为会话。
用实际场景来理解下会话,比如在我们访问京东的时候,当打开浏览器进入京东首页后,浏览器和京东的服务器之间就建立了一次会话,后面的搜索商品,查看商品的详情,加入购物车等都是在这一次会话中完成。
在现今前前后端分离开发模式下,Cookie、Session这种会话技术已很少使用,而且在服务器集群环境下 以及 客户端多样化的情况下,传统的Cookie、Session的会话方案就显得力不从心了,其主要问题,体现在两个方面:
● 服务端集群环境下Session的共享问题。
● 移动端APP端无法使用Cookie。
JWT令牌
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简洁的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。
官网: https://jwt.io/
标准: https://tools.ietf.org/html/rfc7519
优点:
使用 json 作为数据传输,有广泛的通用型,并且体积小,便于传输;
不需要在服务器端保存相关信息;
jwt 载荷部分可以存储业务相关的信息(非敏感的),例如用户信息、角色等;
JWT令牌由Header、Payload、Signature三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz
第一部分:Herder(头),作用:记录令牌类型,签名算法
{
"alg":"HS256",
"type":"JWT"
}
第二部分:Payload(有效载荷)作用:携带一些用户信息及过期时间
{
"id":"1",
"username":"Tom"
}
第三部分:Signature(签名)作用:防止Token被篡改、确保安全性
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
生成
在 pox.xml引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
生成JWT代码实现
public class JwtDemo {
@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() + 12*3600*1000)) //设置令牌的有效期
.compact();
System.out.println(jwt);
}
}
校验
@Test
public void parseJwt(){
Claims claims = Jwts.parser()
.setSigningKey("itheima")
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjU5OTk1NTE3LCJ1c2VybmFtZSI6IlRvbSJ9.EUTfeqPkGslekdKBezcWCe7a7xbcIIwB1MXlIccTMwo")
.getBody();
System.out.println(claims);
}
注意:JWT下校验时使用的签名密钥,必须和生成JWT令牌时使用的密钥是配套的
如果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)
.getBody();
return claims;
}
}
登录成功,生成JWT令牌并返回
@RestController
public class LoginController {
@Autowired
private EmpService empService;
@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",e.getId());
claims.put("username",e.getUsername());
claims.put("name",e.getName());
//生成JWT令牌
String jwt = JwtUtils.generateJwt(claims);
return Result.success(jwt);
}
return Result.error("用户名或密码错误");
}
}
这样在登录成功后,服务端将生成的令牌已经相应给了我们。通过接口文档的描述,我们也可以看出,登录成功之后,前端会在后面每一次请求中蒋令牌携带过来,那接下来,我么需要做的就是需要在服务端统一拦截校验JWT令牌。
那统一拦截请求,在服务端,我们可以通过两种手段实现过滤器Filter、拦截器Interceptor
过滤器Filter
Filter 过滤器,是 JavaWeb 三大组件(Servlet、Filter、Listener)之一。
过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。
过滤器一般完成一些通用的操作,比如:登陆鉴权、统一编码处理、敏感字符处理等等…
引入json数据处理的工具 .
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.39</version>
</dependency>
登录校验过滤器
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String url = request.getRequestURL().toString();
//如果是login, 直接放行
if(url.contains("login")){
System.out.println("登录操作, 直接放行...");
filterChain.doFilter(req, res);
return;
}
//如果不是 login ,需要校验 token
String token = request.getHeader("token");
if(!StringUtils.hasLength(token)){ //如果没有JWT令牌
System.out.println("获取到token为空 , 返回错误信息...");
//返回 未登录 提示信息
String result = JSONObject.toJSONString(Result.error("NOT_LOGIN"));
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(result);
return ;
}
//解析jwt令牌, 如果解析失败, 则说明令牌无效 , 返回 未登录 提示信息
try {
JwtUtils.parseJWT(token);
System.out.println("令牌解析成功, 直接放行 ...");
} catch (Exception e) {
e.printStackTrace();
System.out.println("令牌解析失败 , 返回错误信息...");
//返回 未登录 提示信息
String result = JSONObject.toJSONString(Result.error("NOT_LOGIN"));
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(result);
return ;
}
//如果校验通过放行
filterChain.doFilter(req, res);
}
}
未登录情况下,服务端响应回来了错误信息 NOT_LOGIN。
拦截器Interceptor
拦截器:(Interceptor)是一种动态拦截方法调用的机制,类似于过滤器。在SpringMVC中动态拦截控制器方法的执行
作用:在指定的方法调用前后执行预先设定的代码,完成功能增强
代码实现
//@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
//目标资源方法执行前执行 , true : 放行 ; false : 不放行,拦截 ;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String url = request.getRequestURL().toString();
//如果是login, 直接放行
if(url.contains("login")){
System.out.println("登录操作, 直接放行...");
return true;
}
//如果不是 login ,需要校验 token
String token = request.getHeader("token");
if(!StringUtils.hasLength(token)){ //如果没有JWT令牌
System.out.println("获取到token为空 , 返回错误信息...");
//返回 未登录 提示信息
String result = JSONObject.toJSONString(Result.error("NOT_LOGIN"));
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(result);
return false;
}
//解析jwt令牌, 如果解析失败, 则说明令牌无效 , 返回 未登录 提示信息
try {
JwtUtils.parseJWT(token);
System.out.println("令牌解析成功, 直接放行 ...");
} catch (Exception e) {
e.printStackTrace();
System.out.println("令牌解析失败 , 返回错误信息...");
//返回 未登录 提示信息
String result = JSONObject.toJSONString(Result.error("NOT_LOGIN"));
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(result);
return false;
}
//如果校验通过放行
return 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 ....");
}
}
Filter 与 Interceptor 区别
接口规范不同:过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。
拦截范围不同:过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。