Shiro同時(shí)支持Session和JWT Token兩種認(rèn)證方式

由于手機(jī)端不能存cookie,所以傳統(tǒng)的session存儲(chǔ)登錄信息的登錄方式(后面簡(jiǎn)稱session登錄)不能用,所以需要一個(gè)既支持session登錄后訪問有訪問權(quán)限控制的url又支持無狀態(tài)化token方式的認(rèn)證。對(duì)于無狀態(tài)話的token認(rèn)證,目前比較流行的是JWT token。關(guān)于JWT Token的介紹請(qǐng)自行查閱網(wǎng)上資料。由于我們使用的Shiro認(rèn)證授權(quán)框架,Shiro默認(rèn)實(shí)現(xiàn)的是基于Session的認(rèn)證和授權(quán),為了實(shí)現(xiàn)同時(shí)支持Session和JWT Token兩種認(rèn)證方式,需要在了解Shiro認(rèn)證授權(quán)框架的集成上 實(shí)現(xiàn)JWT token的訪問控制邏輯。

1. 認(rèn)證流程

針對(duì)用戶需求和安全需求,需要實(shí)現(xiàn)以下幾種場(chǎng)景的認(rèn)證。

  • 基于瀏覽器的Session認(rèn)證方式,需要實(shí)現(xiàn)多個(gè)web應(yīng)用之間的SSO。
  • 移動(dòng)端基于JWT Token的無狀態(tài)認(rèn)證,需要考慮token的足夠安全和token的自動(dòng)刷新(因?yàn)橐苿?dòng)端不能因?yàn)閠oken的過期,而中斷應(yīng)用導(dǎo)致用戶體驗(yàn)差)
  • 由前端發(fā)起,后端微服務(wù)之間的調(diào)用,由于這種調(diào)用關(guān)系,微服務(wù)之間會(huì)進(jìn)行session的共享,可以通過cookie來實(shí)現(xiàn)SSO
  • 來自于內(nèi)部的一些服務(wù),比如定時(shí)的Point service,由于它無Session,因此對(duì)于這種服務(wù),系統(tǒng)會(huì)內(nèi)置一個(gè)系統(tǒng)用戶,再以JWT Token的方式進(jìn)行認(rèn)證


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

上面紅色連接線表示基于JWT Token的Mobile App認(rèn)證方式,藍(lán)色連線表示基于Session的登錄方式。其中內(nèi)部定時(shí)器或者服務(wù)也是基于JWT Token認(rèn)證方式,只是需要內(nèi)置一些系統(tǒng)用戶。

2. 實(shí)現(xiàn)步驟

2.1. Shiro默認(rèn)訪問步驟

場(chǎng)景一、訪問登錄請(qǐng)求

比如我們常見會(huì)定義一個(gè)/login的請(qǐng)求,接受用戶名和密碼參數(shù)(一般密碼都會(huì)加鹽hash)。對(duì)于這種請(qǐng)求,Shiro會(huì)執(zhí)行以下的兩步邏輯。

  • 在代碼里會(huì)寫到獲取Shiro的Subject,創(chuàng)建一個(gè)token,通常是UsernamePasswordToken,將請(qǐng)求參數(shù)的賬戶密碼填充進(jìn)去,然后調(diào)用subject.login(token)
  • 接下來到支持處理這個(gè)token的realm中調(diào)用 realm doGetAuthenticationInfo 鑒權(quán),鑒權(quán)后,session中就存有你的登錄信息了

場(chǎng)景二、訪問普通API

  • 到 Shiro 的 PathMatchingFilter preHandle 方法判斷一個(gè)請(qǐng)求的訪問權(quán)限是可以直接放行還是需要 Shiro 自己實(shí)現(xiàn)的AccessControlFilter 來處理訪問請(qǐng)求
  • 假設(shè)到了 AccessControlFilter 實(shí)現(xiàn)類,首先在 isAccessAllowed 判斷是否可以訪問,如果可以則直接放行訪問,如果不可以則到 onAccessDenied 方法處理,并繼續(xù)調(diào)用 realm doGetAuthorizationInfo 授權(quán)判斷是否有足夠的權(quán)限來訪問
  • 假設(shè)有足夠的權(quán)限的話就訪問到自己定義的 controller了

2.2. 支持JWT Token訪問

Shiro默認(rèn)支持的是Session認(rèn)證方式,為了支持JWT Token認(rèn)證方式,需要實(shí)現(xiàn) AccessControlFilter 來修改控制訪問的邏輯。需要完成的工作有以下方面:

要做的有下面幾方面

  • [自定義實(shí)現(xiàn)AccessControlFilter (JWTAuthcFilter)]
  • S[hiro的過濾鏈上添加自定義的]
  • [自定義realm(JWTShiroRealm][),不用賬戶密碼登錄鑒權(quán)(UsernamePasswordToken),而使用自定義的token(JWTToken]
  • [自定義一個(gè)token(TokenRealm),存儲(chǔ)參數(shù)和加密參數(shù)等]
  • 增加一個(gè)JWTTokenRefreshInterceptor來攔截請(qǐng)求,檢測(cè)是否需要刷新token

2.3. 實(shí)現(xiàn)詳情

具體見代碼,分別是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 {
        // 登錄狀態(tài)判斷
        log.info("onAccessDenied......");
        Subject subject = getSubject(request, response);
        if (subject.isAuthenticated()) {
            return true;
        }

        //從header或URL參數(shù)中查找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("認(rèn)證失敗:" + 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自動(dòng)刷新機(jī)制,已注冊(cè)TokenRefreshInterceptor");
        for (String urlPattern : patterns) {
            log.info("TokenRefreshInterceptor匹配URL規(guī)則:" + urlPattern);
            reg.addPathPatterns(urlPattern);
        }
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //允許訪問header中的與token相關(guān)屬性
        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("無效的請(qǐng)求");
        }
        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已經(jīng)過期,請(qǐng)重新登錄");
        }
        JWTPrincipal principal = new JWTPrincipal();
        principal.setAccount(user.getAccount());
        principal.setUserId(user.getUserId());
        principal.setExpiresAt(jwt.getExpiresAt().getTime());
        //這里實(shí)際上會(huì)將AuthenticationToken.getCredentials()與傳入的第二個(gè)參數(shù)credentials進(jìn)行比較
        //第一個(gè)參數(shù)是登錄成功后,可以通過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端和移動(dòng)端對(duì)密碼的統(tǒng)一,在web端使用的是通過JavaScript和Web Crypto API來實(shí)現(xiàn)對(duì)數(shù)據(jù)進(jìn)行端到端加密,因此移動(dòng)端同樣需要實(shí)現(xiàn)此加密算法。為了方便移動(dòng)端的開發(fā),使用Java封裝了這套加密庫,移動(dòng)端可以直接調(diào)用。

2.5. JWT Token刷新

accessToken 的有效期由兩個(gè)配置構(gòu)成,maxAliveMinute 和 maxIdleMinute,配置見下面的配置章節(jié)。maxAliveMinute 定義了 accessToken 的理論過期時(shí)間,而 maxIdleMinute 定義了 accessToken 的最大生存周期。 在用戶管理模塊中增加了 HandlerInterceptor 用來處理 Token 的自動(dòng)刷新問題,如果傳入的 Token 已經(jīng)超過 maxAliveMinute 設(shè)定的時(shí)間,但還沒有達(dá)到 maxIdleMinute 的限制,則會(huì)自動(dòng)刷新該用戶的 accessToken 并添加在 response header,客戶端如果在響應(yīng)頭中發(fā)現(xiàn)有新的 token 返回,說明當(dāng)前 token 即將失效,需要及時(shí)更新自身存儲(chǔ)的 token。這個(gè)機(jī)制實(shí)際是提供一個(gè)窗口期,讓客戶端安全的刷新 accessToken。

2.6. 系統(tǒng)配置

配置主要分為以下幾個(gè)部分:

2.6.1. Shiro session配置

shiro:
  retry: 5 # 重試次數(shù)   lock: 5 # 鎖定次數(shù)   lock-duration: 1 # 鎖定時(shí)長 min   disabled: false
  session:
    timeout: 1800000
    loginurl: /login

2.6.2. Shiro JWT配置

shiro:
  retry: 5 # 重試次數(shù)
  lock: 5 # 鎖定次數(shù)
  lock-duration: 1 # 鎖定時(shí)長 min
  disabled: false # A&A開關(guān)
  session:
    timeout: 1800000
  loginurl: /login
  jwt:
    maxAliveMinute: 1 # jwt token過期時(shí)間,單位minutes
  maxIdleMinute: 120 # Jwt token最大存活時(shí)間,單位minutes
  headerKeyOfToken: access_token # Jwt token的header key name
  accountAlias: account # Jwt token account key name
  enableAutoRefreshToken: true # 是否自動(dòng)刷新access token
  urlPattern: /api/v1.0/* # 需要刷新token的API Pattern

注意urlPattern,為了支持刷新token,定義了urlpattern,因此需要所有的服務(wù)都已a(bǔ)pi/v1.0作為前綴

2.7. 調(diào)用方式

2.7.1. web頁面基于session訪問

在web前端頁面訪問任一個(gè)API,都會(huì)跳轉(zhuǎn)到登錄頁面,輸入用戶名和密碼即可登錄。

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}"

寫在最后

由于涉及到公司的一些業(yè)務(wù)代碼,因此不方便保留在代碼中,因此,上述代碼不能編譯成功,主要是如何實(shí)現(xiàn)多認(rèn)證系統(tǒng)的一個(gè)思路,具體我也是參考下面的兩篇文章來實(shí)現(xiàn)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容