1.实现登录
这种情况我们想到了: 令牌技术
2.令牌技术
令牌其实就是⼀个⽤⼾⾝份的标识, 名称起的很⾼⼤上, 其实本质就是⼀个字符串.
2.1令牌的优缺点
优点:
• 解决了集群环境下的认证问题
• 减轻服务器的存储压⼒(⽆需在服务器端存储)
缺点:
需要⾃⼰实现(包括令牌的⽣成, 令牌的传递, 令牌的校验)
2.2 JWT令牌
令牌本质就是⼀个字符串, 他的实现⽅式有很多, 我们采⽤⼀个JWT令牌来实现.
2.2.1 JWT组成
2.3 JWT令牌生成和校验
1. 引⼊JWT令牌的依赖
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);
}
令牌解析后, 我们可以看到⾥⾯存储的信息,如果在解析的过程当中没有报错,就说明解析成功了
3.用户的登录
3.1 约定前后端交互接口
3.2 实现服务器代码
3.2.1 创建JWT⼯具类
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()
, 作用是刷新, 没有参数
location.href=url 和 location.assign(url) 和 location.replace(url) 和 location.reload()_location href mdn-CSDN博客
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);
});
2) 修改 blog_datail.html和blog_list.html
• 访问⻚⾯时, 添加失败处理代码
• 使⽤ location.assign 进⾏⻚⾯跳转.
4.测试
发现一直被拦截,时由于方法调用错误导致的。