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