1.实现登录
分析传统思路:• 登陆⻚⾯把⽤⼾名密码提交给服务器.• 服务器端验证⽤⼾名密码是否正确, 并返回校验结果给后端• 如果密码正确, 则在服务器端创建 Session . 通过 Cookie 把 sessionId 返回给浏览器
问题:集群环境下⽆法直接使⽤Session.
原因分析:我们开发的项⽬, 在企业中很少会部署在⼀台机器上, 容易发⽣单点故障. (单点故障: ⼀旦这台服务器挂了, 整个应⽤都没法访问了).所以通常情况下, ⼀个Web应⽤会部署在多个服务器上, 通过Nginx等进⾏负载均衡. 此时, 来⾃⼀个⽤⼾的请求就会被分发到不同的服务器上.假如我们使⽤Session进⾏会话跟踪, 我们来思考如下场景:1. ⽤⼾登录 ⽤⼾登录请求, 经过负载均衡, 把请求转给了第⼀台服务器, 第⼀台服务器进⾏账号密码验证, 验证成功后, 把Session存在了第⼀台服务器上2. 查询操作⽤⼾登录成功之后, 携带Cookie(⾥⾯有SessionId)继续执⾏查询操作, ⽐如查询博客列表.此时请求转发到了第⼆台机器, 第⼆台机器会先进⾏权限验证操作(通过SessionId验证⽤⼾是否登录), 此时第⼆台机器上没有该⽤⼾的Session, 就会出现问题, 提⽰⽤⼾登录, 这是⽤⼾不能忍受的.
这种情况我们想到了: 令牌技术
2.令牌技术
令牌其实就是⼀个⽤⼾⾝份的标识, 名称起的很⾼⼤上, 其实本质就是⼀个字符串.
⽐如我们出⾏在外, 会带着⾃⼰的⾝份证, 需要验证⾝份时, 就掏出⾝份证⾝份证不能伪造, 可以辨别真假服务器具备⽣成令牌和验证令牌的能⼒我们使⽤令牌技术, 继续思考上述场景:1. ⽤⼾登录⽤⼾登录请求, 经过负载均衡, 把请求转给了第⼀台服务器, 第⼀台服务器进⾏账号密码验证, 验证成功后, ⽣成⼀个令牌, 并返回给客⼾端.2. 客⼾端收到令牌之后, 把令牌存储起来. 可以存储在Cookie中, 也可以存储在其他的存储空间(⽐如localStorage)3. 查询操作用户登录成功之后, 携带令牌继续执⾏查询操作,比如查询博客列表.此时请求转发到了 第⼆台机器, 第⼆台机器会先进⾏权限验证操作.服务器 验证令牌是否有效 , 如果有效, 就说明⽤⼾已经执⾏了登录操作, 如果令牌是⽆效的, 就说明⽤⼾之前未执行登录操作.
2.1令牌的优缺点
优点:
•
解决了集群环境下的认证问题
•
减轻服务器的存储压⼒(⽆需在服务器端存储)
缺点:
需要⾃⼰实现(包括令牌的⽣成, 令牌的传递, 令牌的校验)
当前企业开发中, 解决会话跟踪使⽤最多的⽅案就是令牌技术.
2.2 JWT令牌
令牌本质就是⼀个字符串, 他的实现⽅式有很多, 我们采⽤⼀个JWT令牌来实现.
介绍JWT全称: JSON Web Token官⽹: https://jwt.io/JSON Web Token(JWT)是⼀个开放的⾏业标准(RFC 7519), ⽤于客⼾端和服务器之间传递安全可靠的信息.其本质是⼀个token, 是⼀种紧凑的URL安全⽅法.
2.2.1 JWT组成
JWT由三部分组成, 每部分中间使⽤点 (.) 分隔,⽐如:aaaaa.bbbbb.cccc• Header(头部)头部包括令牌的类型(即JWT)及使⽤的哈希算法(如HMAC SHA256或RSA)• Payload(负载)负载部分是存放有效信息的地⽅, ⾥⾯是⼀些⾃定义内容.⽐如: {"userId":"123","userName":"zhangsan"} ,也可以存在jwt提供的现场字段, ⽐如exp(过期时间戳)等.此部分不建议存放敏感信息, 因为此部分可以解码还原原始内容.• Signature(签名) 此部分⽤于防⽌jwt内容被篡改, 确保安全性.(base64)防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算 法计算而来。防⽌被篡改, ⽽不是防⽌被解析.
JWT之所以安全, 就是因为最后的签名. jwt当中任何⼀个字符被篡改, 整个令牌都会校验失败.就好⽐我们的⾝份证, 之所以能标识⼀个⼈的⾝份, 是因为他不能被篡改, ⽽不是因为内容加密.(任 何⼈都可以看到⾝份证的信息, jwt 也是)
对上⾯部分的信息, 使⽤Base64Url 进⾏编码, 合并在⼀起就是jwt令牌Base64是编码⽅式,⽽不是加密⽅式
2.3 JWT令牌生成和校验
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 ispreferred -->< version >0.11.5</ version >< scope >runtime</ scope ></ dependency >
2. 使⽤Jar包中提供的API来完成JWT令牌的⽣成和校验
2.1 生成令牌
@SpringBootTest
public class JwtUtilsTest {
//过期毫秒时⻓ 30分钟
public static final long Expiration=30*60*1000;
//密钥
private static final String
secretString="BhIDH5ISHd9c4cX/GMpP8ONEZ9edrGKyWmO7wpHnZFk=";
//⽣成安全密钥
private static final SecretKey KEY =
Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));
@Test
public void genJwt(){
//⾃定义信息
Map<String,Object> claim = new HashMap<>();
claim.put("id",1);
claim.put("username","zhangsan");
String jwt = Jwts.builder()
.setClaims(claim) //⾃定义内容(负载)
.setIssuedAt(new Date())// 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() +
Expiration)) //设置过期时间
.signWith(KEY) //签名算法
.compact();
System.out.println(jwt);
}
/**⽣成密钥*/
@Test
public void genKey(){
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String secretString = Encoders.BASE64.encode(key.getEncoded());
System.out.println(secretString);
}
}
输出的内容, 就是JWT令牌通过点(.)对三个部分进⾏分割, 我们把⽣成的令牌通过官⽹进⾏解析, 就可以看到我们存储的信息了
2.2 校验令牌
完成了令牌的⽣成, 我们需要根据令牌, 来校验令牌的合法性(以防客⼾端伪造)
@Test
public void parseJWT() {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJ6aGFuZ3NhbiIsImlhdCI6" +
"MTcxMjkyOTk4NywiZXhwIjoxNzEyOTMxNzg3fQ.hDyyGBxcv959gtZBX8MBy5JshP__pgtNsSZsoxO0SCo\n";
//创建解析器, 设置签名密钥
JwtParserBuilder jwtParserBuilder =
Jwts.parserBuilder().setSigningKey(KEY);
//解析token
Claims claims = jwtParserBuilder.build().parseClaimsJws(token).getBody();
System.out.println(claims);
}
令牌解析后, 我们可以看到⾥⾯存储的信息,如果在解析的过程当中没有报错,就说明解析成功了
令牌解析时, 也会进⾏时间有效性的校验, 如果令牌过期了, 解析也会失败.修改令牌中的任何⼀个字符, 都会校验失败, 所以令牌⽆法篡改
学习令牌的使⽤之后, 接下来我们通过令牌来完成⽤⼾的登录1. 登陆⻚⾯把⽤⼾名密码提交给服务器.2. 服务器端验证⽤⼾名密码是否正确, 如果正确, 服务器⽣成令牌, 下发给客⼾端.3. 客⼾端把令牌存储起来(⽐如Cookie, local storage等), 后续请求时, 把token发给服务器4. 服务器对令牌进⾏校验, 如果令牌正确, 进⾏下⼀步操作
3.用户的登录
3.1 约定前后端交互接口
[ 请求 ]/user/loginusername= test &password=123[ 响应 ]{"code" : 200,"msg" : "" ,"data" :"eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJ6aGFuZ3NhbiIsImlhdCI6MTY5ODM5Nzg2MCwiZXhwIjoxNjk4Mzk5NjYwfQ.oxup5LfpuPixJrE3uLB9u3q0rHxxTC8_AhX1QlYV- - E"}// 验证成功 , 返回 token, 验证失败返回 ""
3.2 实现服务器代码
3.2.1 创建JWT⼯具类
java Claims类
package com.example.demo.utils;
import com.example.demo.constants.Constant;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import java.util.Base64;
import java.util.Date;
import java.util.Map;
@Slf4j
public class JwtUtils {
//一、法一:自定义生成密钥的办法(自定义必须满足base64编码后字节长度>=256 bits)
//1.密钥
public static String key="HAA/HKhIBFZaj9Ipcw7CDKKf8M1BG3TxychLG+INjcs=";
//2.过期的时间(单位毫秒)->30min
public static long expiration = 30*60*1000;
//3.根据密钥生成安全密钥(需要先对字符串进行BASE64编码才可以设置密钥,自定义密钥需要有足够的长度)
private static SecretKey secretKey= Keys.hmacShaKeyFor(Decoders.BASE64.decode(key));
//二、法二:生成密钥
//SecretKey key = Jwts.SIG.HS256.key().build();
//生成令牌
//(1)设置令牌中携带的内容
public static String genJwt(Map<String, Object> claim){
//签名算法
String jwt = Jwts.builder()
.setClaims(claim) //⾃定义内容(载荷)
.setIssuedAt(new Date())// 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() +
expiration)) //设置过期时间
.signWith(secretKey) //签名算法(密钥,加密算法)
.compact();//返回为字符串类型的jwt令牌
return jwt;
}
/**
* 解析令牌
* @param token
* @return
*/
public static Claims parseToken(String token){
//创建解析器, 设置签名密钥
JwtParserBuilder jwtParserBuilder =
Jwts.parserBuilder().setSigningKey(secretKey);
Claims body = null;
try {
body=jwtParserBuilder.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){
return false;
}
return true;
}
//从token中获取用户id
public static Integer getUserIdFromToken(String token){
Claims body = parseToken(token);
if (body!=null){
return (Integer) body.get(Constant.USER_CLAIM_ID);
}
return null;
}
}
3.2.2.service包
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
public UserInfo selectByName(String userName){
return userInfoMapper.selectByName(userName);
}
}
3.2.3.controller包
package com.example.demo.controller;
import com.example.demo.constants.Constant;
import com.example.demo.model.Result;
import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
import com.example.demo.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
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public Result login(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
String userName, String password){
if(!StringUtils.hasLength(userName)||!StringUtils.hasLength(password)){
log.error("userName"+userName+",password"+password);
return Result.fail(Constant.RESULT_CODE_FAIL,"用户或密码为空");
}
//判断账号密码是否正确
UserInfo userInfo=userService.selectByName(userName);
if(userInfo==null||!userInfo.getPassword().equals(password)){
return Result.fail(Constant.RESULT_CODE_FAIL,"用户或密码输入错误");
}
//登陆成功
Map<String,Object> claims=new HashMap<>();
claims.put(Constant.USER_CLAIM_ID,userInfo.getId());
claims.put(Constant.USER_CLAIM_NAME,userInfo.getUserName());
String token= JwtUtils.genJwt(claims);
System.out.println("生成token"+token);
return Result.success(token);
}
}
3.2.4 测试后端接口
成功!!!
3.2.5 前端代码修改
<script>
function login() {
$.ajax({
type: "get",
url: "/user/login",
data: {
userName: $("#username").val(),
password: $("#password").val()
},
success: function(result){
console.log(result);
if(result.code==200&&result.data!=null){
localStorage.setItem("user_token",result.data);
location.assign("blog_list.html");
}else{
alert("账号或密码有误");
return;
}
}
});
}
</script>
验证成功!!!
3.3 location.href=url 、location.assign(url) 、location.replace(url) 、location.reload()
location.href=url
效果类似location.assign(url)
, 相当于跳转新页面, 可以后退location.replace(url)
, 是改变当前页面的地址, 不能后退location.reload()
, 作用是刷新, 没有参数
3.4 local storage相关操作
存储数据localStorage.setItem("user_token","value");
读取数据localStorage.getItem("user_token");
删除数据localStorage.removeItem("user_token");
4.实现强制登陆操作(拦截器)
当⽤⼾访问 博客列表⻚ 和 博客详情⻚ 时, 如果⽤⼾当前尚未登陆, 就⾃动跳转到登陆⻚⾯.
我们可以采⽤拦截器来完成, token通常由前端放在header中, 我们从header中获取token, 并校验token是否合法
1.注册拦截器
package com.example.demo.config;
import com.example.demo.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//从header中获取token
String jwtToken=request.getHeader("user_token");
log.info("从header中获取token:{}",jwtToken);
//验证⽤⼾token
Claims claims = JwtUtils.parseToken(jwtToken);
if (claims!=null){
log.info("令牌验证通过, 放⾏");
return true;
}
response.setStatus(401);
return true;
}
}
2.定义拦截器
package com.example.demo.config;
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.WebMvcConfigurer;
import java.util.Arrays;
import java.util.List;
@Configuration
public class AppConfig implements WebMvcConfigurer {
private final List excludes = Arrays.asList(
"/**/*.html",
"/blog-editormd/**",
"/css/**",
"/js/**",
"/pic/**",
"/login"
);
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(excludes);
}
}
3.实现客户端代码
1)
前端请求时, header中统⼀添加token, 可以写在common.js中
$(document).ajaxSend(function (e, xhr, opt) {
var user_token = localStorage.getItem("user_token");
xhr.setRequestHeader("user_token", user_token);
});
ajaxSend() ⽅法在 AJAX 请求开始时执⾏函数• event - 包含 event 对象• xhr - 包含 XMLHttpRequest 对象• options - 包含 AJAX 请求中使⽤的选项
2)
修改 blog_datail.html和blog_list.html
•
访问⻚⾯时, 添加失败处理代码
•
使⽤ location.assign 进⾏⻚⾯跳转.
4.测试
发现一直被拦截,时由于方法调用错误导致的。