访问:http://xxlssoclient1.com:8081/
采用 Debug 方式跟踪解析。关键断点:
采用密码模式,基于 Token 增强的微服务应用实现方案
认证服务配置AuthorizationServerConfig
/**
* 自定义Client查询,可以修改表名, 字段等
* @param clients
*/
@
Override@ SneakyThrows
public void configure(ClientDetailsServiceConfigurer clients) {
AuthClientDetailService clientDetailsService = new
AuthClientDetailService(dataSource);
clientDetailsService.setSelectClientDetailsSql(DEFAULT_SELECT_STATEMEN T);
clientDetailsService.setFindClientDetailsSql(DEFAULT_FIND_STATEMENT);
clients.withClientDetails(clientDetailsService);
}
/**
* 防止申请token时出现401错误
* @param oauthServer
*/
@
Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
/**
* 认证服务配置
* @param endpoints
*/
@
Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.allowedTokenEndpointRequestMethods(HttpMethod.GET,
HttpMethod.POST)
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancer())
.userDetailsService(authStockUserDetailService)
.authenticationManager(authenticationManager)
.reuseRefreshTokens(false);
}
/**
* TokenStore实现方式, 采用Redis缓存
* @return
*/
@
Bean
public TokenStore tokenStore() {
RedisTokenStore tokenStore = new
RedisTokenStore(redisConnectionFactory);
tokenStore.setPrefix(GlobalConstants.OAUTH_PREFIX_KEY);
tokenStore.setAuthenticationKeyGenerator(new DefaultAuthenticationKeyGenerator() {@
Override
public String extractKey(OAuth2Authentication authentication) {
return super.extractKey(authentication);
}
});
return tokenStore;
}
/**
* token增强处理, 支持扩展信息
* @return TokenEnhancer
*/
@
Bean
public TokenEnhancer tokenEnhancer() {
return (accessToken, authentication) - > {
try {
if (GlobalConstants.OAUTH_CLIENT_CREDENTIALS
.equals(authentication.getOAuth2Request().getGrantType())) {
return accessToken;
}
// 通过MAP 存储附加的信息
final Map < String, Object > additionalInfo = new
HashMap < > (16);
OAuthTradeUser authTradeUser = (OAuthTradeUser)
authentication.getUserAuthentication().getPrincipal();
if (null != authTradeUser) {
TradeUser tradeUser = authTradeUser.getTradeUser();
// 需要扩充增加的用户附带信息
additionalInfo.put(GlobalConstants.OAUTH_DETAILS_USER_ID,
tradeUser.getId());
additionalInfo.put(GlobalConstants.OAUTH_DETAILS_USERNAME,
tradeUser.getName());
additionalInfo.put(GlobalConstants.OAUTH_DETAILS_LOGIN_INFO,
tradeUser.getEmail() + "|" + tradeUser.getAddress());
additionalInfo.put("active", true);
}
// 将附加的信息记录保存, 形成增强的TOKEN
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return accessToken;
};
}
用户信息服务接口AuthStockUserDetailServiceImpl
import com.itcast.bulls.stock.trade.oauth.repository.TradeUserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service("authStockUserDetailService")
public class AuthStockUserDetailServiceImpl implements UserDetailsService {
/**
* 用户的数据层接口
*/
@Autowired
private TradeUserRepository tradeUserRepository;
/**
* 缓存管理接口
*/
@Autowired
private CacheManager cacheManager;
/**
* 根据用户账号获取用户对象接口
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String userNo) throws UsernameNotFoundException {
// 1. 从缓存中查找用户对象
Cache cache = cacheManager.getCache(GlobalConstants.OAUTH_KEY_STOCK_USER_DETAILS);
if(null != cache && null != cache.get(userNo)) {
return (UserDetails)cache.get(userNo).get();
}
// 2. 如果缓存未找到, 查询数据库
TradeUser tradeUser = tradeUserRepository.findByUserNo(userNo);
if(null == tradeUser) {
throw new UsernameNotFoundException(userNo + " not valid! ");
}
// 3. 对用户信息做封装处理
UserDetails userDetails = new OAuthTradeUser(tradeUser);
// 4. 将封装的用户信息放入到缓存当中
cache.put(userNo, userDetails);
return userDetails;
}
}
这是 Spring Security 提供的用户信息接口, 采用 OAUTH 的密码模式, 需要实现该接口的 loadUserByUsername 方法,为提升性能, 这里我们加入了 Spring Cache 缓存处理。
自定义用户信息: OAuthTradeUser
public class OAuthTradeUser extends User {
private static final long serialVersionUUID = -1L;
/**
* 业务用户信息
*/
private TradeUser tradeUser;
public OAuthTradeUser(TradeUser tradeUser) {
// OAUTH2认证用户信息构造处理
super(tradeUser.getUserNo(), tradeUser.getUserPwd(), (tradeUser.getStatus() == 0 ? true : false),
true, true, (tradeUser.getStatus() == 0 ? true : false), Collections.emptyList());
this.tradeUser = tradeUser;
}
public TradeUser getTradeUser() {
return tradeUser;
}
}
客户端信息服务接口AuthClientDetailService
public class AuthClientDetailService extends JdbcClientDetailsService {
public AuthClientDetailService(DataSource dataSource) {
super(dataSource);
}
/**
* 重写原生方法支持redis缓存
*
* @param clientId
* @return
* @throws InvalidClientException
*/
@Override
@Cacheable(value = GlobalConstants.OAUTH_KEY_CLIENT_DETAILS, key = "#clientId", unless = "#result == null")
public ClientDetails loadClientByClientId(String clientId) {
return super.loadClientByClientId(clientId);
}
}
这是 OAUTH 内置的客户端信息, 重新它是为了实现缓存, 减少数据库查询。
认证配置ResourceSecurityConfigurer
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurer;
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.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
@Primary
@Order(90)
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceSecurityConfigurer implements ResourceServerConfigurer {
@Autowired
private RemoteTokenServices remoteTokenServices;
@Autowired
private RestTemplate restTemplate;
/**
* 远程调用, 采用RestTemplate方式
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
remoteTokenServices.setRestTemplate(restTemplate);
resources.tokenServices(remoteTokenServices);
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("test123");
return converter;
}
/**
* 资源服务的安全配置
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/user/**").authenticated().and()
.formLogin().loginPage("/login")
.failureUrl("/login?error")
.defaultSuccessUrl("/home");
}
/**
* RestTemplate配置
* @return
*/
@Bean
@Primary
@LoadBalanced
public RestTemplate lbRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != HttpStatus.BAD_REQUEST.value()) {
super.handleError(response);
}
}
});
return restTemplate;
}
}
用户服务为资源服务, 认证采用 RestTemplate 调用方式。资源服务一定要开启@EnableResourceServer
注解, @EnableGlobalMethodSecurity
为方法级别安全控制。
提供获取用户增强信息接口StockUserController
import com.itcast.bulls.stock.common.exception.ComponentException;
import com.itcast.bulls.stock.entity.user.TradeUser;
import com.itcast.stock.common.web.vo.ApiRespResult;
import com.itcast.trade.bulls.stock.user.service.IStockUserService;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController()
@RequestMapping("/user")
@Log4j2
public class StockUserController extends BaseController{
@Autowired
private IStockUserService stockUserService;
/**
* 用户登陆接口
* @param userNo
* @param userPwd
* @return
*/
@RequestMapping("/userLogin")
public ApiRespResult userLogin(@RequestParam("userNo")String userNo, @RequestParam("userPwd") String userPwd) {
ApiRespResult result = null;
try {
// 用户登陆逻辑处理
TradeUser tradeUser = stockUserService.userLogin(userNo, userPwd);
result = ApiRespResult.success(tradeUser);
}catch(ComponentException e) {
log.error(e.getMessage(), e);
result = ApiRespResult.error(e.geterrorCodeEnum());
}catch(Exception e) {
log.error(e.getMessage(), e);
result = ApiRespResult.sysError(e.getMessage());
}
return result;
}
/**
* 获取用户JWT扩展信息
* @return
*/
@RequestMapping("/getJwtInfo")
public ApiRespResult getUserEnhancer() {
ApiRespResult result = null;
try {
// 获取用户JWT扩展信息
Map<String, Object> userAdditionalInfos = getUserAdditionalInfos();
result = ApiRespResult.success(userAdditionalInfos);
}catch(ComponentException e) {
log.error(e.getMessage(), e);
result = ApiRespResult.error(e.geterrorCodeEnum());
}catch(Exception e) {
log.error(e.getMessage(), e);
result = ApiRespResult.sysError(e.getMessage());
}
return result;
}
}
import com.itcast.bulls.stock.common.exception.constants.IErrorCodeEnum;
/**
* 自定义组件异常
*/
public class ComponentException extends AbstractException {
/**
*
*/
private static final long serialVersionUID = 2333790764399190094L;
/**
* 错误码枚举信息
*/
private IErrorCodeEnum errorCodeEnum;
/**
* 扩展的错误信息
*/
private String extendErrorMessage;
public ComponentException(IErrorCodeEnum errorCodeEnum) {
super(errorCodeEnum.getCode() + ":" + errorCodeEnum.getMessage());
this.errorCodeEnum = errorCodeEnum;
}
public ComponentException(IErrorCodeEnum errorCodeEnum, String extendErrorMessage) {
super(errorCodeEnum.getCode() + ":" + errorCodeEnum.getMessage() + "["
+ extendErrorMessage + "]");
this.errorCodeEnum = errorCodeEnum;
this.extendErrorMessage = extendErrorMessage;
}
public IErrorCodeEnum geterrorCodeEnum() {
return errorCodeEnum;
}
public void seterrorCodeEnum(IErrorCodeEnum errorCodeEnum) {
this.errorCodeEnum = errorCodeEnum;
}
public String getExtendErrorMessage() {
return extendErrorMessage;
}
public void setExtendErrorMessage(String extendErrorMessage) {
this.extendErrorMessage = extendErrorMessage;
}
}
全局过滤器StockRequestGlobalFilter
import io.netty.util.internal.StringUtil;
import lombok.extern.log4j.Log4j2;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
@Log4j2
public class StockRequestGlobalFilter implements GlobalFilter, Ordered {
/**
* 通过filter来自定义配置转发信息
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String authentication = exchange.getRequest().getHeaders().getFirst("Authorization");
if(!StringUtil.isNullOrEmpty(authentication)){
log.info("enter stockRequestGlobalFilter filter method: " + authentication);
exchange.getRequest().mutate().header("Authorization",authentication);
}
return chain.filter(exchange.mutate().build());
}
@Override
public int getOrder() {
return -1000;
}
}
这是自定义全局过滤器的实现, 防止 header 中的 Authorization 没有转发的问题
申请 Toke
POST http://127.0.0.1:10680/oauth/token?
grant_type=password&username=admin&password=admin&scope=server
Accept: */*
Cache-Control: no-cache
Authorization: Basic YXBwOmFwcA==
返回 Token 信息:
{
"access_token": "cc5c4c1d-b519-458f-b338-ad4bd1ec06b0",
"token_type": "bearer",
"refresh_token": "86fec4ff-6c24-4171-a257-bf2d4e6bc30c",
"expires_in": 29749,
"scope": "server",
"login_info": "hekun1@itcast.cn|null",
"user_id": 1,
"user_name": "admin",
"active": true
}
1234567891011
获取增强用户信息
GET 127.0.0.1:10680/user/getUserEnhancer
Accept: */*
Cache-Control: no-cache
Authorization: Bearer cc5c4c1d-b519-458f-b338-ad4bd1ec06b0
1234
返回增强的用户信息:
{
"code": "SYS_200",
"msg": "成功",
"extendData": null,
"data": {
"login_info": "hekun1@itcast.cn|null",
"user_id": 1,
"user_name": "admin",
"active": true
},
"success": true
}
123456789101112
整体实现流程:采用密码模式,基于 JWT 扩展信息的微服务应用实践方案:
认证服务系统配置AuthorizationServerConfig
/**
* 认证服务配置
* @param endpoints
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
// JWT信息增强配置,采用链式配置, 包含JWT签名配置与JWT扩展信息配置。
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> delegates = new ArrayList<>();
delegates.add(jwtTokenEnhancer());
delegates.add(accessTokenConverter());
enhancerChain.setTokenEnhancers(delegates);
endpoints
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
.tokenStore(tokenStore())
.userDetailsService(authStockUserDetailService)
.authenticationManager(authenticationManager)
.reuseRefreshTokens(false)
.tokenEnhancer(enhancerChain);
}
@Bean
public JwtTokenEnhancer jwtTokenEnhancer() {
return new JwtTokenEnhancer();
}
/**
* TokenStore实现方式, 采用Redis缓存
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("test123");
return converter;
}
12345678910111213141516171819202122232425262728293031323334353637383940414243
认证服务采用 JWT 方式配置,JWT 配置,采用链式配置, 包含 JWT 签名配置与 JWT 扩展信息,JWT 签名设为 test123。这里采用自定义的增强 JWT 作实现。
JwtTokenEnhancer
public class JwtTokenEnhancer implements TokenEnhancer {
/**
* JWT扩展存储用户信息
* @param accessToken
* @param authentication
* @return
*/
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, Object> additionalInfo = new HashMap<>();
OAuthTradeUser authTradeUser = (OAuthTradeUser) authentication.getUserAuthentication().getPrincipal();
if(null != authTradeUser) {
TradeUser tradeUser = authTradeUser.getTradeUser();
// 存储用户扩展信息
additionalInfo.put(GlobalConstants.OAUTH_DETAILS_USER_ID, tradeUser.getId());
additionalInfo.put(GlobalConstants.OAUTH_DETAILS_USERNAME, tradeUser.getName());
additionalInfo.put(GlobalConstants.OAUTH_DETAILS_LOGIN_INFO, tradeUser.getEmail() + "|" + tradeUser.getAddress());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
}
return accessToken;
}
}
123456789101112131415161718192021222324
在 JWT 存储扩展用户信息,可以根据需要扩展不同的信息,但长度要有限制。
认证配置ResourceSecurityConfigurer
@Primary
@Order(90)
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceSecurityConfigurer implements ResourceServerConfigurer {
@Autowired
private RemoteTokenServices remoteTokenServices;
@Autowired
private RestTemplate restTemplate;
/**
* 远程调用, 采用RestTemplate方式
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
remoteTokenServices.setRestTemplate(restTemplate);
resources.tokenServices(remoteTokenServices);
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("test123");
return converter;
}
/**
* 资源服务的安全配置
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/user/**").authenticated().and()
.formLogin().loginPage("/login")
.failureUrl("/login?error")
.defaultSuccessUrl("/home");
}
/**
* RestTemplate配置
* @return
*/
@Bean
@Primary
@LoadBalanced
public RestTemplate lbRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
if (response.getRawStatusCode() != HttpStatus.BAD_REQUEST.value()) {
super.handleError(response);
}
}
});
return restTemplate;
}
}
修改认证配置,采用 JWT 方式,设置签名为 test123,这里要和认证服务里面的签名保持一致,否则不能正常解析 JWT 信息。
StockUserController
/**
* 获取用户JWT扩展信息
* @return
*/
@RequestMapping("/getJwtInfo")
public ApiRespResult getUserEnhancer() {
ApiRespResult result = null;
try {
// 获取用户JWT扩展信息
Map<String, Object> userAdditionalInfos = getUserAdditionalInfos();
result = ApiRespResult.success(userAdditionalInfos);
}catch(ComponentException e) {
log.error(e.getMessage(), e);
result = ApiRespResult.error(e.geterrorCodeEnum());
}catch(Exception e) {
log.error(e.getMessage(), e);
result = ApiRespResult.sysError(e.getMessage());
}
return result;
}
自定义解析 JWT 数据增加依赖:
<!-- JWT TOKEN 组件 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
protected String getJwtToken() {
// 1. 获取Request对象
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 2. 获取token信息
String token = request.getHeader("Authorization");
if(null != token) {
token = token.replace(OAuth2AccessToken.BEARER_TYPE, "").trim();
}
return Jwts.parser()
.setSigningKey("test123".getBytes(StandardCharsets.UTF_8))
.parseClaimsJws(token)
.getBody().toString();
}
申请 Token
POST http://127.0.0.1:10680/oauth/token?
grant_type=password&username=admin&password=admin&scope=server
Accept: */*
Cache-Control: no-cache
Authorization: Basic YXBwOmFwcA==
返回 Token 信息:
{
"access_token":
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbl9pbmZvIjoiaGVrdW4xQGl0Y
2FzdC5jbnxudWxsIiwidXNlcl9pZCI6MSwidXNlcl9uYW1lIjoiYWRtaW4iLCJzY29wZSI6
WyJzZXJ2ZXIiXSwiZXhwIjoxNTk0ODQ5NTg1LCJqdGkiOiI2OWI4MWQzMi05MTk2LTQ5YmI
tOTU3ZC05YmRlZDM2OTY3ZTAiLCJjbGllbnRfaWQiOiJhcHAifQ.bFBKhPf0IYnJ9dpZG4a
PIlmpLECYwK-jTYTPHd2fc_M",
"token_type": "bearer",
"refresh_token":
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbl9pbmZvIjoiaGVrdW4xQGl0Y
2FzdC5jbnxudWxsIiwidXNlcl9pZCI6MSwidXNlcl9uYW1lIjoiYWRtaW4iLCJzY29wZSI6
WyJzZXJ2ZXIiXSwiYXRpIjoiNjliODFkMzItOTE5Ni00OWJiLTk1N2QtOWJkZWQzNjk2N2U
wIiwiZXhwIjoxNTk3Mzk4Mzg1LCJqdGkiOiIyMjhkMmIyZS02YmRkLTQ1NzktYTljNy03ZG
I0NmZmMjA3ZjkiLCJjbGllbnRfaWQiOiJhcHAifQ.yHD0U1WtOH_SAGev3mPwD1L1_XucWv
tRpTT-upHNqTM",
"expires_in": 43199,
"scope": "server",
"login_info": "hekun1@itcast.cn|null",
"user_id": 1,
"user_name": "admin",
"jti": "69b81d32-9196-49bb-957d-9bded36967e0"
}
获取 JWT 扩展用户信息
GET 127.0.0.1:10680/user/getJwtInfo
Accept: */*
Cache-Control: no-cache
Authorization: Bearer
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbl9pbmZvIjoiaGVrdW4xQGl0Y2
FzdC5jbnxudWxsIiwidXNlcl9pZCI6MSwidXNlcl9uYW1lIjoiYWRtaW4iLCJzY29wZSI6W
yJzZXJ2ZXIiXSwiZXhwIjoxNTk0ODQ5NTg1LCJqdGkiOiI2OWI4MWQzMi05MTk2LTQ5YmIt
OTU3ZC05YmRlZDM2OTY3ZTAiLCJjbGllbnRfaWQiOiJhcHAifQ.bFBKhPf0IYnJ9dpZG4aP
IlmpLECYwK-jTYTPHd2fc_M
返回 JWT 扩展用户信息:
{
"code": "SYS_200",
"msg": "成功",
"extendData": null,
"data": {
"login_info": "hekun1@itcast.cn|null",
"user_id": 1,
"user_name": "admin",
"jti": "69b81d32-9196-49bb-957d-9bded36967e0"
},
"success": true
}