一.实现登录
集群环境下⽆法直接使⽤Session.原因分析: 我们开发的项⽬, 在企业中很少会部署在⼀台机器上, 容易发⽣单点故障. (单点故障: ⼀旦这台服务器挂了, 整个应⽤都没法访问了). 所以通常情况下, ⼀个Web应⽤会部署在多个服务器上, 通过Nginx等进⾏负载均衡. 此时, 来⾃⼀个⽤⼾的请求就会被分发到不同的服务器上.
接下来我们介绍第三种⽅案: 令牌技术
1.1JWT令牌
{"userId":"123","userName":"zhangsan"} , 也可以存在jwt提供的现场字段, ⽐如exp(过期时间戳)等. 此部分不建议存放敏感信息, 因为此部分可以解码还原原始内容.
防⽌被篡改, ⽽不是防⽌被解析.JWT之所以安全, 就是因为最后的签名. jwt当中任何⼀个字符被篡改, 整个令牌都会校验失败.就好⽐我们的⾝份证, 之所以能标识⼀个⼈的⾝份, 是因为他不能被篡改, ⽽不是因为内容加密.(任何⼈都可以看到⾝份证的信息, jwt 也是)
JWT令牌⽣成和校验。
1.2项目导入令牌
1.在pom文件,引入依赖
<!-- 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>
2.生成令牌(utils/JwsUtils)
代码:
package com.example.demo.utils;
import io.jsonwebtoken.Claims;
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 {
private static final long JWT_EXPIRATION = 60 * 60 * 1000;
private static final String secretStr = "Le++o8NQWVXWo3+SJtAtnjBW9iA0OvPL0c0mMrol2fU=";
private static final Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretStr));
/**
* 生成token
*/
public static String genJwtToken(Map<String, Object> claim) {
String token = Jwts.builder().setClaims(claim)
.setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION))
.signWith(key)
.compact();
return token;
}
/**
* 校验token
* Claims 为 null 表示校验失败
*/
public static Claims parseToken(String token) {
JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();
Claims claims = null;
try {
claims = build.parseClaimsJws(token).getBody();
} catch (Exception e) {
log.error("解析token失败, token:{}", token);
return null;
}
return claims;
}
}
3.编写后端接口(User Controller)
package com.example.demo.controller;
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 jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.authenticator.Constants;
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 java.util.HashMap;
import java.util.Map;
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public Result login(String username, String password){
log.info("UserController#login接收参数: username:{}, password:{}", username, password);
//1. 参数校验合法性
//2. 校验密码是否正确
//3. 密码正确, 返回token
//4. 密码错误, 返回错误信息
if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)){
return Result.fail("账号或密码不能为空");
}
//从数据库中查找用户
UserInfo userInfo = userService.selectByName(username);
//用户不存在
if (userInfo==null){
log.error("用户不存在");
return Result.fail("用户不存在");
}
//密码错误
if (!password.equals(userInfo.getPassword())){
log.error("密码错误");
return Result.fail("密码错误");
}
//密码正确, 返回token
Map<String,Object> claim = new HashMap<>();
claim.put("id", userInfo.getId());
claim.put("userName", userInfo.getUserName());
String token = JwtUtils.genJwtToken(claim);
log.info("UserController#login 返回结果token:{}",token);
return Result.success(token);
}
/**
* 获取当前登录用户的信息
* @return
*/
// @RequestMapping("/getAuthorInfo")
// public UserInfo getAuthorInfo(Integer blogId){
//
// }
}
service层:
package com.example.demo.service;
import com.example.demo.mapper.UserInfoMapper;
import com.example.demo.model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
public UserInfo selectByName(String userName) {
return userInfoMapper.selectByName(userName);
}
public UserInfo selectById(Integer userId) {
return userInfoMapper.selectById(userId);
}
}
mapper层:
package com.example.demo.mapper;
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserInfoMapper {
/**
* 根据用户名, 查询用户信息
* @return
*/
@Select("select * from user where delete_flag =0 and user_name = #{userName}")
UserInfo selectByName(String userName);
/**
* 根据用户ID, 查询用户信息
* @return
*/
@Select("select * from user where delete_flag =0 and id = #{id}")
UserInfo selectById(Integer id);
}
4.编写前端代码进行交互:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>博客登陆页</title>
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/login.css">
</head>
<body>
<div class="nav">
<img src="pic/rose.jpg" alt="">
<span class="blog-title">文字花园</span>
<div class="space"></div>
<a class="nav-span" href="blog_list.html">主页</a>
<a class="nav-span" href="blog_edit.html">写博客</a>
</div>
<div class="container-login">
<div class="login-dialog">
<h3>登陆</h3>
<div class="row">
<span>用户名</span>
<input type="text" name="username" id="username">
</div>
<div class="row">
<span>密码</span>
<input type="password" name="password" id="password">
</div>
<div class="row">
<button id="submit" onclick="login()">提交</button>
</div>
</div>
</div>
<script src="js/jquery.min.js"></script>
<script>
function login() {
$.ajax({
type:"post",
url: "/user/login",
data:{
username:$("#username").val(),
password: $("#password").val()
},
success:function(result){
if(result.code=="SUCCESS" && result.data !=null){
//后端处理成功
localStorage.setItem("user_token", result.data);//将令牌存储到本地
//页面跳转
location.href = "blog_list.html";
}else{
alert(result.errMsg);
}
}
});
}
</script>
</body>
</html>
展示成功 .
二.实现强制登录
1.1编写拦截器
新添类:interceptor/LoginInterceptor
代码:
package com.example.demo.interceptor;
import com.example.demo.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.authenticator.Constants;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1. 获取token
//2. 校验token, 判断是否放行
String token = request.getHeader("user_token_header");
log.info("从header中获取token:{}", token);
Claims claims = JwtUtils.parseToken(token);
if (claims==null){
//校验失败
response.setStatus(401);
return false;
}
return true;
}
}
1.2应用到项目
1.新添类:config/webconfig
package com.example.demo.config;
import com.example.demo.interceptor.LoginInterceptor;
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 WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
private final List excludes = Arrays.asList(
"/**/*.html",
"/blog-editormd/**",
"/css/**",
"/js/**",
"/pic/**",
"/user/login"
);
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(excludes);
}
}
2. 编写前端代码(blog_list.html):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>博客列表页</title>
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/list.css">
</head>
<body>
<div class="nav">
<img src="pic/rose.jpg" alt="">
<span class="blog-title">文字花园</span>
<div class="space"></div>
<a class="nav-span" href="blog_list.html">主页</a>
<a class="nav-span" href="blog_edit.html">写博客</a>
<a class="nav-span" href="#" onclick="logout()">注销</a>
</div>
<div class="container">
<div class="left">
<div class="card">
<img src="pic/doge.jpg" alt="">
<h3>小李同学</h3>
<a href="#">GitHub 地址</a>
<div class="row">
<span>文章</span>
<span>分类</span>
</div>
<div class="row">
<span>2</span>
<span>1</span>
</div>
</div>
</div>
<div class="right">
sbnbjnmssssssd
</div>
</div>
<script src="js/jquery.min.js"></script>
<script src="js/common.js"></script>
<script>
$.ajax({
type: "get",
url: "/blog/getList",
success: function(result){
if(result.code=="SUCCESS"){
var blogs = result.data;
var finalHtml = "";
for(var blog of blogs){
finalHtml +='<div class="blog">';
finalHtml +='<div class="title">'+blog.title+'</div>';
finalHtml +='<div class="date">'+blog.createTime+'</div>';
finalHtml +='<div class="desc">'+blog.content+'</div>';
finalHtml +='<a class="detail" href="blog_detail.html?blogId='+blog.id+'">查看全文>></a>';
finalHtml +='</div>';
console.log(finalHtml);
}
$(".container .right").html(finalHtml);
}
}
, error:function(error){
if(error!=null && error.status==401){
location.href = "blog_login.html";
}
}
});
</script>
</body>
</html>
3.定义全局 AJAX 事件处理器,它在每个 AJAX 请求发送之前被触发。
(将user_token存放在Header中的名换为user_token_header),后期使用是user_token_header
(记住顺序):用户正确登录后,后端返回令牌,用户端将令牌存储到本地,此时common.js的作用就是从本地内存中获取到令牌,然后放到header中,进行后续的校验等操作。
4.展示
补充:
Q:如上我们进行正确登录时,上面代码令牌应该是获取成功了,但是为什么下面代码还显示获取失败呢?
A:其实这无关紧要,因为可能要经过很多接口之类啥的影响,问题不大。
1.3获取登录的读者信息
1.定义好令牌常量A
//直接把令牌定义成一个常量,这样哪个类需要就去用即可
public class Constants {
public static final String REQUEST_HEADER_TOKEN = "user_token_header";
public static final String TOKEN_ID = "id";
public static final String TOKEN_USERNAME = "userName";
}
2.在令牌类写好一个返回用户id的方法
public static Integer getIdByToken(String token){
Claims claims = parseToken(token);//解析token获取用户信息
if (claims != null){
Integer userId = (Integer) claims.get(Constants.TOKEN_ID);//获取到的id是字符类型,需要强转
if(userId > 0){
return userId;
}
}
return null;
}
3.实现后端接口
@RequestMapping("/getUserInfo")
public UserInfo getLoginUserInfo(HttpServletRequest request){//这个通过令牌获取登录用户信息,获取方式跟获取请求类似
String token = request.getHeader(Constants.REQUEST_HEADER_TOKEN);//获取令牌,这个在Contants类定义好的常量
//从token中获取登录用户信息
Integer userId = JwtUtils.getIdByToken(token);
if(userId == null){
return null;
}
UserInfo userInfo = userService.selectById(userId);
return userInfo;
}
@RequestMapping("/getAuthorInfo")
public UserInfo getAuthorInfo(Integer blogId){//为什么使用博客属性?,因为博客属性有作者id
if (blogId<=0){
return null;
}
UserInfo userInfo = userService.getAuthorInfo(blogId);
return userInfo;//返回user这个属性
}
1.4调试登录的读者信息接口
1.先登录获取令牌
2.在postman调试getUserInfo接口(记得放入令牌)
3.返回下方信息报异常
为什么呢?
在从request的地方获取令牌时,加上了'' '' ,导致找不到,这根本是不对的
要去掉 '' ''
返回信息成功了