实现登录校验功能

登录校验功能的实现有许多种方案,包括传统的CookieSession,以及目前被广泛采用的令牌技术之一JWT( Json Web Token ).目前主要学习JWT的来完成登录校验,Cookie和Session仅作为了解。

通过Spring提供的拦截器 Interceptor 拦截请求,配合JWT校验来决定是否放行

1.会话追踪方案

1.1Cookie

Cookie是Web服务器发送给客户端的一小段信息,客户端请求时,可以读取该信息发送到服务器端。

1.1.1 使用cookie

存入Cookie使用HttpSercletResponseaddCookie

取出Cookie使用HttpServletRequestgetCookies,再进行遍历得到单个cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
public class TestController {

//设置Cookie
@GetMapping("/cookie1")
public Result cookie1(HttpServletResponse response){
response.addCookie(new Cookie("login_userid","123456"));
return Result.success();
}

//获取Cookie
@GetMapping("/cookie2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for (Cookie cookie : cookies) {
System.out.println("cookie:"+cookie.getValue());
}
return Result.success();
}

}

当访问cookie1接口时可以,打开控制台可以看到存储在本地浏览器的cookie

而当前端请求获取cookie,服务器端也可以通过HttpServletRequest获取存储在浏览器中的cookie

1.1.2 优缺点

优点:Cookie是HTTP协议中支持的技术

缺点:

  • 移动端APP无法使用
  • Cookie不安全,用户可以自己禁用Cookie
  • Cookie不能跨域

1.2Session

Session是基于Cookie来工作的,同一个客户端每次访问服务器时,只要当浏览器在第一次访问服务器时,服务器设置一个id并保存一些信息(例如登陆就保存用户信息,视具体情况),并把这个id通过Cookie存到客户端,客户端每次和服务器交互时只传这个id,就可以实现维持浏览器和服务器的状态,而这个ID通常是NAME为JSESSIONID的一个Cookie。

在Java中是通过调用HttpServletRequest的getSession方法(使用true作为参数)创建的,存储在服务器的内存中,tomcat默认保存30分钟。

1.2.1 使用Session

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//往HttpSession中存储值
@GetMapping("/session1")
public Result session1(HttpSession session){
session.setAttribute("login_userid_2","654321");
return Result.success();
}

//从HttpSession中获取值
@GetMapping("/session2")
public Result session2(HttpServletRequest request){
HttpSession session = request.getSession();
Object value = session.getAttribute("login_userid_2");
return Result.success(value);
}

当访问session1时可以看到存在一个名为JSESSIONID的Cookie,这个就是此次会话的id

通过这个session可以在服务器中获取其中的属性

1.2.2 优缺点

优点:由于session中的数据存储在服务器中,只有传输一个id,因此数据安全性更高

缺点:

  • 基于Cookie,所以Cookie存在的问题依然存在
  • Session在高访问量的情况下,对于服务器的存储压力大
  • 无论是Cookie还是Session,在服务器集群环境下需要解决数据共享的问题

1.3 令牌技术

1.3.1 概述

令牌(Token)就是系统的临时密钥,相当于账户名和密码,用来决定是否允许这次请求和判断这次请求时属于哪一个用户的.它允许你不提供密码或其他凭证的前提下,访问网络和系统资源.这些令牌将持续存在于系统中,除非系统重新启动.

JWT 是目前主流的令牌技术,简洁、自包含的格式,用于在通信双方以JSON数据格式安全的传输信息。由于数字签名的存在,这些信息都是可靠的。

组成:

  • 第一部分:Header(头),记录令牌类型、签名算法等。

    ​ 例如:[“alg”:“HS256”,“type”:”WT”)

  • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。

    ​ 例如:“id”.”1”“username”.”Tom”

  • 第三部分: Signature(签名),防止Token被篡改、确保安全性。

    ​ 将header、payload,并加入指定秘钥,通过指定签名算法计算而来。

    ​ 第三部分字符的由来不是Base64编码方式生成的,而是前面指定的签名算法

1.3.2 优缺点

优点:

  • 支持PC端、移动端
  • 解决集群环境下的认证问题
  • 减轻服务器端存储压力

缺点: 需要自己手动实现

2.JWT实现登录校验的过程

用户登录成功后,系统会下发JWT令牌,然后在后续的每次请求中,前端都需要在请求头header中携带JWT令牌到服务端,请求头的名称为 token,值为登录时下发的JWT令牌。

服务端拦截所有需要登录才能访问的请求,通过

  • request.getParameter(“token”)
  • request.getHeader(“token”)
  • public Result xxx(@RequestHeader(“token”) String token)

获得登录时下发的JWT令牌。

如果检测到用户未登录,则会返回错误信息。

3.JWT的使用

3.1 导入依赖

1
2
3
4
5
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

3.2 生成JWT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@GetMapping("/genJwt")
public Result genJwt(){
//自定义内容,通过Map存入
Map<String, Object> claims =new HashMap<>();
claims.put("login_userid_3","123456789");

String jwt = Jwts.builder()
//自定义内容(载荷)
.setClaims(claims)
//签名算法、解析密钥
.signWith(SignatureAlgorithm.HS256, "This_is_a_key")
//设定过期时间
.setExpiration(new Date(System.currentTimeMillis() + 30 * 60 * 1000))
//生成
.compact();

return Result.success(jwt);
}

3.3校验JWT

在此过程中,JWT令牌若发生改变,则校验不会通过,而过期也同样不会通过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@GetMapping("/checkJwt")
public Result checkJwt(){
//模拟前端传入的JWT
String test_jwt = "xxx.xxx.xxx";
//解析
try{
Claims claims = Jwts.parser()
//解析需要之前设置的 解析密钥
.setSigningKey("This_is_a_key")
.parseClaimsJws(test_jwt)
.getBody();

return Result.success(claims);
}catch (Exception e){
return Result.error("Jwt令牌错误");
}
}

3.4 封装为工具类

为方便使用,封装为工具类以便多次调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;

public class JwtUtil {
/**
* 定义两个常量:过期时间(ms)和 解析密钥
*/
public static final long EXPIRE = 1000 * 60 * 60;
public static final String SECRET = "this_is_a_secret_key";
/**
* 生成 JWT令牌
* @param claims JWT第二部分负裁 payload 中存储的内容,即自定义内容
* @return
*/
public static String generateJwt(Map<String,Object> claims){
claims.put("login_userid","123456789");

String jwt = Jwts.builder()
//自定义内容(载荷)
.setClaims(claims)
//签名算法、解析密钥
.signWith(SignatureAlgorithm.HS256, SECRET)
//设定过期时间
.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(SECRET)
//解析为Claims
.parseClaimsJws(jwt)
//返回Claims
.getBody();

return claims;
}
/**
* 检验jwt令牌
* @param jwt 需要解析的jwt令牌
* @return true:通过 false:不通过
*/
public static Boolean checkJwt(String jwt){
if (StringUtils.isBlank(jwt)){
return false;
}

try {
parseJwt(jwt);
return true;
}catch (Exception e){
return false;
}
}

}

4.Interceptor拦截器

4.1 拦截器概述

Spring MVC 中的拦截器 Interceptor 类似于Servlet 中的过滤器 Filter,它主要用于拦截用户请求并做相应的处理。例如通过拦截器可以进行权限验证、记录请求信息的日志、判断用户是否登录等。

4.2 使用

4.2.1 定义拦截器

首先需要自定义一个类,放在interceptor包下

实现HandlerInterceptor接口,然后ctrl+o可以调出重写方法的窗口,重写preHandle()、postHandle()、afterCompletion(),最后在这个类上加上@Component注解,交给IOC容器管理

其中

  • preHandle()是目标资源方法执行前执行,返回true放行,返回false拦截
  • **postHandle()**目标资源方法执行后执行
  • **afterCompletion()**视图渲染完毕后执行,最后执行

4.2.2 注册拦截器

创建一个配置类,重写addInterceptors()方法,注入刚才定义的拦截器,然后通过registry添加拦截器以及配置拦截路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.sky.interceptor.DemoInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import javax.annotation.Resource;

@Configuration
public class WebConfig extends WebMvcConfigurationSupport {

@Resource
@Autowired
private DemoInterceptor demoInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(demoInterceptor)//添加拦截器
.addPathPatterns("/**")//拦截所有请求
.excludePathPatterns("/login");//排除 /login 的请求
}
}

完成定义和注册后,如以上配置,页面将只能访问 /login ,其他请求将会被拦截

4.2.3 拦截器完成登录校验

在拦截器的preHandle()方法中添加以下代码

通过校验JWT令牌来选择是否放行请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override   //目标资源方法执行前执行,返回true放行,返回false拦截
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {

//1.获取请求的url
String url = request.getRequestURL().toString();
//2.通过url判断,如果是登录操作就放行
if(url.contains("login")){
return true;
}
//3.获取请求头中的令牌
String jwt = request.getHeader("token");
System.out.println("获取的jwt:"+jwt);
//4.校验jwt,如果通过就放行
if ( JwtUtil.checkJwt(jwt) ){
return true;
}else{
return false;
}

}