PS:带删除线的方法
可以使用但是不建议使用(方法提供方说的)
加上@Deprecated注解。就代表这个方法可以使用,但是不建议使用。也就会带删除线了
前言
对于用户登录。
我们之前的做法都是
1.用户登录,后端验证用户名和密码正确,则存储Session中。把SessionId存储在Cookie中
2.用户再次访问的时候,后端从Cookie中获取SessionId。根据SessionId获取Session。
存在问题:
**1.Session丢失:**Session存储在服务器内存中,如果服务器重启。那么Session就丢失了
如果用户刚登录成功。服务器进行重启Session丢失。客户端就需要重新登陆了。
**2.多机部署的情况 :**如果是单机,这台机器只要挂掉(①机器出现问题/②修改代码后服务器重启)。整个服务就挂掉。因此公司通常多机部署。
假如现在有一个客户端,和三个服务器。
①用户登录,用户请求到了服务器1。服务器1存储Session。
②用户访问。请求到了服务器2。服务器2根据用户的SessionId查找Session。但找不到。就会告诉用户未登录。这就出现了bug。
解决办法:
1.数据共享,把Session放在同一个地方。比如redis。
2.把数据放在客户端上。(类似身份证,由公安机关发放。用于每个人的身份校验。(我们把这个"身份证"就称作token令牌))
服务器具备生成令牌和验证令牌的能力使用令牌技术后
1.用户登录, 用户发起登录请求, 经过负载均衡, 把请求转给了第一台服务器, 第一台服务器进行账号密码验证, 验证成功后, 生成一个令牌, 并返回给客户端.
2.客户端收到令牌之后, 把令牌存储起来. 可以存储在Cookie中, 也可以存储在其他的存储空间(比如localStorage)
**3.查询操作,**用户登录成功之后, 携带令牌继续执行查询操作, 比如查询博客列表. 此时请求转发到了第二台机器, 第⼆台机器会先进行权限验证操作. 服务器验证令牌是否有效, 如果有效, 就说明用户已经执行了登录操作, 如果令牌是无效的, 就说明用户之前未执行登录操作.
我们将token。也称作令牌。令牌的优缺点
优点:
解决了集群环境下的认证问题。
减轻服务器的存储压力(无需在服务器存储)
缺点:
需要自己实现,包括令牌的生成、令牌的传递、令牌的校验。
一、JWT令牌(一种流行的公共令牌技术)
1.1JWT令牌简介
token令牌其实就是一个字符串。用于校验用户身份。
对上⾯部分的信息, 使用Base64Url 进行编码, 合并在一起就是jwt令牌Base64是编码方式,而不是加密方式 。
简介:JWT由三部分组成、每部分中间使用(.)分隔。
**①Header(头部):**包括令牌类型(JWT)、以及使用的哈希算法(如HMAC、SHA256、RSA)
**②Payload(负载):**负载部分是存放有效信息的地方。里面是一些自定义内容比如。{"userId":"123","userName":"zhangsan"}。也可以存在jwt提供的现场字段, 比如exp(过期时间戳)等.此部分不建议存放敏感信息, 因为此部分可以解码还原原始内容.
**③Signature(签名):**此部分用于防止jwt内容被篡改, 确保安全性。
注:防止被篡改, 而不是防止被解析.
WT之所以安全, 就是因为最后的签名. jwt当中任何一个字符被篡改, 整个令牌都会校验失败.
好比我们的身份证, 之所以能标识一个⼈的⾝份, 是因为他不能被篡改, 二不是因为内容加密.(任何人都可以看到身份证的信息, jwt 也是)
1.2JWT令牌的使用
1.2.1引入依赖
<!-- 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>
1.2.2使用Jar包中提供的API来完成JWT令牌的生成和校验
1.在Test中创建JWTUtilsTest类
如果需要从Spring容器中获取一些信息。则加上SpringBootTest注解。
而我们这里不用。
定义常量
java
//过期时间:设置为一小时后过期
private final static long EXPIRATION_DATE = 60 * 1000;
private final static String secretString = "sqmbcvcBPjfvJ4ilRLqbGmHeUaCEwdpv10jSRbCNtH4=";
private final static Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));//生成key
生成token令牌
java
@Test
public void gentToken(){
Map<String,Object> claim = new HashMap<>();
claim.put("id",5);
claim.put("name","王五");
String token = Jwts.builder()
.setClaims(claim) //设置头部和荷负载
.setExpiration(new Date(System.currentTimeMillis()+EXPIRATION_DATE))
.signWith(key) //设置签名
.compact();
System.out.println(token);
}
随机生成key(标签)
java
/**
* 随机生成Key.(标签)
*/
@Test
public void genKey(){
SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String encode = Encoders.BASE64.encode(secretKey.getEncoded());
System.out.println(encode);
}
解析Token
java/** * 解析Token */ @Test public void parseToken(){ String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi546L5LqUIiwiaWQiOjUsImV4cCI6MTczMTQ3MDYxMX0.s-6fv04cAt_8IY-BDScfzzqq-XtuEZ4THuqj_ekw824"; JwtParser build = Jwts.parserBuilder().setSigningKey(key).build(); Claims body = build.parseClaimsJws(token).getBody(); System.out.println(body); }
二、登录接口的实现(使用JWT令牌技术)
2.1在utils包创建JWTUtils类
java
@Slf4j
public class JWTUtils {
//过期时间:设置为一小时后过期
private final static long EXPIRATION_DATE = 60 * 60 * 1000;
private final static String secretString = "sqmbcvcBPjfvJ4ilRLqbGmHeUaCEwdpv10jSRbCNtH4=";
private final static Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));//生成key
/**
* 生成token令牌
*/
public static String gentToken(Map<String,Object> claim){
return Jwts.builder()
.setClaims(claim) //设置头部和荷负载
.setExpiration(new Date(System.currentTimeMillis()+EXPIRATION_DATE))
.signWith(key) //设置签名
.compact();
}
/**
* 随机生成Key.(标签)
* 由于我们已经生成了因此不需要这个代码了
*/
// public void genKey(){
// SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// String encode = Encoders.BASE64.encode(secretKey.getEncoded());
// System.out.println(encode);
// }
/**
* 解析Token
*/
public static Claims parseToken(String token){
Claims body = null;
JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();
try {
body = build.parseClaimsJws(token).getBody();
} catch (ExpiredJwtException e) {
log.info("token过期,校验失败,token:{}",token);
} catch (Exception e) {
log.info("token校验失败,token:{}",token);
}
return body;
}
public static boolean checkToken(String token){
Claims body = parseToken(token);
if(body == null){
return false;
}
return true;
}
2.2实现后端接口
java
@Autowired
private UserService userService;
@RequestMapping("/login")
public Result login(String userName, String password){
//1.参数校验
//2.对密码进行校验
//3.如果校验成功,生成token
if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
return Result.fail("用户名或密码不能为空!");
}
UserInfo userInfo = userService.queryUserByName(userName);
if(userInfo == null || userInfo.getId() <= 0){
return Result.fail("用户不存在");
}
if(!password.equals(userInfo.getPassword())){
return Result.fail("密码错误!");
}
//密码正确
Map<String,Object> claim = new HashMap<>();
claim.put("id",userInfo.getId());
claim.put("name",userInfo.getUserName());
return Result.success(JWTUtils.gentToken(claim));
}
2.3实现前端接口
客户端可以把Token放在哪里呢?
1.Cookie(推荐,但实现较复杂)
2.本地存储(推荐,实现简单)(下面代码使用这个)
3.url中(一般不这样用)
使用
javascriptlocalStorage.setItem("user_token",result.data)
存储token。登录后按f12。点击应用程序,找到本地存储。就能看到我们存储的token了
java
function login() {
$.ajax({
type: "post",
url: "user/login",
data: {
"userName": $("#username").val(),
"password": $("#password").val()
},
success:function(result){
if(result.code == 200 && result.code != null){
//存储Token
localStorage.setItem("user_token",result.data)
location.href = "blog_list.html"
}else if(result.errMsg == "用户名或密码不能为空!"){
alert("用户名或密码不能为空!");
}else if(result.errMsg == "用户不存在"){
alert("用户不存在");
}else if(result.errMsg == "密码错误!"){
alert("密码错误!");
}
//不为200如何处理.....
}
});
}
成功登录!
三、强制登录(拦截器)
1.客户端访问时,携带token(token通常放在Header中)
2.服务器获取token,验证token,如果token校验成功,放行。否则跳转到登录页面。
客户端返回token
在定义拦截器之前。我们需要从客户端获得token。若token存在且校验正确。那么放行。不然进行拦截
javascript
$(document).ajaxSend(function(e,xhr,opt){
var user_token = localStorage.getItem("user_token")
xhr.setRequestHeader("user_token_header",user_token)
});
//放在common.js中,这时候所有引入common.js的页面都会执行这个代码。
//放在common.js中,这时候所有引入common.js的页面都会执行这个代码。
每当发起ajax请求。就会执行这个方法。ajaxSend
这样我们向后端发送token。将这个变量名命名为user_token_header。
登录状态失效(提示后跳转到登录状态)
放在common.js中,这时候所有引入common.js的页面都会执行这个代码。
每当发起ajax请求。如果请求发生错误。就会执行这个方法。ajaxError
javascript
$(document).ajaxError(function(event, jqxhr, settings, thrownError) {
// 检查是否是未授权错误
if (jqxhr.status === 401) {
alert("登录已失效,请重新登录!");
window.location.href = "/blog_login.html";
} else {
// 处理其他错误
console.error("AJAX 请求出错,状态码:", jqxhr.status);
alert("请求出错,请稍后再试!");
}
});
3.1自定义拦截器
1.创建interceptor包。创建LoginInterceptor类,加上@Component注解
2.实现HandlerInterceptor接口,
3.重写preHandle方法{
//1.从handler中获取请求 //2.校验token //3.成功放行
}
javascript
/**
* 用户登录拦截器
*/
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1.从handler中获取请求
//2.校验token
//3.成功放行
String userToken = request.getHeader("user_token_header");
log.info("获得token,token:{}",userToken);
boolean result = JWTUtils.checkToken(userToken);
if(result){
return true;
}
response.setStatus(401);
return false;
}
}
3.2注册拦截器配置
1.创建config包。创建WebConfig 类implements WebMvcConfigurer。
2.注册拦截器
3.放入拦截内容以及不拦截的内容
java
/**
* 注册拦截器并配置拦截路径
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/**/*.html",
"/blog-editormd/**",
"/css/**",
"/js/**",
"/pic/**",
"/user/login");
}
}
1.注意对应关系,这两个名称可以一样。
我们完全可以写成一样的。
2.common.js的引用。必须放在jQuery.min.js的下面
ajaxSend是基于JQuery实现的。因此common.js的引用。必须放在
jQuery.min.js的下面。如果调整顺序。会导致运行不成功。
javascript<script src="js/jquery.min.js"></script> <script src="js/common.js"></script>
四、获得当前用户信息接口
根据token,获得用户信息
后端代码
controller
java@RequestMapping("getUserInfo") public UserInfo getUserInfo(HttpServletRequest request){ //1.获取token,从token中获取Id //2.根据Id,获得用户信息 String user_token = request.getHeader(Constant.USER_TOKEN_HEADER); Integer userId = JWTUtils.getUserIdFromToken(user_token); if(userId == null || userId<=0){ return null; } return userService.queryUserById(userId); }
Service
javapublic UserInfo queryUserById(Integer userId) { return userMapper.selectById(userId); }
Mapper
java@Select("select *from user where id = #{userId} and delete_flag = 0") UserInfo selectById(Integer userId);
测试的时候。注意加上Header信息。也就是token。
我们发现password返回不太合适。因此可以进行处理。
java@RequestMapping("getUserInfo") public UserInfo getUserInfo(HttpServletRequest request){ //1.获取token,从token中获取Id //2.根据Id,获得用户信息 String user_token = request.getHeader(Constant.USER_TOKEN_HEADER); Integer userId = JWTUtils.getUserIdFromToken(user_token); if(userId == null || userId<=0){ return null; } UserInfo userInfo = userService.queryUserById(userId); userInfo.setPassword(""); return userInfo; }
企业开发中并不会出现这种情况。
企业中。接口返回的实体类是单独定义的。并不是我们项目中使用的那个,
通过解耦的思想。我们返回的接口数据。并不是Userinfozhong
比如接口中返回用户信息。会重新定义一个比如UserInfoApi。
这个实体类。和UserInfo是对应的。UserInfoApi 是接口需要什么。设置什么
而UserInfo是与数据库对应的。
前端代码
html
<div class="container">
<div class="left">
<div class="card">
<img src="pic/doge.jpg" alt="">
<h3></h3>
<a href="#">GitHub 地址</a>
javascript
getUserInfo();
function getUserInfo(){
$.ajax({
type: "get",
url: "/user/getUserInfo",
success:function(result){
if(result.code == 200 && result.data!=null){
$(".left .card h3").text(result.data.userName);
$(".left .card a").attr("href",result.data.githubUrl);
}
}
});
}
javascript".left .card a"
.card前面的空格一定要加上
成功显示
五、 获取作者信息接口
根据博客Id。获取作者Id
根据作者Id。获取作者信息。
后端代码
Controller
java@RequestMapping("/getAuthorInfo") public UserInfo getAuthorInfo(Integer blogId){ //1.根据博客Id,获取作者Id //2.根据作者Id,获取作者信息 if(blogId != null && blogId< 1){ return null; } UserInfo authorInfoByBlogId = userService.getAuthorInfoByBlogId(blogId); authorInfoByBlogId.setPassword(""); return authorInfoByBlogId; }
Service
javapublic UserInfo getAuthorInfoByBlogId(Integer blogId) { //1.根据博客Id,获取作者Id //2.根据作者Id,获取作者信息 BlogInfo blogInfo = blogMapper.selectById(blogId); if(blogInfo ==null || blogInfo.getUserId()<1){ return null; } return userMapper.selectById(blogInfo.getUserId()); }
Mapper
java@Select("select *from user where id = #{userId} and delete_flag = 0") UserInfo selectById(Integer userId);
使用Postman测试一下
前端代码
html
<div class="container">
<div class="left">
<div class="card">
<img src="pic/doge.jpg" alt="">
<h3></h3>
<a href="#">GitHub 地址</a>
javascript
//显示博客作者信息
/* var userUrl = "/user/getAuthorInfo" + location.search;
getUserInfo(userUrl); */
getUserInfo();
function getUserInfo(){
$.ajax({
type: "get",
url: "/user/getAuthorInfo"+location.search,
success:function(result){
if(result.code == 200 && result.data!=null){
$(".left .card h3").text(result.data.userName);
$(".left .card a").attr("href",result.data.githubUrl);
}
}
});
}
最终成功显示
代码整合
我们发现四、五这两个接口的前端代码几乎一样。
因此我们可以把这个代码放在common.js文件中。
javascriptfunction getUserInfo(url){ $.ajax({ type: "get", url: url, success:function(result){ if(result.code == 200 && result.data!=null){ $(".left .card h3").text(result.data.userName); $(".left .card a").attr("href",result.data.githubUrl); } } }); }
接着在前端响应的接口处调用这个方法。传入参数就行了。