25.<Spring博客系统②(实现JWT令牌登录接口+强制登录+获取用户信息+获取作者信息)>

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令牌简介

全称: JSON Web Token(官网)

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中(一般不这样用)
使用

javascript 复制代码
localStorage.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包。创建WebConfigimplements 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

java 复制代码
    public 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

java 复制代码
    public 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文件中。

javascript 复制代码
function 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);
            }
        }
    });
}

接着在前端响应的接口处调用这个方法。传入参数就行了。

相关推荐
王解10 分钟前
【Webpack实用指南】如何拆分CSS资源(2)
前端·css·webpack
理想不理想v30 分钟前
vue2和vue3:diff算法的区别?
前端·javascript·vue.js·python·tornado
OEC小胖胖31 分钟前
Vue 3 中的 ref 完全指南
前端·javascript·vue.js·前端框架·web
windy1a35 分钟前
【C语言】前端未来
前端
Software攻城狮39 分钟前
进阶的巨人
前端
漆黑的莫莫1 小时前
使用 Vue 和 ECharts 创建交互式图表
前端·vue.js·echarts
Magicapprentice1 小时前
fast-api后端 + fetch 前端流式文字响应
前端·后端·状态模式
清水加冰1 小时前
Jenkins + gitee 自动触发项目拉取部署(Webhook配置)
运维·gitee·jenkins
追梦不止~1 小时前
将容器测试托管到Jenkins
运维·docker·jenkins
用屁屁笑1 小时前
Spring Web MVC
前端·spring·mvc