一、oauth 安全认证子模块创建
1.1、右键项目 - New - Module
image.png1.2、bootstrap.yml
server:
port: 9002
servlet:
context-path:
spring:
application:
name: oauth
profiles:
active: dev # 运行环境
freemarker:
check-template-location: false
prefer-file-system-access: false
logging: # logback 配置
path: /usr/local/alibaba/logs/${spring.application.name} # 保存日志文件目录路径
level: # 日志级别
org.springframework.web: DEBUG # 配置spring web日志级别
1.3、bootstrap-dev.yml
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.0.119:8848 # nacos的访问地址,根据上面准备工作中启动的实例配置
namespace: 11ba48cc-9931-4760-bf58-7d3e2c99629c # Nacos test命名空间的ID
config:
server-addr: 192.168.0.119:8848 # nacos的访问地址,根据上面准备工作中启动的实例配置
namespace: 11ba48cc-9931-4760-bf58-7d3e2c99629c # Nacos test命名空间的ID
group: DEFAULT_GROUP # 默认分组就是DEFAULT_GROUP,如果使用默认分组可以不配置
file-extension: yml # 默认properties
1.4、pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.aydan</groupId>
<artifactId>ali-cloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>oauth</artifactId>
<packaging>jar</packaging>
<name>oauth</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.aydan</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos service discovery client依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--nacos config client 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Java Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<!--导入spring cloud oauth2依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Swagger 第三方UI -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>1.9.6</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
1.5、OauthApplication.java
package com.aydan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.Environment;
import javax.annotation.PostConstruct;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.TimeZone;
/**
* @Author ds
* @Date 2023/7/31
*/
@SpringBootApplication
@EnableDiscoveryClient
public class OauthApplication {
public static void main(String[] args) {
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
// SpringApplication.run(MasterApplication.class, args);
ConfigurableApplicationContext application = SpringApplication.run(OauthApplication.class, args);
Environment environment = application.getEnvironment();
String applicationName = environment.getProperty("spring.application.name");
String port = environment.getProperty("server.port");
String contextPath = environment.getProperty("server.servlet.context-path");
System.out.println("---------------------------------------------------------->");
System.out.println(" :: ServletInitializer 启动:" + applicationName);
String localHost;
try {
localHost = InetAddress.getLocalHost().getHostAddress();
System.out.println("\t\t http://" + localHost + ":" + port + "" + contextPath + "");
} catch (UnknownHostException e) {
System.out.println("\t\t " + e.getMessage());
e.printStackTrace();
}
System.out.println("<----------------------------------------------------------");
}
@PostConstruct
void setDefaultTimezone() {
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
}
}
1.6、Nacos配置 oauth-dev.yml
image.pngspring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://********:3306/db_name?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
username: root
password: ********
druid:
initialSize: 100
minIdle: 100
maxActive: 300
maxWait: 50000
redis:
database:
host: 192.168.0.119
port: 6379
#密码
password: ********
#客户端超时时间单位是毫秒 默认是2000
timeout: 10000
#最大空闲数
maxIdle: 300
#控制一个pool可分配多少个jedis实例,用来替换上面的redis.maxActive,如果是jedis 2.4以后用该属性
maxTotal: 1000
#最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。
maxWaitMillis: 1000
#连接的最小空闲时间 默认1800000毫秒(30分钟)
minEvictableIdleTimeMillis: 300000
#每次释放连接的最大数目,默认3
numTestsPerEvictionRun: 1024
#逐出扫描的时间间隔(毫秒) 如果为负数,则不运行逐出线程, 默认-1
timeBetweenEvictionRunsMillis: 30000
#是否在从池中取出连接前进行检验,如果检验失败,则从池中去除连接并尝试取出另一个
testOnBorrow: true
#在空闲时检查有效性, 默认false
testWhileIdle: true
二、使用 Redis 的方式来实现 token 的存储
添加四个类: OAuthConfig.java、OAuthJwtAccessTokenConverter.java、RedisTokenStoreConfig.java、WebSecurityConfig.java
2.1、OAuthConfig.java
package config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import javax.sql.DataSource;
/**
* @Author ds
* @Date 2023/7/31
*/
@Configuration
@EnableAuthorizationServer
public class OAuthConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private TokenStore redisTokenStore;
@Autowired
public PasswordEncoder passwordEncoder;
@Autowired
public UserDetailsService kiteUserDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
/**
* 认证服务器Endpoints配置
* spring security token的生成方式
* @param endpoints
* @throws Exception
*/
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 设置access_token存储器,redis
endpoints.tokenStore(redisTokenStore)
.authenticationManager(authenticationManager)
.userDetailsService(kiteUserDetailsService)
.accessTokenConverter(accessTokenConverter());
}
/**
* AccessToken转换器-定义token的生成方式,这里使用JWT生成token,对称加密只需要加入key等其他信息(自定义)。
* @return JwtAccessTokenConverter
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
// 用新的jwt转换器
return new OAuthJwtAccessTokenConverter();
}
/**
* client存储方式,此处使用jdbc存储
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource).passwordEncoder(passwordEncoder);
}
/**
* 认证服务器相关接口权限管理
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients()
.checkTokenAccess("isAuthenticated()")
.tokenKeyAccess("isAuthenticated()");
}
}
2.2、OAuthJwtAccessTokenConverter.java
package config;
import com.aydan.base.GlobalVar;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @Author ds
* @Date 2023/7/31
*/
public class OAuthJwtAccessTokenConverter extends JwtAccessTokenConverter {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
DefaultOAuth2AccessToken defaultOAuth2AccessToken = new DefaultOAuth2AccessToken(accessToken);
// 设置过期时间
defaultOAuth2AccessToken.setExpiration(new Date(System.currentTimeMillis() + 1000 * 3600 * 24 * 7)); // 有效期一周
List<String> authorities = new ArrayList<>();
// 设置额外用户信息
User user = ((User) authentication.getPrincipal());
user.getAuthorities().forEach(authority -> authorities.add(authority.getAuthority()));
// 将用户信息添加到token额外信息中
defaultOAuth2AccessToken.getAdditionalInformation().put("username", user.getUsername());
defaultOAuth2AccessToken.getAdditionalInformation().put("authorities", authorities);
// 设置秘钥
super.setSigningKey(GlobalVar.OAUTH_TOKEN_SECRET);
return super.enhance(defaultOAuth2AccessToken, authentication);
}
}
2.3、RedisTokenStoreConfig.java
package config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
/**
* @Author ds
* @Date 2023/7/31
*/
@Configuration
public class RedisTokenStoreConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public TokenStore redisTokenStore (){
return new RedisTokenStore(redisConnectionFactory);
}
}
2.4、WebSecurityConfig.java
package config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @Author ds
* @Date 2023/7/31
*/
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 认证管理
* @return 认证管理对象
* @throws Exception 认证异常信息
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 允许匿名访问所有接口 主要是 oauth 接口
* @param http HTTP协议安全配置实例
* @throws Exception 设置异常
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**")
.permitAll();
}
}
2.5、oauth_client 表
-- oauth_client表
create table oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY,
resource_ids VARCHAR(256),
client_secret VARCHAR(256),
scope VARCHAR(256),
authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(256)
);
INSERT INTO oauth_client_details
(client_id, client_secret, scope, authorized_grant_types,
web_server_redirect_uri, authorities, access_token_validity,
refresh_token_validity, additional_information, autoapprove)
VALUES
('user-client', '$2a$10$o2l5kA7z.Caekp72h5kU7uqdTDrlamLq.57M1F6ulJln9tRtOJufq', 'all',
'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true);
INSERT INTO oauth_client_details
(client_id, client_secret, scope, authorized_grant_types,
web_server_redirect_uri, authorities, access_token_validity,
refresh_token_validity, additional_information, autoapprove)
VALUES
('order-client', '$2a$10$GoIOhjqFKVyrabUNcie8d.ADX.qZSxpYbO6YK4L2gsNzlCIxEUDlW', 'all',
'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true);
2.6、sa_user 表
CREATE TABLE `sa_user` (
`user_id` varchar(40) NOT NULL,
`user_name` varchar(20) NOT NULL,
`passwd` varchar(100) DEFAULT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
-- 正常数据库密码加密,这里只为演示
2.7、AuthUserService.java
package com.aydan.user.service;
import com.aydan.business.dao.SaUserDao;
import com.aydan.business.entity.SaUserEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
/**
* @Author ds
* @Date 2023/7/31
*/
@Slf4j
@Component
public class AuthUserService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
protected SaUserDao saUserDao;
/**
* 根据用户名,取用户详细信息
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SaUserEntity saUserEntity = saUserDao.getByUserName(username);
if (saUserEntity == null) {
throw new UsernameNotFoundException("用户不存在");
}
String original = saUserEntity.getPasswd();
String password = passwordEncoder.encode(original); // 解密密码
System.out.println("* ------------------------------------------------------> AuthUserService.loadUserByUsername()");
System.out.println("* 用户名:" + username);
System.out.println("* 解密后的密码:" + password);
System.out.println("* <------------------------------------------------------");
return new User(username, password, new ArrayList<>());
}
}
2.8、common中增加sa_user的相关实体类
image.pngpackage com.aydan.business.dao;
import com.aydan.business.entity.SaUserEntity;
import com.aydan.business.vo.SaUserSearchVo;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @Author ds
* @Date 2023/8/1
*/
@Mapper
public interface SaUserDao extends BaseMapper<SaUserEntity> {
List<SaUserEntity> queryPage(SaUserSearchVo searchVo);
SaUserEntity getById(@Param("userId") String userId);
void removeById(@Param("userId") String userId);
SaUserEntity getByUserName(@Param("userName") String userName);
}
package com.aydan.business.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/**
* @Author ds
* @Date 2023/8/1
*/
@Data
@TableName(value = "sa_user")
@ApiModel(value = "SaUser", description = "用户基本信息")
public class SaUserEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户id
*/
@TableId
@ApiModelProperty(value = "用户id", example = "示例:1", notes = "用户id")
private String userId;
/**
* 登录名
*/
@ApiModelProperty(value = "登录名", example = "示例:登录名", notes = "登录名")
private String userName;
/**
* 登录密码
*/
@ApiModelProperty(value = "登录密码", example = "示例:登录密码", notes = "登录密码")
private String passwd;
}
package com.aydan.business.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import java.io.Serializable;
/**
* @Author ds
* @Date 2023/8/1
*/
@Data
@ApiModel(value = "SaUserSaveVo", description = "用户基本信息")
public class SaUserSaveVo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户id
*/
@Length(max = 40, message = "长度不能超过40个字符")
@ApiModelProperty(value = "用户id", example = "示例:用户id", notes = "用户id")
private String userId;
/**
* 登录名
*/
@Length(max = 20, message = "长度不能超过20个字符")
@ApiModelProperty(value = "登录名", example = "示例:登录名", notes = "登录名")
private String userName;
/**
* 登录密码
*/
@Length(max = 100, message = "长度不能超过100个字符")
@ApiModelProperty(value = "登录密码", example = "示例:登录密码", notes = "登录密码")
private String passwd;
}
package com.aydan.business.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/**
* @Author ds
* @Date 2023/8/1
*/
@Data
@ApiModel(value = "SaUserSearchVo", description = "用户基本信息")
public class SaUserSearchVo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 当前页数
*/
@NotNull(message="当前页属性不能为空")
@ApiModelProperty(value="当前页数", example="1", notes="当前页数")
private int page;
/**
* 每页显示记录数
*/
@Max(value=1000, message="每页显示记录数不能超过1000")
@ApiModelProperty(value="每页显示记录数", example="10", notes="每页显示记录数")
private int limit;
/**
* 要排序的字段
*/
@ApiModelProperty(value = "要排序的字段", example = "createDt", notes = "要排序的字段")
private String sidx;
/**
* 排序方式
*/
@ApiModelProperty(value = "排序方式", example = "示例:asc/desc", notes = "排序方式")
private String order;
/**
* 用户id
*/
@ApiModelProperty(value = "用户id", example = "示例:1", notes = "用户id")
private String userId;
/**
* 登录名
*/
@ApiModelProperty(value = "登录名", example = "示例:登录名", notes = "登录名")
private String userName;
/**
* 登录密码
*/
@ApiModelProperty(value = "登录密码", example = "示例:登录密码", notes = "登录密码")
private String passwd;
}
package com.aydan.business.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import java.io.Serializable;
/**
* @Author ds
* @Date 2023/8/1
*/
@Data
@ApiModel(value = "SaUserUpdateVo", description = "用户基本信息")
public class SaUserUpdateVo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户id
*/
@Length(max = 40, message = "长度不能超过40个字符")
@ApiModelProperty(value = "用户id", example = "示例:1", notes = "用户id")
private String userId;
/**
* 登录名
*/
@Length(max = 20, message = "长度不能超过20个字符")
@ApiModelProperty(value = "登录名", example = "示例:登录名", notes = "登录名")
private String userName;
/**
* 登录密码
*/
@Length(max = 100, message = "长度不能超过100个字符")
@ApiModelProperty(value = "登录密码", example = "示例:登录密码", notes = "登录密码")
private String passwd;
}
package com.aydan.business.service;
import com.aydan.base.PageBean;
import com.aydan.business.entity.SaUserEntity;
import com.aydan.business.vo.SaUserSaveVo;
import com.aydan.business.vo.SaUserSearchVo;
import com.aydan.business.vo.SaUserUpdateVo;
/**
* @Author ds
* @Date 2023/8/1
*/
public interface SaUserService {
PageBean<SaUserEntity> queryList(SaUserSearchVo searchVo);
void create(SaUserSaveVo saveVo) throws Exception;
void update(SaUserUpdateVo updateVo) throws Exception;
void logicDelete(String userId);
void switchEnable(String userId, int enabled);
SaUserEntity getById(String userId);
SaUserEntity getByUserName(String userName);
void removeById(String userId);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.aydan.business.dao.SaUserDao">
<!-- 可根据自己的需求,是否要使用 -->
<resultMap type="com.aydan.business.entity.SaUserEntity" id="saUserMap">
<result property="userId" column="user_id"/>
<result property="userName" column="user_name"/>
<result property="passwd" column="passwd"/>
</resultMap>
<sql id="Base_Column_List">
user_id , user_name, passwd</sql>
<sql id="Base_Where">
<trim prefix="WHERE" prefixOverrides="AND|OR">
<if test="userId != null">AND `user_id` = #{userId}</if>
<if test="userName != null">AND `user_name` = #{userName}</if>
<if test="passwd != null">AND `passwd` = #{passwd}</if>
</trim>
</sql>
<select id="queryPage" parameterType="com.aydan.business.vo.SaUserSearchVo" resultMap="saUserMap">
SELECT
<include refid="Base_Column_List"/>
FROM sa_user
<include refid="Base_Where"/>
<choose>
<when test="sidx != null and sidx.trim() != ''">
order by ${sidx} ${order}
</when>
<otherwise>
order by user_name desc
</otherwise>
</choose>
</select>
<select id="getById" resultType="com.aydan.business.entity.SaUserEntity">
select *
from sa_user
where `user_id` = #{userId}
</select>
<select id="getByUserName" resultType="com.aydan.business.entity.SaUserEntity">
select *
from sa_user
where `user_name` = #{userName}
</select>
<delete id="removeById">
delete
from sa_user
where `user_id` = #{userId}
</delete>
</mapper>
2.9、engine 子模块中创建UserServiceImpl.java
package com.aydan.service.impl;
import com.aydan.base.PageBean;
import com.aydan.business.dao.SaUserDao;
import com.aydan.business.entity.SaUserEntity;
import com.aydan.business.service.SaUserService;
import com.aydan.business.vo.SaUserSaveVo;
import com.aydan.business.vo.SaUserSearchVo;
import com.aydan.business.vo.SaUserUpdateVo;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.apache.dubbo.config.annotation.DubboService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
/**
* @Author ds
* @Date 2023/8/1
*/
@DubboService
public class UserServiceImpl extends ServiceImpl<SaUserDao, SaUserEntity> implements SaUserService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Resource
private SaUserDao saUserDao;
@Override
public PageBean<SaUserEntity> queryList(SaUserSearchVo searchVo) {
PageBean<SaUserEntity> pageBean = new PageBean<>();
try {
PageHelper.startPage(searchVo.getPage(),searchVo.getLimit());
PageInfo<SaUserEntity> page = new PageInfo<>(saUserDao.queryPage(searchVo));
pageBean.setCount(new Long(page.getTotal()).intValue()); // 条数
pageBean.setLimit(searchVo.getLimit()); // 设置每页显示记录数
pageBean.setCurrentPage(searchVo.getPage()); // 设置当前页数
pageBean.setData(page.getList()); // 设置数据
pageBean.setStatus(0); // 设置状态码
pageBean.setMessage(" 查询完成"); // 设置返回信息
} finally {
PageHelper.clearPage();
}
return pageBean;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void create(SaUserSaveVo saveVo) throws Exception{
SaUserEntity saUserEntity = new SaUserEntity();
BeanUtils.copyProperties(saveVo, saUserEntity);
saUserDao.insert(saUserEntity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void update(SaUserUpdateVo updateVo) throws Exception{
SaUserEntity saUserEntity = new SaUserEntity();
BeanUtils.copyProperties(updateVo, saUserEntity);
saUserDao.updateById(saUserEntity);
}
@Override
public void switchEnable(String userId, int enabled) {
SaUserEntity saUserEntity = new SaUserEntity();
saUserEntity.setUserId(userId);
//saUserEntity.setModifierId(userEntity.getUserId());
//saUserEntity.setEnabled(enabled);
saUserDao.updateById(saUserEntity);
}
@Override
public void logicDelete(String userId) {
SaUserEntity saUserEntity = new SaUserEntity();
saUserEntity.setUserId(userId);
//saUserEntity.setModifier(userEntity.getId());
//saUserEntity.setIsDelete(1);
saUserDao.updateById(saUserEntity);
}
@Override
public SaUserEntity getById(String userId) {
return saUserDao.getById(userId);
}
@Override
public SaUserEntity getByUserName(String userName) {
return saUserDao.getByUserName(userName);
}
@Override
public void removeById(String userId) {
saUserDao.removeById(userId);
}
}
三、新增api子模块
3.1、右键项目 - New - Module
image.png3.2、ApiApplication.java
package com.aydan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.Environment;
import javax.annotation.PostConstruct;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.TimeZone;
/**
* @Author ds
* @Date 2023/8/1
*/
@SpringBootApplication
@EnableDiscoveryClient
public class ApiApplication {
public static void main(String[] args) {
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
// SpringApplication.run(MasterApplication.class, args);
ConfigurableApplicationContext application = SpringApplication.run(ApiApplication.class, args);
Environment environment = application.getEnvironment();
String applicationName = environment.getProperty("spring.application.name");
String port = environment.getProperty("server.port");
String contextPath = environment.getProperty("server.servlet.context-path");
System.out.println("---------------------------------------------------------->");
System.out.println(" :: ServletInitializer 启动:" + applicationName);
String localHost;
try {
localHost = InetAddress.getLocalHost().getHostAddress();
System.out.println("\t\t http://" + localHost + ":" + port + "" + contextPath + "");
} catch (UnknownHostException e) {
System.out.println("\t\t " + e.getMessage());
e.printStackTrace();
}
System.out.println("<----------------------------------------------------------");
}
@PostConstruct
void setDefaultTimezone() {
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
}
}
3.3、pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.aydan</groupId>
<artifactId>ali-cloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>api</artifactId>
<packaging>jar</packaging>
<name>api</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--集成公共模块-->
<dependency>
<groupId>com.aydan</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</exclusion>
<exclusion>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</exclusion>
<exclusion>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</exclusion>
<exclusion>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos service discovery client依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--nacos config client 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Java Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!-- 导入spring cloud oauth2依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring cloud+dubbo 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>
<!-- Swagger 第三方UI -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>1.9.6</version>
</dependency>
<!-- SpringCloud Alibaba Sentinel 流量监控控制台 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.22</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<includeSystemScope>true</includeSystemScope>
<!-- 是否限制解压缩 -->
<executable>false</executable>
<mainClass>com.aydan.ApiApplication</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
3.4、bootstrap.yml
server:
port: 9003
servlet:
context-path: /api
tomcat:
uri-encoding: utf-8
spring:
application:
name: api
profiles:
active: dev # 运行环境
freemarker:
check-template-location: false
prefer-file-system-access: false
logging: # logback 配置
path: /usr/local/alibaba/logs/${spring.application.name} # 保存日志文件目录路径
level: # 日志级别
org.springframework.web: DEBUG # 配置spring web日志级别
3.5、bootstrap-dev.yml
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.0.119:8848 # nacos的访问地址,根据上面准备工作中启动的实例配置
namespace: 11ba48cc-9931-4760-bf58-7d3e2c99629c # Nacos test命名空间的ID
config:
server-addr: 192.168.0.119:8848 # nacos的访问地址,根据上面准备工作中启动的实例配置
namespace: 11ba48cc-9931-4760-bf58-7d3e2c99629c # Nacos test命名空间的ID
group: DEFAULT_GROUP # 默认分组就是DEFAULT_GROUP,如果使用默认分组可以不配置
file-extension: yml # 默认properties
3.6、nacos 配置 bootstrap-dev.yml
image.pngspring:
redis:
database:
host: 192.168.0.119
port: 6379
password: 01810bd1983b496684a4c13ab2580cd1
timeout: 5000
security:
oauth2:
client:
client-id: user-client
client-secret: user-secret-8888
user-authorization-uri: http://localhost:9002/oauth/authorize
access-token-uri: http://localhost:9002/oauth/token
resource:
id: user-client
user-info-uri: user-info
authorization:
check-token-access: http://localhost:9002/oauth/check_token
dubbo:
consumer:
timeout: 36000
protocol:
# dubbo 协议
name: dubbo
# 配置本机内网地址
host: 127.0.0.1
# dubbo 协议端口( -1 表示自增端口,从 20880 开始)
port: -1
registry:
# 挂载到 Spring Cloud 注册中心
address: spring-cloud://192.168.0.119
cloud:
# 订阅服务提供方的应用列表,订阅多个服务提供者使用 "," 连接
subscribed-services: engine
rocketmq:
name-server: 192.168.0.119:9876
producer:
# 小坑:必须指定group
group: ali-group
3.7 ResourceServerConfig.java
package com.aydan.config;
import com.aydan.component.ApiAuthenticationEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Value("${security.oauth2.client.client-id}")
private String clientId;
@Value("${security.oauth2.client.client-secret}")
private String secret;
@Value("${security.oauth2.authorization.check-token-access}")
private String checkTokenEndpointUrl;
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Autowired
private ApiAuthenticationEntryPoint apiAuthEnticationEntryPoint;
@Bean
public TokenStore redisTokenStore() {
return new RedisTokenStore(redisConnectionFactory);
}
@Bean
public RemoteTokenServices tokenService() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setClientId(clientId);
tokenService.setClientSecret(secret);
tokenService.setCheckTokenEndpointUrl(checkTokenEndpointUrl);
return tokenService;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.authenticationEntryPoint(apiAuthEnticationEntryPoint);
}
}
3.8 ApiAuthenticationEntryPoint.java
package com.aydan.component;
import com.alibaba.fastjson.JSON;
import com.aydan.base.RestResponse;
import com.aydan.dict.ResultCode;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author ds
* @Date 2023/8/1
*/
@Component("apiAuthenticationEntryPoint")
public class ApiAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
Throwable cause = authException.getCause();
response.setHeader("Content-Type", "application/json;charset=UTF-8");
RestResponse restResponse = RestResponse.instance();
if (!(cause instanceof InvalidTokenException)) {
restResponse.setResult(ResultCode.TOKEN_LOSE);
} else {
restResponse.setResult(ResultCode.TOKEN_INVALID);
}
try {
response.getWriter().write(JSON.toJSONString(restResponse));
} catch (IOException e) {
System.err.println("\n* ############################### token认证失败 ###############################");
System.err.println("* token认证结果:" + cause.getMessage());
System.err.println("* 异常信息:" + e.getMessage());
System.err.println("* ");
e.printStackTrace();
}
}
}
3.9 UserBlockHandler.java
package com.aydan.config;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
import com.alibaba.fastjson.JSON;
import java.util.HashMap;
/**
* @Author ds
* @Date 2023/8/1
*/
public class UserBlockHandler {
public static String handleException(BlockException ex) {
HashMap<String, Object> map = new HashMap<>();
if (ex instanceof FlowException) {
map.put("code", -1);
map.put("msg", "系统限流,请稍等");
} else if (ex instanceof DegradeException) {
map.put("code", -2);
map.put("msg", "降级了");
} else if (ex instanceof ParamFlowException) {
map.put("code", -3);
map.put("msg", "热点参数限流");
} else if (ex instanceof SystemBlockException) {
map.put("code", -4);
map.put("msg", "系统规则(负载/...不满足要求)");
} else if (ex instanceof AuthorityException) {
map.put("code", -5);
map.put("msg", "授权规则不通过");
}
return JSON.toJSONString(map);
}
public static String handleError() {
HashMap<String, Object> map = new HashMap<>();
map.put("code", 500);
map.put("msg", "系统异常");
return JSON.toJSONString(map);
}
}
3.10 UserController.java
package com.aydan.business.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.fastjson.JSON;
import com.aydan.business.entity.SaUserEntity;
import com.aydan.business.service.SaUserService;
import com.aydan.config.UserBlockHandler;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author ds
* @Date 2023/8/1
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Reference
private SaUserService saUserService;
/**
* 获取用户信息
*/
@PostMapping("/userInfo")
public String userInfo() {
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
return JSON.toJSONString(saUserService.getByUserName(userName));
}
/**
* 测试流控规则
*/
@PostMapping("/testFlow")
@SentinelResource(value = "user-testFlow",
blockHandlerClass = UserBlockHandler.class, //对应异常类
blockHandler = "handleException", //只负责sentinel控制台配置违规
fallback = "handleError", //只负责业务异常
fallbackClass = UserBlockHandler.class)
public String testFlow() {
SaUserEntity saUserEntity = saUserService.getByUserName("admin");
return JSON.toJSONString(saUserEntity);
}
/**
* 测试降级规则
*/
@PostMapping("/testDegrade")
@SentinelResource(value = "user-testDegrade",
blockHandlerClass = UserBlockHandler.class, //对应异常类
blockHandler = "handleException", //只负责sentinel控制台配置违规
fallback = "handleError", //只负责业务异常
fallbackClass = UserBlockHandler.class)
public String testDegrade() {
SaUserEntity saUserEntity = saUserService.getByUserName("admin");
return JSON.toJSONString(saUserEntity);
}
}
3.12 获取token
* 假设咱们在一个 web 端使用,grant_type 是 password,表明这是使用 OAuth2 的密码模式。
* username=admin 和 password=123456 就相当于在 web 端登录界面输入的用户名和密码,我们在认证服务端配置中固定了用户名是 admin 、密码是 123456,而线上环境中则应该通过查询数据库获取。
* scope=all 是权限有关的,在认证服务的 OAuthConfig 中指定了 scope 为 all 。
* Authorization 要加在请求头中,格式为 Basic 空格 base64(clientId:clientSecret),这个微服务客户端的 client-id 是 user-client,client-secret 是 user-secret-8888,将这两个值通过冒号连接,并使用 base64 编码(user-client:user-secret-8888)之后的值为 dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==,可以通过 https://www.sojson.com/base64.html在线编码获取。
POST http://localhost:9002/oauth/token
Authorization: Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==
image.png
image.png
# 返回JSON:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2OTE1Njc4OTEsImp0aSI6IjRmMGM0NjhjLTkzODktNDRhZC1hMTM5LWMyOWMyNTA4M2QyZCIsImF1dGhvcml0aWVzIjpbXSwiY2xpZW50X2lkIjoidXNlci1jbGllbnQiLCJ1c2VybmFtZSI6ImFkbWluIn0.MWpUvmzLk7u-PG-LiN0Y--M3iExpi-7WITWBeAfeRdI",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiI0ZjBjNDY4Yy05Mzg5LTQ0YWQtYTEzOS1jMjljMjUwODNkMmQiLCJleHAiOjE2OTA5OTkwOTEsImp0aSI6Ijg5Yjc0ZDYxLTY5MTktNDM2Ni1hOTNiLTFkM2QxNTk4NjA2OCIsImF1dGhvcml0aWVzIjpbXSwiY2xpZW50X2lkIjoidXNlci1jbGllbnQiLCJ1c2VybmFtZSI6ImFkbWluIn0.9p1pwj7vz1CPdlZCOB2a7w47CBrikRNz-zTAoCtXGaA",
"expires_in": 604688,
"scope": "all",
"username": "admin",
"authorities": [],
"jti": "4f0c468c-9389-44ad-a139-c29c25083d2d"
}
access_token : 用户的token;
token_type : bearer
refresh_token : 用这个值换取新的token
expires_in : token 的过期时间(秒)
scope :client的作用域
username : 用户名
authorities :client的权限,不能为null
jti : jwt的唯一身份标识,避免重复
3.13 刷新token
token 过期后,用 refresh_token 换取 access_token
一般都会设置 access_token 的过期时间小于 refresh_token 的过期时间,以便在 access_token 过期后,不用用户再次登录的情况下,获取新的 access_token。
POST http://localhost:9002/oauth/token
Authorization Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==
image.png
image.png
# 返回JSON:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2OTE1NjkxNzQsImp0aSI6ImM1OTY0MTYzLTZmOGEtNDNjOS05ODM2LWM1YzM2MjE1NTg5OSIsImF1dGhvcml0aWVzIjpbXSwiY2xpZW50X2lkIjoidXNlci1jbGllbnQiLCJ1c2VybmFtZSI6ImFkbWluIn0.NLOwVkQkBQJSNEmTpoG1YIPSH4KoS3K8C25FA04vDjo",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiJjNTk2NDE2My02ZjhhLTQzYzktOTgzNi1jNWMzNjIxNTU4OTkiLCJleHAiOjE2OTA5OTkwOTEsImp0aSI6Ijg5Yjc0ZDYxLTY5MTktNDM2Ni1hOTNiLTFkM2QxNTk4NjA2OCIsImF1dGhvcml0aWVzIjpbXSwiY2xpZW50X2lkIjoidXNlci1jbGllbnQiLCJ1c2VybmFtZSI6ImFkbWluIn0.SGkHsv8EtSJXDFJUw1TMyXS0do9CC-ZsWwU9eH3inn8",
"expires_in": 604799,
"scope": "all",
"username": "admin",
"authorities": [],
"jti": "c5964163-6f8a-43c9-9836-c5c362155899"
}
3.14 请求获取当前登录用户信息接口
POST http://localhost:8008/api/user/userInfo
Authorization bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2OTE1NzM3MDIsImp0aSI6IjI5Zjg4YjJhLTJmMmYtNGU4ZS1hZjkyLTllNGFiMmFmOGQ5OSIsImF1dGhvcml0aWVzIjpbXSwiY2xpZW50X2lkIjoidXNlci1jbGllbnQiLCJ1c2VybmFtZSI6ImFkbWluIn0.oRNoTK-eG08RnyiZyFun8BrTkVe-IqqGOXj3wrXZK2U
image.png
# 如果token为空或者错误返回对应信息
image.png
参考:
https://www.jianshu.com/p/4fd45fb565eb
网友评论