由于手机端不能存cookie,所以传统的session存储登录信息的登录方式(后面简称session登录)不能用,所以需要一个既支持session登录后访问有访问权限控制的url又支持无状态化token方式的认证。对于无状态话的token认证,目前比较流行的是JWT token。关于JWT Token的介绍请自行查阅网上资料。由于我们使用的Shiro认证授权框架,Shiro默认实现的是基于Session的认证和授权,为了实现同时支持Session和JWT Token两种认证方式,需要在了解Shiro认证授权框架的集成上 实现JWT token的访问控制逻辑。
1. 认证流程
针对用户需求和安全需求,需要实现以下几种场景的认证。
- 基于浏览器的Session认证方式,需要实现多个web应用之间的SSO。
- 移动端基于JWT Token的无状态认证,需要考虑token的足够安全和token的自动刷新(因为移动端不能因为token的过期,而中断应用导致用户体验差)
- 由前端发起,后端微服务之间的调用,由于这种调用关系,微服务之间会进行session的共享,可以通过cookie来实现SSO
-
来自于内部的一些服务,比如定时的Point service,由于它无Session,因此对于这种服务,系统会内置一个系统用户,再以JWT Token的方式进行认证
上面红色连接线表示基于JWT Token的Mobile App认证方式,蓝色连线表示基于Session的登录方式。其中内部定时器或者服务也是基于JWT Token认证方式,只是需要内置一些系统用户。
2. 实现步骤
2.1. Shiro默认访问步骤
场景一、访问登录请求
比如我们常见会定义一个/login的请求,接受用户名和密码参数(一般密码都会加盐hash)。对于这种请求,Shiro会执行以下的两步逻辑。
- 在代码里会写到获取Shiro的Subject,创建一个token,通常是UsernamePasswordToken,将请求参数的账户密码填充进去,然后调用subject.login(token)
- 接下来到支持处理这个token的realm中调用 realm doGetAuthenticationInfo 鉴权,鉴权后,session中就存有你的登录信息了
场景二、访问普通API
- 到 Shiro 的 PathMatchingFilter preHandle 方法判断一个请求的访问权限是可以直接放行还是需要 Shiro 自己实现的AccessControlFilter 来处理访问请求
- 假设到了 AccessControlFilter 实现类,首先在 isAccessAllowed 判断是否可以访问,如果可以则直接放行访问,如果不可以则到 onAccessDenied 方法处理,并继续调用 realm doGetAuthorizationInfo 授权判断是否有足够的权限来访问
- 假设有足够的权限的话就访问到自己定义的 controller了
2.2. 支持JWT Token访问
Shiro默认支持的是Session认证方式,为了支持JWT Token认证方式,需要实现 AccessControlFilter 来修改控制访问的逻辑。需要完成的工作有以下方面:
要做的有下面几方面
- [自定义实现AccessControlFilter (JWTAuthcFilter)]
- S[hiro的过滤链上添加自定义的]
- [自定义realm(JWTShiroRealm][),不用账户密码登录鉴权(UsernamePasswordToken),而使用自定义的token(JWTToken]
- [自定义一个token(TokenRealm),存储参数和加密参数等]
- 增加一个JWTTokenRefreshInterceptor来拦截请求,检测是否需要刷新token
2.3. 实现详情
具体见代码,分别是JWTAuthcFilter,JWTPrincipal,JWTTokenRefreshInterceptor,JWTWebMvcConfigurer,ShiroConfig,JWTToken等。
2.3.1 JWTAuthcFilter
import com.google.common.base.Strings;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@AllArgsConstructor
public class JWTAuthcFilter extends AccessControlFilter {
private final String headerKeyOfToken;
private final JWTUserAuthService userAuthService;
private final boolean isDisabled;
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if(isDisabled){
log.info("Shiro Authentication is disabled, hence can access api directly.");
return true;
}else{
log.info("Shiro Authentication is enabled, to continue to execute onAccessDenied method");
}
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
// 登录状态判断
log.info("onAccessDenied......");
Subject subject = getSubject(request, response);
if (subject.isAuthenticated()) {
return true;
}
//从header或URL参数中查找token
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader(headerKeyOfToken);
if (Strings.isNullOrEmpty(authorization)) {
authorization = req.getParameter(headerKeyOfToken);
}
JWTToken token = new JWTToken(authorization);
try {
getSubject(request, response).login(token);
} catch (Exception e) {
log.error("认证失败:" + e.getMessage());
this.userAuthService.onAuthenticationFailed((HttpServletRequest) request, (HttpServletResponse) response);
return false;
}
return true;
}
}
2.3.2 JWTPrincipal
import lombok.Data;
@Data
public class JWTPrincipal {
private String account;
private int userId;
private long expiresAt;
}
2.3.3 JWTWebMvcConfigurer
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Slf4j
@Configuration
@ConditionalOnProperty(prefix = "shiro.jwt", name = "enable-auto-refresh-token", havingValue = "true")
public class JWTWebMvcConfigurer implements WebMvcConfigurer {
@Autowired
private ShiroConfig shiroConfig;
@Autowired
private JWTUserAuthService userAuthService;
@Bean
@ConditionalOnProperty(prefix = "shiro.jwt", name = "enable-auto-refresh-token", havingValue = "true")
public JWTTokenRefreshInterceptor tokenRefreshInterceptor() {
return new JWTTokenRefreshInterceptor(userAuthService, shiroConfig.getHeaderKeyOfToken(),
shiroConfig.getMaxAliveMinute(), shiroConfig.getAccountAlias());
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
InterceptorRegistration reg = registry.addInterceptor(tokenRefreshInterceptor());
String[] patterns = shiroConfig.getUrlPattern().split(",");
log.info("启用token自动刷新机制,已注册TokenRefreshInterceptor");
for (String urlPattern : patterns) {
log.info("TokenRefreshInterceptor匹配URL规则:" + urlPattern);
reg.addPathPatterns(urlPattern);
}
}
@Override
public void addCorsMappings(CorsRegistry registry) {
//允许访问header中的与token相关属性
String[] urls = shiroConfig.getUrlPattern().split(",");
for (String url : urls) {
registry.addMapping(url).exposedHeaders(shiroConfig.getHeaderKeyOfToken());
}
}
}
2.3.4 ShiroConfig
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.StrUtil;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.pam.FirstSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.Cookie;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
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 javax.servlet.Filter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Configuration
@Slf4j
@Data
public class ShiroConfig {
@Value("${shiro.session.timeout:1800000}")
private Long sessionTimeout;
@Value("${shiro.retry}")
private Integer retryLimit;
@Value("${shiro.lock}")
private Integer lockLimit;
@Value("${shiro.disabled:false}")
private boolean isDisabled;
@Value("${shiro.lock-duration}")
private Long lockDuration;
@Value("${spring.application.name}")
private String name;
@Value("${server.servlet.session.cookie.http-only:true}")
private Boolean httpOnly;
@Value("${server.servlet.session.cookie.secure:false}")
private Boolean secure;
@Value("${shiro.loginurl:/platform-user-service/login}")
private String loginUrl;
@Value("${shiro.overwrite.loginurl:}")
private String overWriteLoginUrl;
@Value("${shiro.jwt.urlPattern:/*}")
private String urlPattern;
@Value("${shiro.jwt.maxAliveMinute:30}")
private int maxAliveMinute;
@Value("${shiro.jwt.maxIdleMinute:60}")
private int maxIdleMinute;
@Value("${shiro.jwt.headerKeyOfToken:access_token}")
private String headerKeyOfToken;
@Value("${shiro.jwt.accountAlias:account}")
private String accountAlias;
@Value("${shiro.jwt.enableAutoRefreshToken:false}")
private boolean enableAutoRefreshToken;
@Autowired
private JWTUserAuthService userAuthService;
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
log.info("overwrite login url {}", overWriteLoginUrl);
if(overWriteLoginUrl == null || overWriteLoginUrl.isEmpty()){
shiroFilterFactoryBean.setLoginUrl(loginUrl);
}else{
shiroFilterFactoryBean.setLoginUrl(overWriteLoginUrl);
}
Map filters = new HashMap();
filters.put(GlobalConstant.JWT_AUTHC, jwtAuthcFilter());
shiroFilterFactoryBean.setFilters(filters);
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
Map filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/images/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/plugins/**", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/token", "anon");
filterChainDefinitionMap.put("/api/v1.0/login", "anon");
filterChainDefinitionMap.put("/api/v1.0/token", "anon");
filterChainDefinitionMap.put("/api/v1.0/ping", "anon");
filterChainDefinitionMap.put("/api/v1.0/message", "anon");
filterChainDefinitionMap.put("/api/v1.0/user", GlobalConstant.JWT_AUTHC);
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setAuthenticator(modularRealmAuthenticator());
List realms = new ArrayList<>();
realms.add(jwtShiroRealm());
realms.add(shiroRealm());
defaultWebSecurityManager.setRealms(realms);
defaultWebSecurityManager.setSessionManager(getDefaultWebSessionManager());
//defaultWebSecurityManager.setRememberMeManager(cookieRememberMeManager());
defaultWebSecurityManager.setCacheManager(ehCacheManager());
return defaultWebSecurityManager;
}
private DefaultWebSessionManager getDefaultWebSessionManager() {
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
defaultWebSessionManager.setGlobalSessionTimeout(sessionTimeout);
defaultWebSessionManager.setSessionIdCookie(getSessionIdCookie());
defaultWebSessionManager.setSessionIdCookieEnabled(true);
defaultWebSessionManager.setCacheManager(ehCacheManager());
defaultWebSessionManager.setSessionDAO(sessionDAO());
return defaultWebSessionManager;
}
@Bean
public EhCacheManager ehCacheManager() {
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return ehCacheManager;
}
private SimpleCookie rememberMeCookie() {
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
simpleCookie.setHttpOnly(true);
simpleCookie.setMaxAge(2592000);
return simpleCookie;
}
private SimpleCookie getSessionIdCookie() {
SimpleCookie simpleCookie = new SimpleCookie(name);
simpleCookie.setHttpOnly(httpOnly);
simpleCookie.setMaxAge(1000 * 60);
simpleCookie.setPath(StrUtil.SLASH);
simpleCookie.setSameSite(Cookie.SameSiteOptions.LAX);
simpleCookie.setSecure(secure);
return simpleCookie;
}
/**
* Remember my manager
*
* @author FastKing
* @date 12:52 2018/9/28
**/
private CookieRememberMeManager cookieRememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
return cookieRememberMeManager;
}
@Bean
public SessionDAO sessionDAO() {
EnterpriseCacheSessionDAO cacheSessionDAO = new EnterpriseCacheSessionDAO();
cacheSessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache");
return cacheSessionDAO;
}
@Bean
public CredentialsMatcher retryLimitCredentialsMatcher() {
return new RetryLimitCredentialsMatcher(retryLimit, lockLimit, lockDuration);
}
@Bean
public JWTAuthcFilter jwtAuthcFilter() {
return new JWTAuthcFilter(GlobalConstant.HEADER_KEY_TOKEN, userAuthService, isDisabled);
}
@Bean
public ModularRealmAuthenticator modularRealmAuthenticator(){
ModularRealmAuthenticator modularRealmAuthenticator=new ModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
return modularRealmAuthenticator;
}
@Bean
public JWTShiroRealm jwtShiroRealm() {
JWTShiroRealm tokenRealm = new JWTShiroRealm(userAuthService, accountAlias, maxIdleMinute);
tokenRealm.setCachingEnabled(false);
return tokenRealm;
}
@Bean
public ShiroRealm shiroRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
shiroRealm.setCredentialsMatcher(retryLimitCredentialsMatcher());
return shiroRealm;
}
}
2.3.5 JWTShiroRealm
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
@AllArgsConstructor
@Slf4j
public class JWTShiroRealm extends AuthorizingRealm {
private final JWTUserAuthService userAuthService;
private final String accountAlias;
private final int maxIdleMinute;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
JWTPrincipal principal = (JWTPrincipal) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo authInfo = new SimpleAuthorizationInfo();
UserInfo up = userAuthService.getUserInfo(principal.getAccount());
if (up != null && up.getPermissions() != null) {
authInfo.addStringPermissions(up.getPermissions());
}
return authInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth){
String token = (String) auth.getCredentials();
String username = JWTHelper.getAccount(token, accountAlias);
if (username == null) {
throw new AuthenticationException("无效的请求");
}
UserInfo user = userAuthService.getUserInfo(username);
if (user == null) {
throw new AuthenticationException("未找到用户信息");
}
DecodedJWT jwt = JWTHelper.verify(token, user.getSecret(), maxIdleMinute);
if (jwt == null) {
throw new AuthenticationException("token已经过期,请重新登录");
}
JWTPrincipal principal = new JWTPrincipal();
principal.setAccount(user.getAccount());
principal.setUserId(user.getUserId());
principal.setExpiresAt(jwt.getExpiresAt().getTime());
//这里实际上会将AuthenticationToken.getCredentials()与传入的第二个参数credentials进行比较
//第一个参数是登录成功后,可以通过subject.getPrincipal获取
return new SimpleAuthenticationInfo(principal, token, this.getName());
}
}
2.3.6 ShiroRealm
import cn.hutool.core.text.CharSequenceUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import javax.annotation.Resource;
import java.util.Objects;
import java.util.Set;
@Slf4j
public class ShiroRealm extends AuthorizingRealm {
@Resource
private LoginService loginService;
@Resource
private RoleService roleService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
UserInfoVO emsUserInfo = (UserInfoVO) principals.getPrimaryPrincipal();
Set perms = roleService.selectPermsByRole(emsUserInfo.getRoleId());
Set roles = roleService.selectRoleCodeByRole(emsUserInfo.getRoleId());
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setStringPermissions(perms);
authorizationInfo.setRoles(roles);
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
String loginId = (String) authenticationToken.getPrincipal();
UserInfoVO emsUserInfo = loginService.getEmsUserInfo(loginId);
if (Objects.isNull(emsUserInfo)) {
emsUserInfo = new UserInfoVO();
emsUserInfo.setPassword(CharSequenceUtil.EMPTY);
}
return new SimpleAuthenticationInfo(emsUserInfo, emsUserInfo.getPassword(), this.getName());
}
@Override
public boolean isPermitted(PrincipalCollection principals, String permission) {
UserInfoVO emsUserInfo = (UserInfoVO) principals.getPrimaryPrincipal();
if (Objects.isNull(emsUserInfo.getRoleMenuInfo())) {
return false;
}
return emsUserInfo.getRoleMenuInfo().getIsAdmin() || super.isPermitted(principals, permission);
}
@Override
public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
UserInfoVO emsUserInfo = (UserInfoVO) principals.getPrimaryPrincipal();
if (Objects.isNull(emsUserInfo.getRoleMenuInfo())) {
return false;
}
return emsUserInfo.getRoleMenuInfo().getIsAdmin() || super.isPermitted(principals, roleIdentifier);
}
}
2.4. 密码加密
为了兼容web端和移动端对密码的统一,在web端使用的是通过JavaScript和Web Crypto API来实现对数据进行端到端加密,因此移动端同样需要实现此加密算法。为了方便移动端的开发,使用Java封装了这套加密库,移动端可以直接调用。
2.5. JWT Token刷新
accessToken 的有效期由两个配置构成,maxAliveMinute 和 maxIdleMinute,配置见下面的配置章节。maxAliveMinute 定义了 accessToken 的理论过期时间,而 maxIdleMinute 定义了 accessToken 的最大生存周期。 在用户管理模块中增加了 HandlerInterceptor 用来处理 Token 的自动刷新问题,如果传入的 Token 已经超过 maxAliveMinute 设定的时间,但还没有达到 maxIdleMinute 的限制,则会自动刷新该用户的 accessToken 并添加在 response header,客户端如果在响应头中发现有新的 token 返回,说明当前 token 即将失效,需要及时更新自身存储的 token。这个机制实际是提供一个窗口期,让客户端安全的刷新 accessToken。
2.6. 系统配置
配置主要分为以下几个部分:
2.6.1. Shiro session配置
shiro:
retry: 5 # 重试次数 lock: 5 # 锁定次数 lock-duration: 1 # 锁定时长 min disabled: false
session:
timeout: 1800000
loginurl: /login
2.6.2. Shiro JWT配置
shiro:
retry: 5 # 重试次数
lock: 5 # 锁定次数
lock-duration: 1 # 锁定时长 min
disabled: false # A&A开关
session:
timeout: 1800000
loginurl: /login
jwt:
maxAliveMinute: 1 # jwt token过期时间,单位minutes
maxIdleMinute: 120 # Jwt token最大存活时间,单位minutes
headerKeyOfToken: access_token # Jwt token的header key name
accountAlias: account # Jwt token account key name
enableAutoRefreshToken: true # 是否自动刷新access token
urlPattern: /api/v1.0/* # 需要刷新token的API Pattern
注意urlPattern,为了支持刷新token,定义了urlpattern,因此需要所有的服务都已api/v1.0作为前缀
2.7. 调用方式
2.7.1. web页面基于session访问
在web前端页面访问任一个API,都会跳转到登录页面,输入用户名和密码即可登录。
2.7.2. Mobile基于JWT Token访问
login
curl -X POST [http://localhost:50000/api/v1.0/token](http://localhost:50000/api/v1.0/token) -H "accept: application/json" -H "Content-Type: application/json" -d "{\"loginId\":\"admin\",\"password\":\"8SLGGbu7IYXVx4DJ.IGcMdlUQkaxDHG82fbCNCMC7LzWgex40qAFMnQ==\"}"
在access_token中返回jwt token如下:
login response
{
"code": 200,
"message": "操作成功",
"data": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2Mjc5Njg5MDUsImFjY291bnQiOiJhZG1pbiJ9.I5ToKyLKb22lxpo_LmA2mEHPXLMUUdmXm556LqRsHd0"
}
request api
curl -X POST [http://localhost:50000/api/v1.0/user](http://localhost:50000/api/v1.0/user) -H "accept: application/json" -H "Content-Type: application/json" -H "access_token:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2Mjc5Njg5MDUsImFjY291bnQiOiJhZG1pbiJ9.I5ToKyLKb22lxpo_LmA2mEHPXLMUUdmXm556LqRsHd0" -d "{\"userId\":8}"
写在最后
由于涉及到公司的一些业务代码,因此不方便保留在代码中,因此,上述代码不能编译成功,主要是如何实现多认证系统的一个思路,具体我也是参考下面的两篇文章来实现。
- https://github.com/t4Wang/shirostateless
- https://github.com/davidfantasy/shrio-with-jwt-spring-boot-starter/tree/master/src/main/java/com/github/davidfantasy/jwtshiro