目录

Life in Flow

知不知,尚矣;不知知,病矣。
不知不知,殆矣。

X

JWT登录解决方案

session 共享、分布式缓存

  • 单机 Tomcat 应用登录检验
    • sesssion 保存在浏览器和应用服务器会话之间
    • 用户登录成功,服务端会保存一个 session,当然客户端有一个 sessionId
    • 客户端会把 sessionId 保存在 cookie 中,每次请求都会携带这个 sessionId
  • 分布式应用中 session 共享
    • 真实的应用不可能单节点部署,所以就有个多节点登录 session 共享的问题需要解决
    • Tomcat 支持 session 共享,但是有广播风暴;用户量大的时候,占用资源就严重,不推荐
    • 使用 Redis 存储 token:
      • 服务端使用 UUID 生成随机 64 位或者 128 位 token,放入 Redis 中,然后返回给客户端并存储在 cookie 中
      • 用户每次访问都携带此 token,服务端去 Redis 中校验是否有此用户即可

image-20200417222658225

分布式应用下登录检验解决方案 JWT

  • 什么是 JWT

    • JWT 是一个开放标准,它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名
    • 简单来说: 就是通过一定规范来生成 token,然后可以通过解密算法逆向解密 token,这样就可以获取用户信息
  • 优点

    • 生产的 token 可以包含基本信息,比如 id、用户昵称、头像等信息,避免再次查库
    • 存储在客户端,不占用服务端的内存资源
  • 缺点

    • token 是经过 base64 编码,所以可以解码,因此 token 加密前的对象不应该包含敏感信息,如用户权限,密码等
    • 如果没有服务端存储,则不能做登录失效处理,除非服务端改秘钥
 1      {
 2                id:888,
 3                name:'小D',
 4                expire:10000
 5            }
 6      
 7            funtion 加密(object, appsecret){
 8                xxxx
 9                return base64( token);
10            }
11
12            function 解密(token ,appsecret){
13
14                xxxx
15                //成功返回true,失败返回false
16            }
  • JWT 格式组成 头部、负载、签名

    • header+payload+signature
      • 头部:主要是描述签名算法
      • 负载:主要描述是加密对象的信息,如用户的 id 等,也可以加些规范里面的东西,如 iss 签发者,exp 过期时间,sub 面向的用户
      • 签名:主要是把前面两部分进行加密,防止别人拿到 token 进行 base 解密后篡改 token
  • 关于 jwt 客户端存储

    • 可以存储在 cookie,localstorage 和 sessionStorage 里面

JsonWebToken 工具类封装

 1package net.xdclass.online_xdclass.utils;
 2
 3import io.jsonwebtoken.Claims;
 4import io.jsonwebtoken.Jwts;
 5import io.jsonwebtoken.SignatureAlgorithm;
 6import net.xdclass.online_xdclass.domain.User;
 7
 8import java.util.Date;
 9
10/**
11 * Jwt工具类
12 * 注意点:
13 * 1、生成的token, 是可以通过base64进行解密出明文信息
14 * 2、base64进行解密出明文信息,修改再进行编码,则会解密失败
15 * 3、无法作废已颁布的token,除非改秘钥
16 */
17public class JWTUtils {
18
19
20    /**
21     * 过期时间,一周
22     */
23    private  static final long EXPIRE = 60000 * 60 * 24 * 7;
24
25
26    /**
27     * 加密秘钥
28     */
29    private  static final String SECRET = "xdclass.net168";
30
31
32    /**
33     * 令牌前缀
34     */
35    private  static final String TOKEN_PREFIX = "xdclass";
36
37
38    /**
39     * subject
40     */
41    private  static final String SUBJECT = "xdclass";
42
43
44    /**
45     * 根据用户信息,生成令牌
46     * @param user
47     * @return
48     */
49    public static String geneJsonWebToken(User user){
50
51        String token = Jwts.builder().setSubject(SUBJECT)
52                .claim("head_img",user.getHeadImg())
53                .claim("id",user.getId())
54                .claim("name",user.getName())
55                .setIssuedAt(new Date())
56                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
57                .signWith(SignatureAlgorithm.HS256,SECRET).compact();
58
59        token = TOKEN_PREFIX + token;
60
61        return token;
62    }
63
64
65    /**
66     * 校验token的方法
67     * @param token
68     * @return
69     */
70    public static Claims checkJWT(String token){
71
72        try{
73
74            final  Claims claims = Jwts.parser().setSigningKey(SECRET)
75                    .parseClaimsJws(token.replace(TOKEN_PREFIX,"")).getBody();
76
77            return claims;
78
79        }catch (Exception e){
80            //解密失败返回Null
81            return null;
82        }
83
84    }
85
86}
87

测试

src/test/java/net/xdclass/online_xdclass/OnlineXdclassApplicationTests.java

 1package net.xdclass.online_xdclass;
 2
 3import io.jsonwebtoken.Claims;
 4import net.xdclass.online_xdclass.model.entity.User;
 5import net.xdclass.online_xdclass.utils.JWTUtils;
 6import org.junit.jupiter.api.Test;
 7import org.springframework.boot.test.context.SpringBootTest;
 8import org.springframework.util.Assert;
 9
10@SpringBootTest
11class OnlineXdclassApplicationTests {
12
13	@Test
14	public void testGeneJwt(){
15
16
17		User user = new User();
18		user.setId(66);
19		user.setName("二当家小D");
20		user.setHeadImg("png");
21
22		String token = JWTUtils.geneJsonWebToken(user);
23
24		System.out.println(token);
25
26//		try {
27//			Thread.sleep(4000L);
28//		} catch (InterruptedException e) {
29//			e.printStackTrace();
30//		}
31
32		Claims claims = JWTUtils.checkJWT(token);
33
34
35		System.out.println(claims.get("name"));
36
37	}
38
39
40
41
42}

登录模块整合 JSON Web Token

Model

在 model 下增加 request 包,并创建登录对象 LoginRequest

 1package net.xdclass.online_xdclass.model.request;
 2
 3/**
 4 * 登录 request
 5 */
 6public class LoginRequest {
 7
 8    private String phone;
 9
10    private String pwd;
11
12    public String getPhone() {
13        return phone;
14    }
15
16    public void setPhone(String phone) {
17        this.phone = phone;
18    }
19
20    public String getPwd() {
21        return pwd;
22    }
23
24    public void setPwd(String pwd) {
25        this.pwd = pwd;
26    }
27}
28

Controller

 1package net.xdclass.online_xdclass.controller;
 2
 3import net.xdclass.online_xdclass.model.request.LoginRequest;
 4import net.xdclass.online_xdclass.service.UserService;
 5import net.xdclass.online_xdclass.utils.JsonData;
 6import org.springframework.beans.factory.annotation.Autowired;
 7import org.springframework.web.bind.annotation.PostMapping;
 8import org.springframework.web.bind.annotation.RequestBody;
 9import org.springframework.web.bind.annotation.RequestMapping;
10import org.springframework.web.bind.annotation.RestController;
11
12import java.util.Map;
13
14@RestController
15@RequestMapping("api/v1/pri/user")
16public class UserController {
17
18
19
20    @Autowired
21    private UserService userService;
22
23    /**
24     * 注册接口
25     * @param userInfo
26     * @return
27     */
28    @PostMapping("register")
29    public JsonData register(@RequestBody Map<String,String> userInfo ){
30
31        int rows = userService.save(userInfo);
32
33        return rows == 1 ? JsonData.buildSuccess(): JsonData.buildError("注册失败,请重试");
34
35    }
36
37
38    /**
39     * 登录接口
40     * @param loginRequest
41     * @return
42     */
43    @PostMapping("login")
44    public JsonData login(@RequestBody LoginRequest loginRequest){
45
46        String token = userService.findByPhoneAndPwd(loginRequest.getPhone(), loginRequest.getPwd());
47
48        return token == null ?JsonData.buildError("登录失败,账号密码错误"): JsonData.buildSuccess(token);
49
50    }
51
52
53
54}
55

Service

interface

 1package net.xdclass.online_xdclass.service;
 2
 3import java.util.Map;
 4
 5public interface UserService {
 6
 7    /**
 8     * 新增用户
 9     * @param userInfo
10     * @return
11     */
12    int save(Map<String, String> userInfo);
13
14
15    String findByPhoneAndPwd(String phone, String pwd);
16}
17

impl

 1package net.xdclass.online_xdclass.service.impl;
 2
 3import net.xdclass.online_xdclass.model.entity.User;
 4import net.xdclass.online_xdclass.mapper.UserMapper;
 5import net.xdclass.online_xdclass.service.UserService;
 6import net.xdclass.online_xdclass.utils.CommonUtils;
 7import net.xdclass.online_xdclass.utils.JWTUtils;
 8import org.springframework.beans.factory.annotation.Autowired;
 9import org.springframework.stereotype.Service;
10
11import java.util.Date;
12import java.util.Map;
13import java.util.Random;
14
15@Service
16public class UserServiceImpl implements UserService {
17
18    @Autowired
19    private UserMapper userMapper;
20
21
22    @Override
23    public int save(Map<String, String> userInfo) {
24
25        User user = parseToUser(userInfo);
26        if( user != null){
27           return userMapper.save(user);
28        }else {
29            return -1;
30        }
31
32    }
33
34
35    @Override
36    public String findByPhoneAndPwd(String phone, String pwd) {
37
38        User user = userMapper.findByPhoneAndPwd(phone, CommonUtils.MD5(pwd));
39
40        if(user == null){
41            return null;
42
43        }else {
44            String token = JWTUtils.geneJsonWebToken(user);
45            return token;
46        }
47
48    }
49
50    /**
51     * 解析 user 对象
52     * @param userInfo
53     * @return
54     */
55    private User parseToUser(Map<String,String> userInfo) {
56
57        if(userInfo.containsKey("phone") && userInfo.containsKey("pwd") && userInfo.containsKey("name")){
58            User user = new User();
59            user.setName(userInfo.get("name"));
60            user.setHeadImg(getRandomImg());
61            user.setCreateTime(new Date());
62            user.setPhone(userInfo.get("phone"));
63            String pwd = userInfo.get("pwd");
64            //MD5加密
65            user.setPwd(CommonUtils.MD5(pwd));
66
67            return user;
68        }else {
69            return null;
70        }
71
72    }
73
74    /**
75     * 放在CDN上的随机头像
76     */
77    private static final String [] headImg = {
78            "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/default/head_img/12.jpeg",
79            "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/default/head_img/11.jpeg",
80            "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/default/head_img/13.jpeg",
81            "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/default/head_img/14.jpeg",
82            "https://xd-video-pc-img.oss-cn-beijing.aliyuncs.com/xdclass_pro/default/head_img/15.jpeg"
83    };
84
85    private String getRandomImg(){
86        int size =  headImg.length;
87        Random random = new Random();
88        int index = random.nextInt(size);
89        return headImg[index];
90    }
91
92}
93

Mapper

 1<?xml version="1.0" encoding="UTF-8" ?>
 2<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
 3<mapper namespace="net.xdclass.online_xdclass.mapper.UserMapper">
 4
 5
 6    <insert id="save" parameterType="User">
 7
 8        INSERT  INTO user (name, pwd, head_img, phone , create_time)
 9        values (#{name,jdbcType=VARCHAR}, #{pwd,jdbcType=VARCHAR}, #{headImg,jdbcType=VARCHAR},
10        #{phone,jdbcType=VARCHAR},#{createTime,jdbcType=TIMESTAMP})
11
12    </insert>
13
14
15
16    <!--根据手机号查询用户信息-->
17    <select id="findByPhone" resultType="User">
18
19        select  * from user where phone =#{phone}
20
21    </select>
22
23
24    <!--根据手机号和密码找用户-->
25    <select id="findByPhoneAndPwd" resultType="User">
26
27        select  * from user where phone =#{phone} and pwd = #{pwd}
28
29
30    </select>
31  
32
33</mapper>

测试

 1localhost:8081/api/v1/pri/user/login
 2
 3POST:Body->raw->JSON
 4
 5
 6{
 7    "phone":"12345678",
 8    "pwd":"111111"
 9}
10
11
12***Response***
13{
14    "code": 0,
15    "data": "xdclasseyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ4ZGNsYXNzIiwiaGVhZF9pbWciOiJodHRwczovL3hkLXZpZGVvLXBjLWltZy5vc3MtY24tYmVpamluZy5hbGl5dW5jcy5jb20veGRjbGFzc19wcm8vZGVmYXVsdC9oZWFkX2ltZy8xNS5qcGVnIiwiaWQiOjgsIm5hbWUiOiJhbGljZSIsImlhdCI6MTYxMDUwMzM5NSwiZXhwIjoxNjExMTA4MTk1fQ.Hru8k9bwXd32t2iQX-6br_30M-HwiYKVerfChv_Recw",
16    "msg": null
17}

作者:Soulboy