Spring项目:文字花园(四)

一.实现登录

传统思路:
登陆⻚⾯把⽤⼾名密码提交给服务器.
服务器端验证⽤⼾名密码是否正确, 并返回校验结果给后端
如果密码正确, 则在服务器端创建 Session . 通过 Cookie 把 sessionId 返回给浏览器.
问题:
集群环境下⽆法直接使⽤Session.

原因分析: 我们开发的项⽬, 在企业中很少会部署在⼀台机器上, 容易发⽣单点故障. (单点故障: ⼀旦这台服务器挂了, 整个应⽤都没法访问了). 所以通常情况下, ⼀个Web应⽤会部署在多个服务器上, 通过Nginx等进⾏负载均衡. 此时, 来⾃⼀个⽤⼾的请求就会被分发到不同的服务器上.

假如我们使⽤Session进⾏会话跟踪, 我们来思考如下场景:
1. ⽤⼾登录 ⽤⼾登录请求, 经过负载均衡, 把请求转给了第⼀台服务器, 第⼀台服务器进⾏账号密码
验证, 验证成功后, 把Session存在了第⼀台服务器上
2. 查询操作 ⽤⼾登录成功之后, 携带Cookie(⾥⾯有SessionId)继续执⾏查询操作, ⽐如查询博客列
表. 此时请求转发到了第⼆台机器, 第⼆台机器会先进⾏权限验证操作(通过SessionId验证⽤⼾是否
登录), 此时第⼆台机器上没有该⽤⼾的Session, 就会出现问题, 提⽰⽤⼾登录, 这是⽤⼾不能忍受的.

接下来我们介绍第三种⽅案: 令牌技术

 1.1JWT令牌

令牌其实就是⼀个⽤⼾⾝份的标识, 名称起的很⾼⼤上, 其实本质就是⼀个字符串.
JWT全称: JSON Web Token
官⽹: https://jwt.io/
JSON Web Token(JWT)是⼀个开放的⾏业标准(RFC 7519), ⽤于客⼾端和服务器之间传递安全可靠的信息. 其本质是⼀个token, 是⼀种紧凑的URL安全⽅法.

⽐如我们出⾏在外, 会带着⾃⼰的⾝份证, 需要验证⾝份时, 就掏出⾝份证
⾝份证不能伪造, 可以辨别真假
服务器具备⽣成令牌和验证令牌的能⼒
我们使⽤令牌技术, 继续思考上述场景:
1. ⽤⼾登录 ⽤⼾登录请求, 经过负载均衡, 把请求转给了第⼀台服务器, 第⼀台服务器进⾏账号密码
验证, 验证成功后, ⽣成⼀个令牌, 并返回给客⼾端.
2. 客⼾端收到令牌之后, 把令牌存储起来. 可以存储在Cookie中, 也可以存储在其他的存储空间(⽐如localStorage)
3. 查询操作 ⽤⼾登录成功之后, 携带令牌继续执⾏查询操作, ⽐如查询博客列表. 此时请求转发到了
第⼆台机器, 第⼆台机器会先进⾏权限验证操作. 服务器验证令牌是否有效, 如果有效, 就说明⽤⼾已经执⾏了登录操作, 如果令牌是⽆效的, 就说明⽤⼾之前未执⾏登录操作.

令牌的优缺点
优点:
解决了集群环境下的认证问题
减轻服务器的存储压⼒(⽆需在服务器端存储)
缺点:
需要⾃⼰实现(包括令牌的⽣成, 令牌的传递, 令牌的校验)
当前企业开发中, 解决会话跟踪使⽤最多的⽅案就是令牌技术.

JWT组成
JWT由三部分组成, 每部分中间使⽤点 (.) 分隔,⽐如:aaaaa.bbbbb.cccc
Header(头部) 头部包括令牌的类型(即JWT)及使⽤的哈希算法(如HMAC SHA256或RSA)
Payload(负载) 负载部分是存放有效信息的地⽅, ⾥⾯是⼀些⾃定义内容. ⽐如:
{"userId":"123","userName":"zhangsan"} , 也可以存在jwt提供的现场字段, ⽐如
exp(过期时间戳)等. 此部分不建议存放敏感信息, 因为此部分可以解码还原原始内容.
Signature(签名) 此部分⽤于防⽌jwt内容被篡改, 确保安全性.
防⽌被篡改, ⽽不是防⽌被解析.
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)

使⽤Jar包中提供的API来完成JWT令牌的⽣成和校验

代码:

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+'">查看全文&gt;&gt;</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的地方获取令牌时,加上了'' '' ,导致找不到,这根本是不对的

要去掉 '' ''

返回信息成功了 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值