广告位联系
返回顶部
分享到

springboot结合JWT实现单点登录的代码

java 来源:互联网 作者:佚名 发布时间:2025-02-04 09:13:03 人浏览
摘要

JWT实现单点登录 登录流程: 校验用户名密码-生成随机JWT Token-返回给前端。之后前端发请求携带该Token就能验证是哪个用户了。 校验流程: 从前端请求的header获取JWT Token-根据工具包校验JWT

JWT实现单点登录

  • 登录流程:
    校验用户名密码->生成随机JWT Token->返回给前端。之后前端发请求携带该Token就能验证是哪个用户了。
  • 校验流程:
    从前端请求的header获取JWT Token->根据工具包校验JWT Token->校验成功或失败

JWT 简介

结构
Header 头部信息,主要声明了JWT的签名算法等信息
Payload 载荷信息,主要承载了各种声明并传递明文数据
Signature 签名,拥有该部分的JWT被称为JWS,也就是签了名的JWT,用于校验数据
整体结构是:

header.payload.signature

参考文档:https://doc.hutool.cn/pages/jwt/

存在问题及解决方案

  • token被解密:如工具包被获取。可通过增加“盐值”来解决。

  • token被拿到第三方使用:如被包装到第三方使用(ChatGPT工具),可以通过限流来解决。

登录流程

后端程序实现

封装hutool工具类:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

public class JwtUtil {

    private static final Logger LOG = LoggerFactory.getLogger(JwtUtil.class);

 

    /**

     * 盐值很重要,不能泄漏,且每个项目都应该不一样,可以放到配置文件中

     */

    private static final String key = "xxx";

 

    public static String createToken(Long id, String mobile) {

        LOG.info("开始生成JWT token,id:{},mobile:{}", id, mobile);

        GlobalBouncyCastleProvider.setUseBouncyCastle(false);

        DateTime now = DateTime.now();

        DateTime expTime = now.offsetNew(DateField.HOUR, 24);

//        DateTime expTime = now.offsetNew(DateField.SECOND, 10);

 

        Map<String, Object> payload = new HashMap<>();

        // 签发时间

        payload.put(JWTPayload.ISSUED_AT, now);

        // 过期时间

        payload.put(JWTPayload.EXPIRES_AT, expTime);

        // 生效时间

        payload.put(JWTPayload.NOT_BEFORE, now);

        // 内容

        payload.put("id", id);

        payload.put("mobile", mobile);

        String token = JWTUtil.createToken(payload, key.getBytes());

        LOG.info("生成JWT token:{}", token);

        return token;

    }

 

    public static boolean validate(String token) {

        LOG.info("开始JWT token校验,token:{}", token);

        GlobalBouncyCastleProvider.setUseBouncyCastle(false);

        JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes());

        // validate包含了verify

        boolean validate = jwt.validate(0);

        LOG.info("JWT token校验结果:{}", validate);

        return validate;

    }

 

    public static JSONObject getJSONObject(String token) {

        GlobalBouncyCastleProvider.setUseBouncyCastle(false);

        JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes());

        JSONObject payloads = jwt.getPayloads();

        payloads.remove(JWTPayload.ISSUED_AT);

        payloads.remove(JWTPayload.EXPIRES_AT);

        payloads.remove(JWTPayload.NOT_BEFORE);

        LOG.info("根据token获取原始内容:{}", payloads);

        return payloads;

    }

 

    public static void main(String[] args) {

        createToken(1L, "123");

 

        String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE3MzY0ODczMDQsIm1vYmlsZSI6IjEyMyIsImlkIjoxLCJleHAiOjE3MzY1NzM3MDQsImlhdCI6MTczNjQ4NzMwNH0.Bui7guCvPEF557eqxRLwmt5tO-W-3oVLnn37H4qOVfA";

        validate(token);

 

        getJSONObject(token);

    }

}

后端定义登录业务:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

public MemberLoginResp login(MemberLoginReq memberLoginReq){

    String mobile = memberLoginReq.getMobile();

    String code = memberLoginReq.getCode();

    Member memberDB = selectByMobile(mobile);

 

    if (ObjectUtil.isEmpty(memberDB)){

        throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_NOT_EXIST);

    }

 

    if(!code.equals("8888")){

        throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_CODE_ERROR);

    }

 

    MemberLoginResp memberLoginResp = new MemberLoginResp();

    memberLoginResp.setId(memberDB.getId());

    memberLoginResp.setMobile(mobile);

 

    String token = JwtUtil.createToken(memberDB.getId(), memberDB.getMobile());

    memberLoginResp.setToken(token);

 

    return memberLoginResp;

}

通过调用封装的JwtUtil生成token并返回前端

在这里插入图片描述

成功返回Token结果

前端保存Token

Vuex全局保存Token到store中

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

import { createStore } from 'vuex'

 

const MEMBER = "MEMBER";

 

export default createStore({

  state: {

    member: {}

  },

  getters: {

  },

  mutations: {

    setMember (state, _member) {

      state.member = _member;

    }

  },

  actions: {

  },

  modules: {

  }

})

1

2

3

4

5

6

7

8

9

10

11

12

13

const login = () => {

  axios.post("/member/member/login", loginForm).then((response) => {

    let data = response.data;

    if (data.success) {

      notification.success({ description: '登录成功!' });

      // 登录成功,跳到控台主页

      router.push("/welcome");

      store.commit("setMember", data.content);

    } else {

      notification.error({ description: data.message });

    }

  })

};

store存放信息的缺点及解决

store存放用户信息后,如果刷新页面,那么信息也会消失!store可以理解为缓存,一旦重新加载,则缓存全都没了。

解决方法:

  • step1. 新增session-storage.js,封装会话缓存sessionStorage

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

// 所有的session key都在这里统一定义,可以避免多个功能使用同一个key

SESSION_ORDER = "SESSION_ORDER";

SESSION_TICKET_PARAMS = "SESSION_TICKET_PARAMS";

 

SessionStorage = {

    get: function (key) {

        var v = sessionStorage.getItem(key);

        if (v && typeof(v) !== "undefined" && v !== "undefined") {

            return JSON.parse(v);

        }

    },

    set: function (key, data) {

        sessionStorage.setItem(key, JSON.stringify(data));

    },

    remove: function (key) {

        sessionStorage.removeItem(key);

    },

    clearAll: function () {

        sessionStorage.clear();

    }

};

  • step2. 在index.html中引入该js

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

<!DOCTYPE html>

<html lang="">

  <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">

    <link rel="icon" href="<%= BASE_URL %>favicon.ico" rel="external nofollow" >

        <!-- 引入js -->

      <script src="<%= BASE_URL %>js/session-storage.js"></script>

    <title><%= htmlWebpackPlugin.options.title %></title>

  </head>

  <body>

    <noscript>

      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>

    </noscript>

    <div id="app"></div>

    <!-- built files will be auto injected -->

  </body>

</html>

  • step3. 修改store的index.js

1

2

3

4

5

6

7

8

9

10

11

12

13

14

const MEMBER = "MEMBER";

 

export default createStore({

  state: {

    member: window.SessionStorage.get(MEMBER) || {} # 读取

  },

  getters: {

  },

  mutations: {

    setMember (state, _member) {

      state.member = _member;

      window.SessionStorage.set(MEMBER, _member); # 设置

    }

  },

不再是把member定义为{},而是首先在缓存中获取,如果没有则设置为{}。同时避免空指针
同时在用户登录后设置MEMBER缓存

校验流程:为gateway增加登录校验拦截器

  • 添加依赖

1

2

3

4

5

<dependency>

    <groupId>cn.hutool</groupId>

    <artifactId>hutool-all</artifactId>

    <version>5.8.10</version>

</dependency>

  • 拦截器类

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

@Component

public class LoginMemberFilter implements Ordered, GlobalFilter {

 

    private static final Logger LOG = LoggerFactory.getLogger(LoginMemberFilter.class);

 

    @Override

    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        String path = exchange.getRequest().getURI().getPath();

 

        // 排除不需要拦截的请求

        if (path.contains("/admin")

                || path.contains("/redis")

                || path.contains("/test")

                || path.contains("/member/member/login")

                || path.contains("/member/member/send-code")) {

            LOG.info("不需要登录验证:{}", path);

            return chain.filter(exchange);

        } else {

            LOG.info("需要登录验证:{}", path);

        }

        // 获取header的token参数

        String token = exchange.getRequest().getHeaders().getFirst("token");

        LOG.info("会员登录验证开始,token:{}", token);

        if (token == null || token.isEmpty()) {

            LOG.info( "token为空,请求被拦截" );

            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);

            return exchange.getResponse().setComplete();

        }

 

        // 校验token是否有效,包括token是否被改过,是否过期

        boolean validate = JwtUtil.validate(token);

        if (validate) {

            LOG.info("token有效,放行该请求");

            return chain.filter(exchange);

        } else {

            LOG.warn( "token无效,请求被拦截" );

            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);

            return exchange.getResponse().setComplete();

        }

 

    }

 

    /**

     * 优先级设置  值越小  优先级越高

     *

     * @return

     */

    @Override

    public int getOrder() {

        return 0;

    }

}

  • 测试结果:
  • 直接调用不需要验证登录的接口

1

2

3

4

5

6

7

8

9

@RestController

public class TestController {

 

    @GetMapping("/test")

    public String test(){

        return "test";

    }

 

}

在这里插入图片描述

调用需要登录的接口方法(未登录)

在这里插入图片描述

同时服务器端没有打印,表示请求已被拦截

调用login登陆后再次执行上述请求
login打印日志:

在这里插入图片描述

调用请求打印日志:

在这里插入图片描述

可见成功校验token,并读取登录用户信息,通过校验

另一种单点登录方法:Token+Redis实现单点登录

  • 登录流程:
    校验用户名密码->生成随机Token->将Token存放到Redis,并返回给前端。
    之后前端发请求携带该Token就能验证是哪个用户了。
  • 校验流程:
    从前端请求的header获取Token->根据Token到Redis获取用户数据->若有数据则登录校验通过,否则失败

版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 :
相关文章
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计