文章目录
- 1 环境搭建及简介
- 2 项目介绍
- 2.1 应用
- 2.2 业务说明
- 2.3 技术栈
- 2.4 收获
- 2.5 大纲
- 3 Nacos准备
- 3.1 安装Nacos
- 4 初始工程搭建
- 4.1 环境准备
- 4.1.1 导入项目
- 4.1.2 设置本地仓库
- 4.1.3 设置项目编码格式
- 4.2 全局异常
- 4.2.1 自动装配
- 4.3 工程主体结构
- 5 登录功能开发
- 5.1 需求分析
- 5.1.1 表结构分析
- 5.1.2 实体类ApUser的导入
- 5.1.3 表结构中salt的解释
- 5.1.3.1 注册过程
- 5.1.3.2 登录过程
- 6 用户端微服务的搭建
- 6.1 service模块的依赖说明
- 6.2 创建用户微服务模块
- 6.2.1 创建引导类
- 6.2.2 创建controller.v1、service、mapper、config
- 6.2.3 创建resources的配置文件
- 6.2.3.1 bootstrap.yml
- 6.2.3.2 logback.xml
- 6.3 整体用户端框架
- 7 登录接口实现
- 7.1 app登录-接口定义
- 7.1.1 DTO的LoginDto类
- 7.1.2 Controller层ApUserLoginController类
- 7.1.3 Mapper层ApUserMapper接口
- 7.1.4 Service层ApUserService接口
- 7.1.5 实现ApUserService接口
- 7.1.6 完善业务层接口
- 7.2 登录思路分析
- 7.2.1 业务层登录实现
- 7.2.2 Controller注入
- 7.2.3 测试
- 8 App端网关
- 8.1 导入依赖
- 8.2 微服务创建网关
- 8.2.1 AppGateway
- 8.2.2 创建配置文件bootstrap.yml
- 8.2.3 在Nacos中创建配置中心
- 9 认证过滤器
- 9.1 全局过滤器
- 9.2 测试
- 10 app前端项目集成
- 10.1 Nginx反向代理和静态资源配置
1 环境搭建及简介
2 项目介绍
2.1 应用
2.2 业务说明
2.3 技术栈
前端
后端
2.4 收获
2.5 大纲
3 Nacos准备
3.1 安装Nacos
1)拉取镜像
docker pull nacos/nacos-server:1.2.0
2)创建容器
docker run --env MODE=standalone --name nacos --restart=always -d -p 8848:8848 nacos/nacos-server:1.2.0
3)查看日志
docker logs -f nacos
4)访问当前Nacos
http://192.168.204.129:8848/nacos
4 初始工程搭建
4.1 环境准备
4.1.1 导入项目
解压heima-leadnews.zip,并且导入idea,设置jdk为1.8
4.1.2 设置本地仓库
respository_new.zip解压到本地仓库文件夹
打开idea-settings设置本地仓库
修改为
4.1.3 设置项目编码格式
4.2 全局异常
heima-leadnews-common模块下的com.heima.common.exception包下自定义异常类CustomException,如果我们手动抛出异常就需要抛出CustomException类
public class CustomException extends RuntimeException {private AppHttpCodeEnum appHttpCodeEnum;public CustomException(AppHttpCodeEnum appHttpCodeEnum){this.appHttpCodeEnum = appHttpCodeEnum;}public AppHttpCodeEnum getAppHttpCodeEnum() {return appHttpCodeEnum;}
}
另外一个类ExceptionCatch类,全局异常拦截类
注解@ControllerAdvice
:ControllerAdvice本质上是一个Component,因此也会被当成组件扫描,一视同仁,扫扫扫。
- 这个类是为那些声明了(@ExceptionHandler、@InitBinder 或 @ModelAttribute注解修饰的)方法的类而提供的专业化的@Component , 以供多个 Controller类所共享。
@ControllerAdvice //控制器增强类
@Slf4j
public class ExceptionCatch {/*** 处理不可控异常* @param e* @return*/@ExceptionHandler(Exception.class)@ResponseBodypublic ResponseResult exception(Exception e){e.printStackTrace();log.error("catch exception:{}",e.getMessage());return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR);}/*** 处理可控异常 自定义异常* @param e* @return*/@ExceptionHandler(CustomException.class)@ResponseBodypublic ResponseResult exception(CustomException e){log.error("catch exception:{}",e);return ResponseResult.errorResult(e.getAppHttpCodeEnum());}
}
在heima-leadnews-model模块下的com.heima.model包下的common包下的enums包下有个枚举类AppHttpCodeEnum,用来存放对异常的说明
public enum AppHttpCodeEnum {// 成功段0SUCCESS(200,"操作成功"),// 登录段1~50NEED_LOGIN(1,"需要登录后操作"),LOGIN_PASSWORD_ERROR(2,"密码错误"),// TOKEN50~100TOKEN_INVALID(50,"无效的TOKEN"),TOKEN_EXPIRE(51,"TOKEN已过期"),TOKEN_REQUIRE(52,"TOKEN是必须的"),// SIGN验签 100~120SIGN_INVALID(100,"无效的SIGN"),SIG_TIMEOUT(101,"SIGN已过期"),// 参数错误 500~1000PARAM_REQUIRE(500,"缺少参数"),PARAM_INVALID(501,"无效参数"),PARAM_IMAGE_FORMAT_ERROR(502,"图片格式有误"),SERVER_ERROR(503,"服务器内部错误"),// 数据错误 1000~2000DATA_EXIST(1000,"数据已经存在"),AP_USER_DATA_NOT_EXIST(1001,"ApUser数据不存在"),DATA_NOT_EXIST(1002,"数据不存在"),// 数据错误 3000~3500NO_OPERATOR_AUTH(3000,"无权限操作"),NEED_ADMIND(3001,"需要管理员权限");int code;String errorMessage;AppHttpCodeEnum(int code, String errorMessage){this.code = code;this.errorMessage = errorMessage;}public int getCode() {return code;}public String getErrorMessage() {return errorMessage;}
}
4.2.1 自动装配
resource下META-INF有spring.factories,只有有微服务引用了heima-leadnews-common,服务器初始化spring容器的时候就会找到spring.factories,把spring.factories中需要自动配置的文件加载到当前微服务的容器中,也就能用全局处理器了。
4.3 工程主体结构
5 登录功能开发
5.1 需求分析
5.1.1 表结构分析
打开本地sql工具,引入sql脚本,创建leadnews_user,有四张表
5.1.2 实体类ApUser的导入
在heima-leadnews-model模块下创建com.heima.model.user.pojos包下创建实体类ApUser
@TableName("ap_user")
:表的映射
@TableId(value = "id", type = IdType.AUTO)
:主键的映射
@TableField("name")
:其他字段的映射
@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;}
5.1.3 表结构中salt的解释
salt:密码、通信等加密盐
5.1.3.1 注册过程
5.1.3.2 登录过程
6 用户端微服务的搭建
heima-leadnews-service模块是管理其他所有微服务模块
6.1 service模块的依赖说明
6.2 创建用户微服务模块
在heima-leadnews-service下创建模块
6.2.1 创建引导类
然后创建一个包com.heima.user,再创建一个引导类UserApplication
@SpringBootApplication
: 启动类注解
@EnableDiscoveryClient
:集成当前的注册中心,加入注册中心
@MapperScan("com.heima.user.mapper")
:继承MybatisPlus,扫描Mapper接口,没有就在com.heima.user下创建mapper包
@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.heima.user.mapper")
public class UserApplication {public static void main(String[] args) {SpringApplication.run(UserApplication.class, args);}
}
6.2.2 创建controller.v1、service、mapper、config
controller.v1是不同版本的controller
6.2.3 创建resources的配置文件
6.2.3.1 bootstrap.yml
若application.yml 和bootstrap.yml在同一目录下:bootstrap.yml 先加载 application.yml后加载,application.yml会覆盖
创建bootstrap.yml
server:port: 51801
spring:application:name: leadnews-usercloud:nacos:discovery:server-addr: 192.168.204.129:8848config:server-addr: 192.168.204.129:8848file-extension: yml
这里面暂时只有Nacos的配置,关于数据库的等等都需要在Nacos的配置中心进行配置
spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/leadnews_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=falseusername: rootpassword: 123sjbsjb
# 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
mybatis-plus:mapper-locations: classpath*:mapper/*.xml# 设置别名包扫描路径,通过该属性可以给包中的类注册别名type-aliases-package: com.heima.model.user.pojos
发布查看
6.2.3.2 logback.xml
创建日志文件logback.xml
<?xml version="1.0" encoding="UTF-8"?><configuration><!--定义日志文件的存储地址,使用绝对路径--><property name="LOG_HOME" value="D:\Code\JavaCode\HeimaToutiao\logs"/><!-- Console 输出设置 --><appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"><encoder><!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符--><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern><charset>utf8</charset></encoder></appender><!-- 按照每天生成日志文件 --><appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><!--日志文件输出的文件名--><fileNamePattern>${LOG_HOME}/leadnews.%d{yyyy-MM-dd}.log</fileNamePattern></rollingPolicy><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern></encoder></appender><!-- 异步输出 --><appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender"><!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 --><discardingThreshold>0</discardingThreshold><!-- 更改默认的队列的深度,该值会影响性能.默认值为256 --><queueSize>512</queueSize><!-- 添加附加的appender,最多只能添加一个 --><appender-ref ref="FILE"/></appender><logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG" additivity="false"><appender-ref ref="CONSOLE"/></logger><logger name="org.springframework.boot" level="debug"/><root level="info"><!--<appender-ref ref="ASYNC"/>--><appender-ref ref="FILE"/><appender-ref ref="CONSOLE"/></root>
</configuration>
6.3 整体用户端框架
7 登录接口实现
7.1 app登录-接口定义
7.1.1 DTO的LoginDto类
heima-leadnews-model模块下的com.heima.model.user.pojos.dtos包下创建LoginDto类
@Data
public class LoginDto {private String phone;private String password;
}
7.1.2 Controller层ApUserLoginController类
再在heima-leadnews-service模块下的com.heima.user.controller.v1下创建ApUserLoginController类
POST请求采用@RequestBody
@RestController
@RequestMapping("/api/v1/login")
public class ApUserLoginController {@PostMapping("/login_auth")public ResponseResult login(@RequestBody(required=false) LoginDto dto) {return null;}
}
7.1.3 Mapper层ApUserMapper接口
使用mybatisplus
在com.heima.user.mapper创建ApUserMapper接口
@Mapper
public interface ApUsermapper extends BaseMapper<ApUser> {}
7.1.4 Service层ApUserService接口
public interface ApUserService extends IService<ApUser>{
}
7.1.5 实现ApUserService接口
@Service
@Transactional
@Slf4j
public class ApUserServiceImpl extends ServiceImpl<ApUsermapper, ApUser> implements ApUserService {
}
@Service
标注业务层实现
@Transactional
标注事务
7.1.6 完善业务层接口
public interface ApUserService extends IService<ApUser>{/*** 登录功能* @param dto* @return*/public ResponseResult login(LoginDto dto);
}
实现方法
@Service
@Transactional
@Slf4j
public class ApUserServiceImpl extends ServiceImpl<ApUsermapper, ApUser> implements ApUserService {@Overridepublic ResponseResult login(LoginDto dto) {return null;}
}
7.2 登录思路分析
7.2.1 业务层登录实现
在ApUserServiceImpl类中实现思路
@Service
@Transactional
@Slf4j
public class ApUserServiceImpl extends ServiceImpl<ApUsermapper, ApUser> implements ApUserService {@Overridepublic ResponseResult login(LoginDto dto) {//1.正常登录 用户名、密码if(StringUtils.isNotBlank(dto.getPhone()) && StringUtils.isNotBlank(dto.getPassword())) {//1.1 查询用户信息,根据手机号查询用户信息ApUser dbUser = getOne(Wrappers.<ApUser>lambdaQuery().eq(ApUser::getPhone, dto.getPhone()));if(dbUser == null) {return ResponseResult.errorResult(AppHttpCodeEnum.AP_USER_DATA_NOT_EXIST, "用户不存在");}//1.2 比对密码String salt = dbUser.getSalt();String password = dto.getPassword();String pwd = DigestUtils.md5DigestAsHex((password + salt).getBytes());if(!pwd.equals(dbUser.getPassword())) {return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR, "密码错误");}//1.3 没问题的话,返回数据,生成jwtString token = AppJwtUtil.getToken(dbUser.getId().longValue());Map<String,Object> map=new HashMap<>();map.put("token",token);//清空敏感信息,前端只要id,手机号,昵称ApUser userVO=new ApUser();userVO.setId(dbUser.getId());userVO.setPhone(dbUser.getPhone());userVO.setName(dbUser.getName());map.put("user",userVO);return ResponseResult.okResult(map);}else{//2.游客登录Map<String,Object> map=new HashMap<>();map.put("token",AppJwtUtil.getToken(0L));return ResponseResult.okResult(map);}}
}
加密采用DigestUtils.md5DigestAsHex
传入字节流
生成JWT令牌是使用heima-leadnews-utils模块下的com.heima.utils.common包下的AppJWTUtil
public class AppJwtUtil {// TOKEN的有效期一天(S)private static final int TOKEN_TIME_OUT = 3_600;// 加密KEYprivate static final String TOKEN_ENCRY_KEY = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY";// 最小刷新间隔(S)private static final int REFRESH_TIME = 300;// 生产IDpublic static String getToken(Long id){Map<String, Object> 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<Claims> 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());// 需要自动刷新TOKENif((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<Claims> jws = AppJwtUtil.getJws("eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAADWLQQqEMAwA_5KzhURNt_qb1KZYQSi0wi6Lf9942NsMw3zh6AVW2DYmDGl2WabkZgreCaM6VXzhFBfJMcMARTqsxIG9Z888QLui3e3Tup5Pb81013KKmVzJTGo11nf9n8v4nMUaEY73DzTabjmDAAAA.4SuqQ42IGqCgBai6qd4RaVpVxTlZIWC826QA9kLvt9d-yVUw82gU47HDaSfOzgAcloZedYNNpUcd18Ne8vvjQA");Claims claims = jws.getBody();System.out.println(claims.get("id"));}}
7.2.2 Controller注入
@RestController
@RequestMapping("/api/v1/login")
public class ApUserLoginController {@Autowiredprivate ApUserService apUserService;@PostMapping("/login_auth")public ResponseResult login(@RequestBody LoginDto dto) {return apUserService.login(dto);}
}
7.2.3 测试
启动redis报错
修改bootstrap.yml文件,添加redis地址
redis:host: 127.0.0.1port: 6379database: 0
启动,Nacos的服务管理中有leadnews-user
访问http://localhost:51801/api/v1/login/login_auth
8 App端网关
8.1 导入依赖
在heima-leadnews-gateway导入以下依赖
<dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId></dependency>
</dependencies>
8.2 微服务创建网关
8.2.1 AppGateway
针对各个微服务创建相应的网关
创建引导类com.heima.app.gateway.AppGatewayApplication
@SpringBootApplication
@EnableDiscoveryClient
public class AppGatewayApplication {public static void main(String[] args) {SpringApplication.run(AppGatewayApplication.class, args);}
}
8.2.2 创建配置文件bootstrap.yml
server:port: 51601
spring:application:name: leadnews-app-gatewaycloud:nacos:discovery:server-addr: 192.168.204.129:8848config:server-addr: 192.168.204.129:8848file-extension: yml
8.2.3 在Nacos中创建配置中心
spring:cloud:gateway:globalcors:add-to-simple-url-handler-mapping: truecorsConfigurations:'[/**]':allowedHeaders: "*"allowedOrigins: "*"allowedMethods:- GET- POST- DELETE- PUT- OPTIONroutes:# 平台管理- id: useruri: lb://leadnews-userpredicates:- Path=/user/**filters:- StripPrefix= 1
id: user
: 这是此路由的唯一标识符。uri: lb://leadnews-user
: 当请求匹配到该路由时,它会被路由到"leadnews-user"服务的实例。URI的前缀"lb://"表示路由到负载均衡器。predicates
: 定义了路由的匹配规则。在这种情况下,请求路径必须以"/user/"开头才会匹配到此路由。filters
: 定义了路由的过滤器。在这里,使用了"StripPrefix"过滤器,该过滤器会将请求的路径中的一层前缀移除。具体来说,这里是将路径中的第一层"/user"移除,以便在转发请求到"leadnews-user"服务时去除不必要的前缀。
访问http://localhost:51601/user/api/v1/login/login_auth
9 认证过滤器
9.1 全局过滤器
要实现这个功能,要实现一个全局的过滤器
在heima-leadnews-gateway模块下创建com.heima.app.gateway.filter.AuthorizeFilter类用作全局过滤器
@Component
@Slf4j
public class AuthorizeFilter implements Ordered, GlobalFilter {@Overridepublic Mono<Void> 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是否有效//5.1 解析tokentry{Claims body = AppJwtUtil.getClaimsBody(token);//5.2 判断token是否有效int result = AppJwtUtil.verifyToken(body);if(result == 1||result == 2) {//5.3 token过期response.setStatusCode(HttpStatus.UNAUTHORIZED);return response.setComplete();}}catch (Exception e) {e.printStackTrace();//5.4 token无效response.setStatusCode(HttpStatus.UNAUTHORIZED);return response.setComplete();}//6.放行return chain.filter(exchange);}/*** 过滤器的执行顺序,返回值越小,执行优先级越高* @return*/@Overridepublic int getOrder() {return 0;}
}
把AppJwtUtil放入gateway的工具包中
9.2 测试
包含/login直接放行
10 app前端项目集成
10.1 Nginx反向代理和静态资源配置
访问http://localhost:80
将静态资源和nginx都放在工作目录下
在nginx的conf目录下创建文件夹leadnews.conf,新建文件heimi-leadnews-app.conf
nginx端口8801然后转发到gateway端口51601,gateway添加user路径后通过Nacos路由到leadnews-user也就是端口51801完成访问
静态资源存放在D:/Code/JavaCode/HeimaToutiao/app-web/展示其目录下的index.html
upstream heima-app-gateway{server localhost:51601;
}server {listen 8801;location / {root D:/Code/JavaCode/HeimaToutiao/app-web/;index index.html;}location ~/app/(.*) {proxy_pass http://heima-app-gateway/$1;proxy_set_header HOST $host; # 不改变源请求头的值proxy_pass_request_body on; #开启获取请求体proxy_pass_request_headers on; #开启获取请求头proxy_set_header X-Real-IP $remote_addr; # 记录真实发出请求的客户端IPproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #记录代理信息}
}
在nginx.conf中引入
#user nobody;
worker_processes 1;events {worker_connections 1024;
}
http {include mime.types;default_type application/octet-stream;sendfile on;keepalive_timeout 65;# 引入自定义配置文件include leadnews.conf/*.conf;
}
访问http://localhost:8801
访问成功