5. 实现登陆
分析
传统思路:
1、 登陆⻚⾯把⽤⼾名密码提交给服务器.
2、服务器端验证⽤⼾名密码是否正确, 并返回校验结果给后端
3、如果密码正确, 则在服务器端创建 Session . 通过 Cookie 把 sessionId 返回给浏览器.
问题:
集群环境下⽆法直接使⽤Session.
原因分析:
我们开发的项⽬, 在企业中很少会部署在⼀台机器上, 容易发⽣单点故障. (单点故障: ⼀旦这台服务器挂 了, 整个应⽤都没法访问了). 所以通常情况下, ⼀个Web应⽤会部署在多个服务器上, 通过Nginx等进⾏负载均衡,此时, 来⾃⼀个⽤⼾的请求就会被分发到不同的服务器上,请求逻辑如下所示:
假如我们使⽤Session进⾏会话跟踪, 会出现下面的场景:
1. ⽤⼾登录 ⽤⼾登录请求, 经过负载均衡, 把请求转给了第⼀台服务器, 第⼀台服务器进⾏账号密码 验证, 验证成功后, 把Session存在了第⼀台服务器上
2. 查询操作 ⽤⼾登录成功之后, 携带Cookie(⾥⾯有SessionId)继续执⾏查询操作, ⽐如查询博客列 表. 此时请求转发到了第⼆台机器, 第⼆台机器会先进⾏权限验证操作(通过SessionId验证⽤⼾是否 登录), 此时第⼆台机器上没有该⽤⼾的Session, 就会出现问题,来提⽰⽤⼾登录, 这样就会给用户带来不好的体验;该场景的逻辑如下所示:
接下来我们介绍另一种⽅案: 令牌技术
5.1 令牌技术
令牌其实就是⼀个⽤⼾⾝份的标识, 其实本质就是⼀个字符串. ⽐如我们出⾏在外, 会带着⾃⼰的⾝份证, 需要验证⾝份时, 就掏出⾝份证⾝份证不能伪造, 可以辨别真假 ;其运行逻辑如下所示:
服务器具备⽣成令牌和验证令牌的能⼒
我们使⽤令牌技术, 继续思考上述场景:
1. ⽤⼾登录 ⽤⼾登录请求, 经过负载均衡, 把请求转给了第⼀台服务器, 第⼀台服务器进⾏账号密码 验证, 验证成功后, ⽣成⼀个令牌, 并返回给客⼾端.
2. 客⼾端收到令牌之后, 把令牌存储起来. 可以存储在Cookie中, 也可以存储在其他的存储空间(⽐如 localStorage)
3. 查询操作 ⽤⼾登录成功之后, 携带令牌继续执⾏查询操作, ⽐如查询博客列表. 此时请求转发到了 第⼆台机器, 第⼆台机器会先进⾏权限验证操作. 服务器验证令牌是否有效, 如果有效, 就说明⽤⼾已 经执⾏了登录操作, 如果令牌是⽆效的, 就说明⽤⼾之前未执⾏登录操作
令牌的优缺点
优点:
• 解决了集群环境下的认证问题
• 减轻服务器的存储压⼒(⽆需在服务器端存储)
缺点:
需要⾃⼰实现(包括令牌的⽣成, 令牌的传递, 令牌的校验)
当前企业开发中, 解决会话跟踪使⽤最多的⽅案就是令牌技术.
5.2 JWT令牌
令牌本质就是⼀个字符串, 他的实现⽅式有很多, 我们采⽤⼀个JWT令牌来实现.
5.2.1 介绍
JWT全称: JSON Web Token
JSON Web Token(JWT)是⼀个开放的⾏业标准(RFC 7519), ⽤于客⼾端和服务器之间传递安全可靠的信息. 其本质是⼀个token, 是⼀种紧凑的URL安全⽅法.
5.2.2 JWT组成
JWT由三部分组成, 每部分中间使⽤点 (.) 分隔,⽐如:aaaaa.bbbbb.cccc
• Header(头部) 头部包括令牌的类型(即JWT)及使⽤的哈希算法(如HMAC SHA256或RSA)
• Payload(负载) 负载部分是存放有效信息的地⽅, ⾥⾯是⼀些⾃定义内容. ⽐如: {"userId":"123","userName":"shenemngyao"} , 也可以存在jwt提供的现场字段, ⽐如 exp(过期时间戳)等. 此部分不建议存放敏感信息, 因为此部分可以解码还原原始内容.
• Signature(签名) 此部分⽤于防⽌jwt内容被篡改, 确保安全性. 防⽌被篡改, ⽽不是防⽌被解析. JWT之所以安全, 就是因为最后的签名. jwt当中任何⼀个字符被篡改, 整个令牌都会校验失败. 就好⽐我们的⾝份证, 之所以能标识⼀个⼈的⾝份, 是因为他不能被篡改, ⽽不是因为内容加密.(任 何⼈都可以看到⾝份证的信息, jwt 也是)
对上⾯部分的信息, 使⽤Base64Url 进⾏编码, 合并在⼀起就是jwt令牌 Base64是编码⽅式,⽽不是加密⽅式
5.3 JWT令牌⽣成和校验
5.3.1. 引⼊JWT令牌的依赖
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<!-- or jjwt-gson if Gson is preferred -->
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<!-- or jjwt-gson if Gson is preferred -->
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
5.3.2. JWT令牌的⽣成和校验
使⽤Jar包中提供的API来完成JWT令牌的⽣成和校验,代码如下:
package com.example.spring_blog_24_9_8;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import org.junit.jupiter.api.Test;
import javax.crypto.SecretKey;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JWTUtisTest {
//过期时间: 1小时的毫秒数
private final static long EXPIRATION_DATE = 60 * 60 * 1000;
//秘钥
private final static String secretString = "MDDOvlf8sQ675YuOsOxu45EXYK1dl/PoAyJq9C8vta0=";
//生成安全秘钥
private final static Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));
//生成令牌
@Test
public void genToken(){
//⾃定义信息
Map<String, Object> claim = new HashMap<>();
claim.put("userId", 1);
claim.put("userName", "shenmengyao");
String token = Jwts.builder()
.setClaims(claim) //⾃定义内容(负载
.setExpiration(new Date(System.currentTimeMillis()+EXPIRATION_DATE))//设置过期时间
.signWith(key)
.compact();
System.out.println(token);
}
//生成key
@Test
public void genKey(){
//生成key
SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String encode = Encoders.BASE64.encode(secretKey.getEncoded());
System.out.println(encode);
}
//校验令牌
@Test
public void parseToken(){
String token = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6InNoZW5tZW5neWFvIiwidXNlcklkIjoxLCJleHAiOjE3MjU5NTMzMTl9.Aa7kKOH6vJFo09eEYCLtGCqRh6lBiXucRh-ze-F-IY8";
JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();
Claims body = null;
try {
body = build.parseClaimsJws(token).getBody();
} catch (Exception e) {
System.out.println("令牌校验失败");
}
System.out.println(body);
}
}
生成密钥:
生成令牌:
我们把⽣成的令牌通过官⽹进⾏解析, 就可以看到我们存储的信息了:
令牌校验:
令牌解析后, 我们可以看到⾥⾯存储的信息,如果在解析的过程当中没有报错,就说明解析成功了. 令牌解析时, 也会进⾏时间有效性的校验, 如果令牌过期了, 解析也会失败. 修改令牌中的任何⼀个字符, 都会校验失败, 所以令牌⽆法篡改
学习令牌的使⽤之后, 接下来我们通过令牌来完成⽤⼾的登录
1. 登陆⻚⾯把⽤⼾名密码提交给服务器.
2. 服务器端验证⽤⼾名密码是否正确, 如果正确, 服务器⽣成令牌, 下发给客⼾端.
3. 客⼾端把令牌存储起来(⽐如Cookie, local storage等), 后续请求时, 把token发给服务器
4. 服务器对令牌进⾏校验, 如果令牌正确, 进⾏下⼀步操作
5.4 约定前后端交互接⼝
[请求]
/user/login
username=shenmengyao&password=111111
[响应]
{
"code": 200,
"msg": "",
"data":
"eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJ6aGFuZ3NhbiIsImlhdCI6MTY5ODM5N
zg2MCwiZXhwIjoxNjk4Mzk5NjYwfQ.oxup5LfpuPixJrE3uLB9u3q0rHxxTC8_AhX1QlYV--E"
}
//验证成功, 返回token, 验证失败返回 ""
5.5 实现服务器代码
创建JWT⼯具类
package com.example.spring_blog_24_9_8.utils;
import com.example.spring_blog_24_9_8.constants.Constant;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import java.security.Key;
import java.util.Date;
import java.util.Map;
@Slf4j
public class JwtUtils {
//过期时间: 1小时的毫秒数
private final static long EXPIRATION_DATE = 600000 * 60 * 1000;
private final static String secretString = "2LT0G2Og7zG9aOPJR+Y4cLMGicyUoRVPS4J0xRlpZzg=";
private final static Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));
//生成令牌
public static String genToken(Map<String, Object> claim){
return Jwts.builder()
.setClaims(claim)
.setExpiration(new Date(System.currentTimeMillis()+EXPIRATION_DATE))
.signWith(key)
.compact();
}
/**
* 解析令牌
* @param token
* @return
*/
public static Claims parseToken(String token){
JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();
Claims body = null;
try {
body = build.parseClaimsJws(token).getBody();
} catch (ExpiredJwtException e) {
log.error("token过期, 校验失败, token:",token);
} catch (Exception e) {
log.error("token校验失败, token:",token);
}
return body;
}
//校验令牌
public static boolean checkToken(String token){
Claims body = parseToken(token);
if (body==null){
System.out.println("555");
return false;
}
System.out.println("444");
return true;
}
public static Integer getUserIdFromToken(String token){
Claims body = parseToken(token);
if (body!=null){
System.out.println("222");
return (Integer) body.get("id");
}
System.out.println("111");
return null;
}
}
创建 UserController
package com.example.spring_blog_24_9_8.controller;
import com.example.spring_blog_24_9_8.model.Result;
import com.example.spring_blog_24_9_8.model.User;
import com.example.spring_blog_24_9_8.service.UserService;
import com.example.spring_blog_24_9_8.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public Result login(HttpServletRequest request, HttpServletResponse
response, String username, String password){
if (!StringUtils.hasLength(username) ||
!StringUtils.hasLength(password)){
log.error("username:"+username+",password:"+password);
return Result.fail(-1, "⽤⼾名或密码为空");
}
//判断账号密码是否正确
User user = userService.getUserInfo(username);
if (user==null || !user.getPassword().equals(password)){
return Result.fail(-1, "⽤⼾名或密码错误");
}
//登录成功, 返回token
Map<String , Object> claims = new HashMap<>();
claims.put("id", user.getId());
claims.put("username", user.getUserName());
String token = JwtUtils.genToken(claims);
System.out.println("⽣成token:"+ token);
return Result.success(token);
}
}
userservice:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User getUserInfo(String username){
return userMapper.selectByName(username);
}
}
5.6 实现客⼾端代码
修改 login.html, 完善登录⽅法 前端收到token之后, 保存在localstorage中
function login() {
//发送ajax请求, 获得token
$.ajax({
type:"post",
url: "/user/login",
data:{
"username": $("#username").val(),
"password": $("#password").val()
},
success:function(result){
if(result.code==200 && result.data != ""){
//存储token
localStorage.setItem("user_token", result.data);
location.href = "blog_list.html";
}else{ // 自行补充完整
alert("用户名或密码错误")
}
}
});
}
local storage相关操作
存储数据
localStorage.setItem("user_token","value");
读取数据
localStorage.getItem("user_token");
删除数据
localStorage.removeItem("user_token");
运行服务器,访问登录页面,输入正确的账号和密码:
输入错误的账号和密码,查看返回的结果:
本文的内容到这里就结束了,谢谢观看!!!