项目5-博客系统2(实现登录-令牌技术)

1.实现登录

分析
传统思路:
登陆⻚⾯把⽤⼾名密码提交给服务器.
服务器端验证⽤⼾名密码是否正确, 并返回校验结果给后端
如果密码正确, 则在服务器端创建 Session . 通过 Cookie 把 sessionId 返回给浏览器
问题:
集群环境下⽆法直接使⽤Session.
原因分析:
我们开发的项⽬, 在企业中很少会部署在⼀台机器上, 容易发⽣单点故障. (单点故障: ⼀旦这台服务器挂了, 整个应⽤都没法访问了).
所以通常情况下, ⼀个Web应⽤会部署在多个服务器上, 通过Nginx等进⾏负载均衡. 此时, 来⾃⼀个⽤⼾的请求就会被分发到不同的服务器上.
假如我们使⽤Session进⾏会话跟踪, 我们来思考如下场景:
1. ⽤⼾登录 ⽤⼾登录请求, 经过负载均衡, 把请求转给了第⼀台服务器, 第⼀台服务器进⾏账号密码验证, 验证成功后, 把Session存在了第⼀台服务器上
2. 查询操作⽤⼾登录成功之后, 携带Cookie(⾥⾯有SessionId)继续执⾏查询操作, ⽐如查询博客列表.
此时请求转发到了第⼆台机器, 第⼆台机器会先进⾏权限验证操作(通过SessionId验证⽤⼾是否登录), 此时第⼆台机器上没有该⽤⼾的Session, 就会出现问题, 提⽰⽤⼾登录, 这是⽤⼾不能忍受的.
这种情况我们想到了: 令牌技术

2.令牌技术

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

2.1令牌的优缺点

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

2.2 JWT令牌 

令牌本质就是⼀个字符串, 他的实现⽅式有很多, 我们采⽤⼀个JWT令牌来实现.
介绍
JWT全称: JSON Web Token
官⽹: https://jwt.io/
JSON Web Token(JWT)是⼀个开放的⾏业标准(RFC 7519), ⽤于客⼾端和服务器之间传递安全可靠的信息.
其本质是⼀个token, 是⼀种紧凑的URL安全⽅法.

2.2.1 JWT组成

JWT由三部分组成, 每部分中间使⽤点 (.) 分隔,⽐如:aaaaa.bbbbb.cccc
Header(头部)
  头部包括令牌的类型(即JWT)及使⽤的哈希算法(如HMAC SHA256或RSA)
Payload(负载)
  负载部分是存放有效信息的地⽅, ⾥⾯是⼀些⾃定义内容.
  ⽐如: {"userId":"123","userName":"zhangsan"} ,
  也可以存在jwt提供的现场字段, ⽐如exp(过期时间戳)等.
  此部分不建议存放敏感信息, 因为此部分可以解码还原原始内容.
Signature(签名) 此部分⽤于防⽌jwt内容被篡改, 确保安全性.(base64)
  防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算    法计算而来。
  防⽌被篡改, ⽽不是防⽌被解析.
JWT之所以安全, 就是因为最后的签名. jwt当中任何⼀个字符被篡改, 整个令牌都会校验失败.
就好⽐我们的⾝份证, 之所以能标识⼀个⼈的⾝份, 是因为他不能被篡改, ⽽不是因为内容加密.(任 何⼈都可以看到⾝份证的信息, jwt 也是)

 

对上⾯部分的信息, 使⽤Base64Url 进⾏编码, 合并在⼀起就是jwt令牌
Base64是编码⽅式,⽽不是加密⽅式 

2.3 JWT令牌生成和校验

1. 引⼊JWT令牌的依赖

<!-- 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. 使⽤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);
    }

令牌解析后, 我们可以看到⾥⾯存储的信息,如果在解析的过程当中没有报错,就说明解析成功了

令牌解析时, 也会进⾏时间有效性的校验, 如果令牌过期了, 解析也会失败.
修改令牌中的任何⼀个字符, 都会校验失败, 所以令牌⽆法篡改
学习令牌的使⽤之后, 接下来我们通过令牌来完成⽤⼾的登录
1. 登陆⻚⾯把⽤⼾名密码提交给服务器.
2. 服务器端验证⽤⼾名密码是否正确, 如果正确, 服务器⽣成令牌, 下发给客⼾端.
3. 客⼾端把令牌存储起来(⽐如Cookie, local storage等), 后续请求时, 把token发给服务器
4. 服务器对令牌进⾏校验, 如果令牌正确, 进⾏下⼀步操作

3.用户的登录

3.1 约定前后端交互接口

[ 请求 ]
/user/login
username= test &password=123
[ 响应 ]
{
        "code" : 200,
        "msg" : "" ,
        "data" :
        "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJ6aGFuZ3NhbiIsImlhdCI6MTY5ODM5N
        zg2MCwiZXhwIjoxNjk4Mzk5NjYwfQ.oxup5LfpuPixJrE3uLB9u3q0rHxxTC8_AhX1QlYV-        -        E"
}
// 验证成功 , 返回 token, 验证失败返回 ""

 3.2 实现服务器代码

3.2.1 创建JWT⼯具类

java Claims类

java Claims类_mob649e81680b4f的技术博客_51CTO博客

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博客

3.4 local storage相关操作

存储数据
localStorage.setItem("user_token","value");
读取数据
localStorage.getItem("user_token");
删除数据
localStorage.removeItem("user_token");

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);
});
ajaxSend() ⽅法在 AJAX 请求开始时执⾏函数
event - 包含 event 对象
xhr - 包含 XMLHttpRequest 对象
options - 包含 AJAX 请求中使⽤的选项
2) 修改 blog_datail.html和blog_list.html
访问⻚⾯时, 添加失败处理代码
使⽤ location.assign 进⾏⻚⾯跳转.

4.测试

发现一直被拦截,时由于方法调用错误导致的。

  • 26
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值