个人简介:
> ?个人主页:赵四司机
> ?学习方向:JAVA后端开发
> ?种一棵树最好的时间是十年前,其次是现在!
> ⏰往期文章:SpringBoot项目整合微信支付
> ?喜欢的话麻烦点点关注喔,你们的支持是我的最大动力。
前言:
最近在做一个基于SpringCloud+Springboot+Docker的新闻头条微服务项目,现在项目开发进入了尾声,我打算通过写文章的形式进行梳理一遍,并且会将梳理过程中发现的Bug进行修复,有需要改进的地方我也会继续做出改进。这一系列的文章我将会放入微服务项目专栏中,这个项目适合刚接触微服务的人作为练手项目,假如你对这个项目感兴趣你可以订阅我的专栏进行查看,需要资料可以私信我,当然要是能给我点个小小的关注就更好了,你们的支持是我最大的动力。
一:需求分析
现在无论什么应用都少不了登录验证环节,而登录环节又少不了安全与校验,一是要防止用户信息被盗取,而是要防止用户利用漏洞进行暴力登录。所以针对这两个方面我选用的方案是采用MD5加盐进行加密并采用JWT进行验证的方案。当然用户还可以选择以游客身份进行访问,但是只有在登录的情况下才能对文章进行点赞、关注及收藏等动作。
二:表结构分析
1.数据库结构
由于App端关于用户相关信息比较多,所以单独创建了一个数据库(headlines_user)进行管理,主要包括如下几张表:
表名称 | 说明 |
---|---|
ap_user | APP用户信息表 |
ap_user_fan | APP用户粉丝信息表 |
ap_user_follow | APP用户关注信息表 |
ap_user_realname | APP实名认证信息表 |
关于用户登录用到的是ap_user表 ,表的结构如下:
2.实体类
- package com.my.model.user.pojos;
-
- import com.baomidou.mybatisplus.annotation.IdType;
- import com.baomidou.mybatisplus.annotation.TableField;
- import com.baomidou.mybatisplus.annotation.TableId;
- import com.baomidou.mybatisplus.annotation.TableName;
- import lombok.Data;
-
- import java.io.Serializable;
- import java.util.Date;
-
- /**
- *
- * APP用户信息表
- *
- *
- * @author itheima
- */
- @Data
- @TableName("ap_user")
- public class ApUser implements Serializable {
-
- private static final long serialVersionUID = 1L;
-
- /**
- * 主键
- */
- @TableId(value = "id", type = IdType.AUTO)
- private Integer id;
-
- /**
- * 密码、通信等加密盐
- */
- @TableField("salt")
- private String salt;
-
- /**
- * 用户名
- */
- @TableField("name")
- private String name;
-
- /**
- * 密码,md5加密
- */
- @TableField("password")
- private String password;
-
- /**
- * 手机号
- */
- @TableField("phone")
- private String phone;
-
- /**
- * 头像
- */
- @TableField("image")
- private String image;
-
- /**
- * 0 男
- * 1 女
- * 2 未知
- */
- @TableField("sex")
- private Boolean sex;
-
- /**
- * 0 未
- * 1 是
- */
- @TableField("is_certification")
- private Boolean certification;
-
- /**
- * 是否身份认证
- */
- @TableField("is_identity_authentication")
- private Boolean identityAuthentication;
-
- /**
- * 0正常
- * 1锁定
- */
- @TableField("status")
- private Boolean status;
-
- /**
- * 0 普通用户
- * 1 自媒体人
- * 2 大V
- */
- @TableField("flag")
- private Short flag;
-
- /**
- * 注册时间
- */
- @TableField("created_time")
- private Date createdTime;
-
- }
三:思路分析
首先用户的任何请求都会经过网关,这时候网关就会进行拦截认证,假如用户现在的请求时登录,那这时候可以直接放行让用户去登录,否则的话就会检验用户请求头中是否包含有效的token信息,假如包含则直接放行,否则进行拦截并让用户重新登录。
而用户登录时候首先会根据用户账号到数据库中查询该用户信息,然后将用户输入的密码与数据库中获得的Salt进行合并加密并与数据库中的密码(已加盐加密)进行比对,如果比对失败则提示相关信息,比对成功则判断用户状态,只有未被锁定方可继续操作。用户账号状态正常的话才能根据用户id生成token并保存然后放行。
四:代码实现
1.网关配置
(1)引入依赖
在tbug-headlines-gateway模块引入以下依赖
- dependencies>
- dependency>
- groupId>org.springframework.cloudgroupId>
- artifactId>spring-cloud-starter-gatewayartifactId>
- dependency>
- dependency>
- groupId>com.alibaba.cloudgroupId>
- artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
- dependency>
- dependency>
- groupId>com.alibaba.cloudgroupId>
- artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
- dependency>
- dependency>
- groupId>io.jsonwebtokengroupId>
- artifactId>jjwtartifactId>
- dependency>
- dependencies>
(2)搭建工程
在tbug-headlines-gateway模块下创建工程tbug-headlines-app-gateway,整体架构如下:
(3)Nacos配置
在nacos的配置中心创建dataid为headlines-app-gateway的yml配置
- spring:
- cloud:
- gateway:
- globalcors:
- add-to-simple-url-handler-mapping: true
- corsConfigurations:
- '[/**]':
- allowedHeaders: "*"
- allowedOrigins: "*"
- allowedMethods:
- - GET
- - POST
- - DELETE
- - PUT
- - OPTION
- routes:
- # 用户微服务
- - id: user
- uri: lb://headlines-user
- predicates:
- - Path=/user/**
- filters:
- - StripPrefix= 1
(3)配置文件
application.yml
- server:
- port: 51601
- spring:
- application:
- name: headlines-app-gateway
- cloud:
- nacos:
- discovery:
- server-addr: 49.234.52.192:8848
- config:
- server-addr: 49.234.52.192:8848
- file-extension: yml
该配置主要是配置你前面安装的nacos注册中心。
(5)相关代码
认证过滤器:
- package com.my.app.gateway.filter;
-
-
- import com.my.app.gateway.util.AppJwtUtil;
- import io.jsonwebtoken.Claims;
- import lombok.extern.slf4j.Slf4j;
- import org.apache.commons.lang.StringUtils;
- import org.springframework.cloud.gateway.filter.GatewayFilterChain;
- import org.springframework.cloud.gateway.filter.GlobalFilter;
- import org.springframework.core.Ordered;
- import org.springframework.http.HttpStatus;
- import org.springframework.http.server.reactive.ServerHttpRequest;
- import org.springframework.http.server.reactive.ServerHttpResponse;
- import org.springframework.stereotype.Component;
- import org.springframework.web.server.ServerWebExchange;
- import reactor.core.publisher.Mono;
-
- @Component
- @Slf4j
- public class AuthorizeFilter implements Ordered, GlobalFilter {
- @Override
- public Mono
filter(ServerWebExchange exchange, GatewayFilterChain chain) { - //1.获取request和response对象
- ServerHttpRequest request = exchange.getRequest();
- ServerHttpResponse response = exchange.getResponse();
-
- //2.判断是否是登录
- if(request.getURI().getPath().contains("/login")){
- //放行
- return chain.filter(exchange);
- }
-
-
- //3.获取token
- String token = request.getHeaders().getFirst("token");
-
- //4.判断token是否存在
- if(StringUtils.isBlank(token)){
- response.setStatusCode(HttpStatus.UNAUTHORIZED);
- return response.setComplete();
- }
-
- //5.判断token是否有效
- try {
- Claims claimsBody = AppJwtUtil.getClaimsBody(token);
- //是否是过期
- int result = AppJwtUtil.verifyToken(claimsBody);
- if(result == 1 || result == 2){
- response.setStatusCode(HttpStatus.UNAUTHORIZED);
- return response.setComplete();
- }
- //获取token解析后的用户信息
- Object userId = claimsBody.get("id");
- //在header中添加新的信息
- ServerHttpRequest serverHttpRequest = request.mutate().headers(httpHeaders -> {
- httpHeaders.add("userId",userId + "");
- }
- ).build();
-
- //重置header
- exchange.mutate().request(serverHttpRequest).build();
- }catch (Exception e){
- e.printStackTrace();
- response.setStatusCode(HttpStatus.UNAUTHORIZED);
- return response.setComplete();
- }
-
- //6.放行
- return chain.filter(exchange);
- }
-
- /**
- * 优先级设置 值越小 优先级越高
- * @return
- */
- @Override
- public int getOrder() {
- return 0;
- }
- }
JWT工具类:
- package com.my.app.gateway.util;
-
- import io.jsonwebtoken.*;
-
- import javax.crypto.SecretKey;
- import javax.crypto.spec.SecretKeySpec;
- import java.util.*;
-
- public class AppJwtUtil {
-
- // TOKEN的有效期一天(S)
- private static final int TOKEN_TIME_OUT = 3_600;
- // 加密KEY
- private static final String TOKEN_ENCRY_KEY = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY";
- // 最小刷新间隔(S)
- private static final int REFRESH_TIME = 300;
-
- // 生产ID
- public static String getToken(Long id){
- Map
claimMaps = new HashMap(); - claimMaps.put("id",id);
- long currentTime = System.currentTimeMillis();
- return Jwts.builder()
- .setId(UUID.randomUUID().toString())
- .setIssuedAt(new Date(currentTime)) //签发时间
- .setSubject("system") //说明
- .setIssuer("heima") //签发者信息
- .setAudience("app") //接收用户
- .compressWith(CompressionCodecs.GZIP) //数据压缩方式
- .signWith(SignatureAlgorithm.HS512, generalKey()) //加密方式
- .setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000)) //过期时间戳
- .addClaims(claimMaps) //cla信息
- .compact();
- }
-
- /**
- * 获取token中的claims信息
- *
- * @param token
- * @return
- */
- private static Jws
getJws(String token) { - return Jwts.parser()
- .setSigningKey(generalKey())
- .parseClaimsJws(token);
- }
-
- /**
- * 获取payload body信息
- *
- * @param token
- * @return
- */
- public static Claims getClaimsBody(String token) {
- try {
- return getJws(token).getBody();
- }catch (ExpiredJwtException e){
- return null;
- }
- }
-
- /**
- * 获取hearder body信息
- *
- * @param token
- * @return
- */
- public static JwsHeader getHeaderBody(String token) {
- return getJws(token).getHeader();
- }
-
- /**
- * 是否过期
- *
- * @param claims
- * @return -1:有效,0:有效,1:过期,2:过期
- */
- public static int verifyToken(Claims claims) {
- if(claims==null){
- return 1;
- }
- try {
- claims.getExpiration()
- .before(new Date());
- // 需要自动刷新TOKEN
- if((claims.getExpiration().getTime()-System.currentTimeMillis())>REFRESH_TIME*1000){
- return -1;
- }else {
- return 0;
- }
- } catch (ExpiredJwtException ex) {
- return 1;
- }catch (Exception e){
- return 2;
- }
- }
-
- /**
- * 由字符串生成加密key
- *
- * @return
- */
- public static SecretKey generalKey() {
- byte[] encodedKey = Base64.getEncoder().encode(TOKEN_ENCRY_KEY.getBytes());
- SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
- return key;
- }
-
- public static void main(String[] args) {
- /* Map map = new HashMap();
- map.put("id","11");*/
- System.out.println(AppJwtUtil.getToken(1102L));
- Jws
jws = AppJwtUtil.getJws("eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAADWLQQqEMAwA_5KzhURNt_qb1KZYQSi0wi6Lf9942NsMw3zh6AVW2DYmDGl2WabkZgreCaM6VXzhFBfJMcMARTqsxIG9Z888QLui3e3Tup5Pb81013KKmVzJTGo11nf9n8v4nMUaEY73DzTabjmDAAAA.4SuqQ42IGqCgBai6qd4RaVpVxTlZIWC826QA9kLvt9d-yVUw82gU47HDaSfOzgAcloZedYNNpUcd18Ne8vvjQA"); - Claims claims = jws.getBody();
- System.out.println(claims.get("id"));
-
- }
-
- }
2.微服务搭建
(1)搭建工程
在tbug-headlines-service下创建工程tbug-headlines-user
(2)配置信息
①application.yml
- server:
- port: 51801
- spring:
- application:
- name: headlines-user
- cloud:
- nacos:
- discovery:
- server-addr: 49.234.52.192:8848
- config:
- server-addr: 49.234.52.192:8848
- file-extension: yml
②Nacos配置
在Nacos中添加id为headlines-user的配置项,配置信息如下
- spring:
- redis:
- host: 49.234.52.192
- password: 440983
- port: 6379
- datasource:
- driver-class-name: com.mysql.jdbc.Driver
- url: jdbc:mysql://localhost:3306/headlines_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
- username: root
- password: 440983
- # 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
- mybatis-plus:
- mapper-locations: classpath*:mapper/*.xml
- # 设置别名包扫描路径,通过该属性可以给包中的类注册别名
- type-aliases-package: com.my.model.user.pojos
(3)功能代码
Controller层:
- package com.my.user.controller.v1;
-
- import com.my.model.common.dtos.ResponseResult;
- import com.my.model.user.dtos.LoginDto;
- import com.my.user.service.ApUserService;
- import io.swagger.annotations.Api;
- import io.swagger.annotations.ApiOperation;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.web.bind.annotation.PostMapping;
- import org.springframework.web.bind.annotation.RequestBody;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RestController;
-
-
- @RestController
- @RequestMapping("/api/v1/login")
- @Api(value = "app端用户登录",tags = "app端用户登录")
- public class ApUserLoginController {
-
- @Autowired
- private ApUserService apUserService;
-
- @PostMapping("/login_auth")
- @ApiOperation("用户登录")
- public ResponseResult login(@RequestBody LoginDto dto){
- return apUserService.login(dto);
- }
- }
mapper层:
- package com.my.user.mapper;
-
-
- import com.baomidou.mybatisplus.core.mapper.BaseMapper;
- import com.my.model.user.pojos.ApUser;
- import org.apache.ibatis.annotations.Mapper;
-
- @Mapper
- public interface ApUserMapper extends BaseMapper
{ - }
service层:
- package com.my.user.service;
-
- import com.baomidou.mybatisplus.extension.service.IService;
- import com.my.model.admin.dtos.UserAuditDto;
- import com.my.model.common.dtos.ResponseResult;
- import com.my.model.user.dtos.LoginDto;
- import com.my.model.user.pojos.ApUser;
-
- public interface ApUserService extends IService
{ - /**
- * app端登录功能
- * @param dto
- * @return
- */
- ResponseResult login(LoginDto dto);
- }
- package com.my.user.service.impl;
-
- import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
- import com.baomidou.mybatisplus.core.metadata.IPage;
- import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
- import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
- import com.my.model.admin.dtos.UserAuditDto;
- import com.my.model.common.dtos.ResponseResult;
- import com.my.model.common.enums.AppHttpCodeEnum;
- import com.my.model.user.dtos.LoginDto;
- import com.my.model.user.pojos.ApUser;
- import com.my.user.mapper.ApUserMapper;
- import com.my.user.service.ApUserService;
- import com.my.utils.common.AppJwtUtil;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.stereotype.Service;
- import org.springframework.transaction.annotation.Transactional;
- import org.springframework.util.DigestUtils;
-
- import java.nio.charset.StandardCharsets;
- import java.util.HashMap;
- import java.util.Map;
-
-
- @Service
- @Transactional
- @Slf4j
- public class ApUserServiceImpl extends ServiceImpl
implements ApUserService { - /**
- * app端登录功能
- * @param dto
- * @return
- */
- @Override
- public ResponseResult login(LoginDto dto) {
- //1.正常登录 用户名和密码
- if(!dto.getPhone().isEmpty() && !dto.getPassword().isEmpty()){
- //1.1根据用户名获取用户信息
- LambdaQueryWrapper
lqw = new LambdaQueryWrapper(); - lqw.eq(ApUser::getPhone,dto.getPhone());
- ApUser user = this.getOne(lqw);
-
- //1.2没有该用户信息
- if(user == null) {
- return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"用户信息不存在");
- }
-
- //1.3比对用户密码
- String password = user.getPassword();
- String salt = user.getSalt();
- String loginPassword = DigestUtils.md5DigestAsHex((dto.getPassword() + salt).getBytes(StandardCharsets.UTF_8));
- if(!loginPassword.equals(password)) {
- //密码不正确
- return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR,"密码错误");
- }
-
- //1.4查看用户状态
- if(user.getStatus()) {
- //用户被锁定
- return ResponseResult.errorResult(AppHttpCodeEnum.NO_OPERATOR_AUTH,"用户已被锁定");
- }
-
- //1.5设置token
- String token = AppJwtUtil.getToken(user.getId().longValue());
- Map
map = new HashMap(); - map.put("token",token);
- map.put("user",user);
- return ResponseResult.okResult(map);
- }else {
- //2.游客登录
- Map
map = new HashMap(); - map.put("token",AppJwtUtil.getToken(0L));
- return ResponseResult.okResult(map);
- }
- }
-
-
- }
下篇预告:App端文章的加载