• 【Spring Cloud】新闻头条微服务项目:使用JWT+MD5+Salt进行登录验证


     8420b26844034fab91b6df661ae68671.png

    个人简介: 

    > 📦个人主页:赵四司机
    > 🏆学习方向:JAVA后端开发 
    > 📣种一棵树最好的时间是十年前,其次是现在!
    > ⏰往期文章:SpringBoot项目整合微信支付
    > 🧡喜欢的话麻烦点点关注喔,你们的支持是我的最大动力。

    前言:

    最近在做一个基于SpringCloud+Springboot+Docker的新闻头条微服务项目,现在项目开发进入了尾声,我打算通过写文章的形式进行梳理一遍,并且会将梳理过程中发现的Bug进行修复,有需要改进的地方我也会继续做出改进。这一系列的文章我将会放入微服务项目专栏中,这个项目适合刚接触微服务的人作为练手项目,假如你对这个项目感兴趣你可以订阅我的专栏进行查看,需要资料可以私信我,当然要是能给我点个小小的关注就更好了,你们的支持是我最大的动力。

    一:需求分析

            现在无论什么应用都少不了登录验证环节,而登录环节又少不了安全与校验,一是要防止用户信息被盗取,而是要防止用户利用漏洞进行暴力登录。所以针对这两个方面我选用的方案是采用MD5加盐进行加密并采用JWT进行验证的方案。当然用户还可以选择以游客身份进行访问,但是只有在登录的情况下才能对文章进行点赞、关注及收藏等动作。

    二:表结构分析

    1.数据库结构

    由于App端关于用户相关信息比较多,所以单独创建了一个数据库(headlines_user)进行管理,主要包括如下几张表:

    表名称说明
    ap_userAPP用户信息表
    ap_user_fanAPP用户粉丝信息表
    ap_user_followAPP用户关注信息表
    ap_user_realnameAPP实名认证信息表

    关于用户登录用到的是ap_user表 ,表的结构如下:

    2.实体类

    1. package com.my.model.user.pojos;
    2. import com.baomidou.mybatisplus.annotation.IdType;
    3. import com.baomidou.mybatisplus.annotation.TableField;
    4. import com.baomidou.mybatisplus.annotation.TableId;
    5. import com.baomidou.mybatisplus.annotation.TableName;
    6. import lombok.Data;
    7. import java.io.Serializable;
    8. import java.util.Date;
    9. /**
    10. *

    11. * APP用户信息表
    12. *

    13. *
    14. * @author itheima
    15. */
    16. @Data
    17. @TableName("ap_user")
    18. public class ApUser implements Serializable {
    19. private static final long serialVersionUID = 1L;
    20. /**
    21. * 主键
    22. */
    23. @TableId(value = "id", type = IdType.AUTO)
    24. private Integer id;
    25. /**
    26. * 密码、通信等加密盐
    27. */
    28. @TableField("salt")
    29. private String salt;
    30. /**
    31. * 用户名
    32. */
    33. @TableField("name")
    34. private String name;
    35. /**
    36. * 密码,md5加密
    37. */
    38. @TableField("password")
    39. private String password;
    40. /**
    41. * 手机号
    42. */
    43. @TableField("phone")
    44. private String phone;
    45. /**
    46. * 头像
    47. */
    48. @TableField("image")
    49. private String image;
    50. /**
    51. * 0 男
    52. * 1 女
    53. * 2 未知
    54. */
    55. @TableField("sex")
    56. private Boolean sex;
    57. /**
    58. * 0 未
    59. * 1 是
    60. */
    61. @TableField("is_certification")
    62. private Boolean certification;
    63. /**
    64. * 是否身份认证
    65. */
    66. @TableField("is_identity_authentication")
    67. private Boolean identityAuthentication;
    68. /**
    69. * 0正常
    70. * 1锁定
    71. */
    72. @TableField("status")
    73. private Boolean status;
    74. /**
    75. * 0 普通用户
    76. * 1 自媒体人
    77. * 2 大V
    78. */
    79. @TableField("flag")
    80. private Short flag;
    81. /**
    82. * 注册时间
    83. */
    84. @TableField("created_time")
    85. private Date createdTime;
    86. }

    三:思路分析

            首先用户的任何请求都会经过网关,这时候网关就会进行拦截认证,假如用户现在的请求时登录,那这时候可以直接放行让用户去登录,否则的话就会检验用户请求头中是否包含有效的token信息,假如包含则直接放行,否则进行拦截并让用户重新登录。

            而用户登录时候首先会根据用户账号到数据库中查询该用户信息,然后将用户输入的密码与数据库中获得的Salt进行合并加密并与数据库中的密码(已加盐加密)进行比对,如果比对失败则提示相关信息,比对成功则判断用户状态,只有未被锁定方可继续操作。用户账号状态正常的话才能根据用户id生成token并保存然后放行。

    四:代码实现

    1.网关配置

    (1)引入依赖

    在tbug-headlines-gateway模块引入以下依赖

    1. <dependencies>
    2. <dependency>
    3. <groupId>org.springframework.cloudgroupId>
    4. <artifactId>spring-cloud-starter-gatewayartifactId>
    5. dependency>
    6. <dependency>
    7. <groupId>com.alibaba.cloudgroupId>
    8. <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
    9. dependency>
    10. <dependency>
    11. <groupId>com.alibaba.cloudgroupId>
    12. <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
    13. dependency>
    14. <dependency>
    15. <groupId>io.jsonwebtokengroupId>
    16. <artifactId>jjwtartifactId>
    17. dependency>
    18. dependencies>

    (2)搭建工程

    在tbug-headlines-gateway模块下创建工程tbug-headlines-app-gateway,整体架构如下:

    (3)Nacos配置

    在nacos的配置中心创建dataid为headlines-app-gateway的yml配置

    1. spring:
    2. cloud:
    3. gateway:
    4. globalcors:
    5. add-to-simple-url-handler-mapping: true
    6. corsConfigurations:
    7. '[/**]':
    8. allowedHeaders: "*"
    9. allowedOrigins: "*"
    10. allowedMethods:
    11. - GET
    12. - POST
    13. - DELETE
    14. - PUT
    15. - OPTION
    16. routes:
    17. # 用户微服务
    18. - id: user
    19. uri: lb://headlines-user
    20. predicates:
    21. - Path=/user/**
    22. filters:
    23. - StripPrefix= 1

    (3)配置文件

    application.yml

    1. server:
    2. port: 51601
    3. spring:
    4. application:
    5. name: headlines-app-gateway
    6. cloud:
    7. nacos:
    8. discovery:
    9. server-addr: 49.234.52.192:8848
    10. config:
    11. server-addr: 49.234.52.192:8848
    12. file-extension: yml

    该配置主要是配置你前面安装的nacos注册中心。

    (5)相关代码

    认证过滤器:

    1. package com.my.app.gateway.filter;
    2. import com.my.app.gateway.util.AppJwtUtil;
    3. import io.jsonwebtoken.Claims;
    4. import lombok.extern.slf4j.Slf4j;
    5. import org.apache.commons.lang.StringUtils;
    6. import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    7. import org.springframework.cloud.gateway.filter.GlobalFilter;
    8. import org.springframework.core.Ordered;
    9. import org.springframework.http.HttpStatus;
    10. import org.springframework.http.server.reactive.ServerHttpRequest;
    11. import org.springframework.http.server.reactive.ServerHttpResponse;
    12. import org.springframework.stereotype.Component;
    13. import org.springframework.web.server.ServerWebExchange;
    14. import reactor.core.publisher.Mono;
    15. @Component
    16. @Slf4j
    17. public class AuthorizeFilter implements Ordered, GlobalFilter {
    18. @Override
    19. public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    20. //1.获取request和response对象
    21. ServerHttpRequest request = exchange.getRequest();
    22. ServerHttpResponse response = exchange.getResponse();
    23. //2.判断是否是登录
    24. if(request.getURI().getPath().contains("/login")){
    25. //放行
    26. return chain.filter(exchange);
    27. }
    28. //3.获取token
    29. String token = request.getHeaders().getFirst("token");
    30. //4.判断token是否存在
    31. if(StringUtils.isBlank(token)){
    32. response.setStatusCode(HttpStatus.UNAUTHORIZED);
    33. return response.setComplete();
    34. }
    35. //5.判断token是否有效
    36. try {
    37. Claims claimsBody = AppJwtUtil.getClaimsBody(token);
    38. //是否是过期
    39. int result = AppJwtUtil.verifyToken(claimsBody);
    40. if(result == 1 || result == 2){
    41. response.setStatusCode(HttpStatus.UNAUTHORIZED);
    42. return response.setComplete();
    43. }
    44. //获取token解析后的用户信息
    45. Object userId = claimsBody.get("id");
    46. //在header中添加新的信息
    47. ServerHttpRequest serverHttpRequest = request.mutate().headers(httpHeaders -> {
    48. httpHeaders.add("userId",userId + "");
    49. }
    50. ).build();
    51. //重置header
    52. exchange.mutate().request(serverHttpRequest).build();
    53. }catch (Exception e){
    54. e.printStackTrace();
    55. response.setStatusCode(HttpStatus.UNAUTHORIZED);
    56. return response.setComplete();
    57. }
    58. //6.放行
    59. return chain.filter(exchange);
    60. }
    61. /**
    62. * 优先级设置 值越小 优先级越高
    63. * @return
    64. */
    65. @Override
    66. public int getOrder() {
    67. return 0;
    68. }
    69. }

     JWT工具类:

    1. package com.my.app.gateway.util;
    2. import io.jsonwebtoken.*;
    3. import javax.crypto.SecretKey;
    4. import javax.crypto.spec.SecretKeySpec;
    5. import java.util.*;
    6. public class AppJwtUtil {
    7. // TOKEN的有效期一天(S)
    8. private static final int TOKEN_TIME_OUT = 3_600;
    9. // 加密KEY
    10. private static final String TOKEN_ENCRY_KEY = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY";
    11. // 最小刷新间隔(S)
    12. private static final int REFRESH_TIME = 300;
    13. // 生产ID
    14. public static String getToken(Long id){
    15. Map claimMaps = new HashMap<>();
    16. claimMaps.put("id",id);
    17. long currentTime = System.currentTimeMillis();
    18. return Jwts.builder()
    19. .setId(UUID.randomUUID().toString())
    20. .setIssuedAt(new Date(currentTime)) //签发时间
    21. .setSubject("system") //说明
    22. .setIssuer("heima") //签发者信息
    23. .setAudience("app") //接收用户
    24. .compressWith(CompressionCodecs.GZIP) //数据压缩方式
    25. .signWith(SignatureAlgorithm.HS512, generalKey()) //加密方式
    26. .setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000)) //过期时间戳
    27. .addClaims(claimMaps) //cla信息
    28. .compact();
    29. }
    30. /**
    31. * 获取token中的claims信息
    32. *
    33. * @param token
    34. * @return
    35. */
    36. private static Jws getJws(String token) {
    37. return Jwts.parser()
    38. .setSigningKey(generalKey())
    39. .parseClaimsJws(token);
    40. }
    41. /**
    42. * 获取payload body信息
    43. *
    44. * @param token
    45. * @return
    46. */
    47. public static Claims getClaimsBody(String token) {
    48. try {
    49. return getJws(token).getBody();
    50. }catch (ExpiredJwtException e){
    51. return null;
    52. }
    53. }
    54. /**
    55. * 获取hearder body信息
    56. *
    57. * @param token
    58. * @return
    59. */
    60. public static JwsHeader getHeaderBody(String token) {
    61. return getJws(token).getHeader();
    62. }
    63. /**
    64. * 是否过期
    65. *
    66. * @param claims
    67. * @return -1:有效,0:有效,1:过期,2:过期
    68. */
    69. public static int verifyToken(Claims claims) {
    70. if(claims==null){
    71. return 1;
    72. }
    73. try {
    74. claims.getExpiration()
    75. .before(new Date());
    76. // 需要自动刷新TOKEN
    77. if((claims.getExpiration().getTime()-System.currentTimeMillis())>REFRESH_TIME*1000){
    78. return -1;
    79. }else {
    80. return 0;
    81. }
    82. } catch (ExpiredJwtException ex) {
    83. return 1;
    84. }catch (Exception e){
    85. return 2;
    86. }
    87. }
    88. /**
    89. * 由字符串生成加密key
    90. *
    91. * @return
    92. */
    93. public static SecretKey generalKey() {
    94. byte[] encodedKey = Base64.getEncoder().encode(TOKEN_ENCRY_KEY.getBytes());
    95. SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
    96. return key;
    97. }
    98. public static void main(String[] args) {
    99. /* Map map = new HashMap();
    100. map.put("id","11");*/
    101. System.out.println(AppJwtUtil.getToken(1102L));
    102. Jws jws = AppJwtUtil.getJws("eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAADWLQQqEMAwA_5KzhURNt_qb1KZYQSi0wi6Lf9942NsMw3zh6AVW2DYmDGl2WabkZgreCaM6VXzhFBfJMcMARTqsxIG9Z888QLui3e3Tup5Pb81013KKmVzJTGo11nf9n8v4nMUaEY73DzTabjmDAAAA.4SuqQ42IGqCgBai6qd4RaVpVxTlZIWC826QA9kLvt9d-yVUw82gU47HDaSfOzgAcloZedYNNpUcd18Ne8vvjQA");
    103. Claims claims = jws.getBody();
    104. System.out.println(claims.get("id"));
    105. }
    106. }

     2.微服务搭建

    (1)搭建工程

    在tbug-headlines-service下创建工程tbug-headlines-user

    (2)配置信息

    ①application.yml

    1. server:
    2. port: 51801
    3. spring:
    4. application:
    5. name: headlines-user
    6. cloud:
    7. nacos:
    8. discovery:
    9. server-addr: 49.234.52.192:8848
    10. config:
    11. server-addr: 49.234.52.192:8848
    12. file-extension: yml

    ②Nacos配置

    在Nacos中添加id为headlines-user的配置项,配置信息如下

    1. spring:
    2. redis:
    3. host: 49.234.52.192
    4. password: 440983
    5. port: 6379
    6. datasource:
    7. driver-class-name: com.mysql.jdbc.Driver
    8. url: jdbc:mysql://localhost:3306/headlines_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
    9. username: root
    10. password: 440983
    11. # 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
    12. mybatis-plus:
    13. mapper-locations: classpath*:mapper/*.xml
    14. # 设置别名包扫描路径,通过该属性可以给包中的类注册别名
    15. type-aliases-package: com.my.model.user.pojos

    (3)功能代码

    Controller层:

    1. package com.my.user.controller.v1;
    2. import com.my.model.common.dtos.ResponseResult;
    3. import com.my.model.user.dtos.LoginDto;
    4. import com.my.user.service.ApUserService;
    5. import io.swagger.annotations.Api;
    6. import io.swagger.annotations.ApiOperation;
    7. import org.springframework.beans.factory.annotation.Autowired;
    8. import org.springframework.web.bind.annotation.PostMapping;
    9. import org.springframework.web.bind.annotation.RequestBody;
    10. import org.springframework.web.bind.annotation.RequestMapping;
    11. import org.springframework.web.bind.annotation.RestController;
    12. @RestController
    13. @RequestMapping("/api/v1/login")
    14. @Api(value = "app端用户登录",tags = "app端用户登录")
    15. public class ApUserLoginController {
    16. @Autowired
    17. private ApUserService apUserService;
    18. @PostMapping("/login_auth")
    19. @ApiOperation("用户登录")
    20. public ResponseResult login(@RequestBody LoginDto dto){
    21. return apUserService.login(dto);
    22. }
    23. }

    mapper层:

    1. package com.my.user.mapper;
    2. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    3. import com.my.model.user.pojos.ApUser;
    4. import org.apache.ibatis.annotations.Mapper;
    5. @Mapper
    6. public interface ApUserMapper extends BaseMapper {
    7. }

    service层:

    1. package com.my.user.service;
    2. import com.baomidou.mybatisplus.extension.service.IService;
    3. import com.my.model.admin.dtos.UserAuditDto;
    4. import com.my.model.common.dtos.ResponseResult;
    5. import com.my.model.user.dtos.LoginDto;
    6. import com.my.model.user.pojos.ApUser;
    7. public interface ApUserService extends IService {
    8. /**
    9. * app端登录功能
    10. * @param dto
    11. * @return
    12. */
    13. ResponseResult login(LoginDto dto);
    14. }
    1. package com.my.user.service.impl;
    2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    3. import com.baomidou.mybatisplus.core.metadata.IPage;
    4. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    5. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    6. import com.my.model.admin.dtos.UserAuditDto;
    7. import com.my.model.common.dtos.ResponseResult;
    8. import com.my.model.common.enums.AppHttpCodeEnum;
    9. import com.my.model.user.dtos.LoginDto;
    10. import com.my.model.user.pojos.ApUser;
    11. import com.my.user.mapper.ApUserMapper;
    12. import com.my.user.service.ApUserService;
    13. import com.my.utils.common.AppJwtUtil;
    14. import lombok.extern.slf4j.Slf4j;
    15. import org.springframework.stereotype.Service;
    16. import org.springframework.transaction.annotation.Transactional;
    17. import org.springframework.util.DigestUtils;
    18. import java.nio.charset.StandardCharsets;
    19. import java.util.HashMap;
    20. import java.util.Map;
    21. @Service
    22. @Transactional
    23. @Slf4j
    24. public class ApUserServiceImpl extends ServiceImpl implements ApUserService {
    25. /**
    26. * app端登录功能
    27. * @param dto
    28. * @return
    29. */
    30. @Override
    31. public ResponseResult login(LoginDto dto) {
    32. //1.正常登录 用户名和密码
    33. if(!dto.getPhone().isEmpty() && !dto.getPassword().isEmpty()){
    34. //1.1根据用户名获取用户信息
    35. LambdaQueryWrapper lqw = new LambdaQueryWrapper<>();
    36. lqw.eq(ApUser::getPhone,dto.getPhone());
    37. ApUser user = this.getOne(lqw);
    38. //1.2没有该用户信息
    39. if(user == null) {
    40. return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"用户信息不存在");
    41. }
    42. //1.3比对用户密码
    43. String password = user.getPassword();
    44. String salt = user.getSalt();
    45. String loginPassword = DigestUtils.md5DigestAsHex((dto.getPassword() + salt).getBytes(StandardCharsets.UTF_8));
    46. if(!loginPassword.equals(password)) {
    47. //密码不正确
    48. return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR,"密码错误");
    49. }
    50. //1.4查看用户状态
    51. if(user.getStatus()) {
    52. //用户被锁定
    53. return ResponseResult.errorResult(AppHttpCodeEnum.NO_OPERATOR_AUTH,"用户已被锁定");
    54. }
    55. //1.5设置token
    56. String token = AppJwtUtil.getToken(user.getId().longValue());
    57. Map map = new HashMap<>();
    58. map.put("token",token);
    59. map.put("user",user);
    60. return ResponseResult.okResult(map);
    61. }else {
    62. //2.游客登录
    63. Map map = new HashMap<>();
    64. map.put("token",AppJwtUtil.getToken(0L));
    65. return ResponseResult.okResult(map);
    66. }
    67. }
    68. }

     下篇预告:App端文章的加载

  • 相关阅读:
    springMvc48-返回json数据
    指针和数组笔试题解析
    Golang开源流媒体服务器(RTMP/RTSP/HLS/FLV等协议)
    如何在 Vue.js 中引入原子设计?
    NX二次开发-一个简单的连接曲线例子剖析学会如何使用NXOPEN做二次开发
    基于Springboot+vue的汽车租赁系统 elementui
    嵌入式学习之Linux驱动(第九期_设备模型_教程更新了)_基于RK3568
    推荐一款基于 .NET Core开源的小程序商城系统
    【深度学习】浅显易懂的残差网络(Residual Network)
    C语言学生信息管理系统
  • 原文地址:https://blog.csdn.net/weixin_45750572/article/details/126136810