Shiro同时支持Session和JWT Token两种认证方式

由于手机端不能存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的方式进行认证


    Screen Shot 2021-08-03 at 3.41.22 PM.png

上面红色连接线表示基于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等。

Screen Shot 2021-08-03 at 3.44.04 PM.png

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<String, Filter> filters = new HashMap();
        filters.put(GlobalConstant.JWT_AUTHC, jwtAuthcFilter());
        shiroFilterFactoryBean.setFilters(filters);

        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
        Map<String, String> 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<Realm> 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<String> perms = roleService.selectPermsByRole(emsUserInfo.getRoleId());
        Set<String> 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}"

写在最后

由于涉及到公司的一些业务代码,因此不方便保留在代码中,因此,上述代码不能编译成功,主要是如何实现多认证系统的一个思路,具体我也是参考下面的两篇文章来实现。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容