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

由于手機(jī)端不能存cookie,所以傳統(tǒng)的session存儲(chǔ)登錄信息的登錄方式(后面簡(jiǎn)稱session登錄)不能用冠息,所以需要一個(gè)既支持session登錄后訪問(wèn)有訪問(wèn)權(quán)限控制的url又支持無(wú)狀態(tài)化token方式的認(rèn)證。對(duì)于無(wú)狀態(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的訪問(wèn)控制邏輯。

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的無(wú)狀態(tài)認(rèn)證稽屏,需要考慮token的足夠安全和token的自動(dòng)刷新(因?yàn)橐苿?dòng)端不能因?yàn)閠oken的過(guò)期,而中斷應(yīng)用導(dǎo)致用戶體驗(yàn)差)
  • 由前端發(fā)起西乖,后端微服務(wù)之間的調(diào)用狐榔,由于這種調(diào)用關(guān)系,微服務(wù)之間會(huì)進(jìn)行session的共享获雕,可以通過(guò)cookie來(lái)實(shí)現(xiàn)SSO
  • 來(lái)自于內(nèi)部的一些服務(wù)薄腻,比如定時(shí)的Point service,由于它無(wú)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)訪問(wèn)步驟

場(chǎng)景一蜓斧、訪問(wèn)登錄請(qǐng)求

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

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

場(chǎng)景二弥搞、訪問(wèn)普通API

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

2.2. 支持JWT Token訪問(wèn)

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

要做的有下面幾方面

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

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

具體見(jià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) {
        //允許訪問(wèn)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("無(wú)效的請(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)過(guò)期蝗敢,請(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ù)是登錄成功后,可以通過(guò)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端使用的是通過(guò)JavaScript和Web Crypto API來(lái)實(shí)現(xiàn)對(duì)數(shù)據(jù)進(jìn)行端到端加密寿谴,因此移動(dòng)端同樣需要實(shí)現(xiàn)此加密算法。為了方便移動(dòng)端的開(kāi)發(fā)失受,使用Java封裝了這套加密庫(kù)讶泰,移動(dòng)端可以直接調(diào)用咏瑟。

2.5. JWT Token刷新

accessToken 的有效期由兩個(gè)配置構(gòu)成,maxAliveMinute 和 maxIdleMinute痪署,配置見(jiàn)下面的配置章節(jié)码泞。maxAliveMinute 定義了 accessToken 的理論過(guò)期時(shí)間,而 maxIdleMinute 定義了 accessToken 的最大生存周期狼犯。 在用戶管理模塊中增加了 HandlerInterceptor 用來(lái)處理 Token 的自動(dòng)刷新問(wèn)題余寥,如果傳入的 Token 已經(jīng)超過(guò) maxAliveMinute 設(shè)定的時(shí)間,但還沒(méi)有達(dá)到 maxIdleMinute 的限制悯森,則會(huì)自動(dòng)刷新該用戶的 accessToken 并添加在 response header宋舷,客戶端如果在響應(yīng)頭中發(fā)現(xiàn)有新的 token 返回,說(shuō)明當(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í)長(zhǎng) min   disabled: false
  session:
    timeout: 1800000
    loginurl: /login

2.6.2. Shiro JWT配置

shiro:
  retry: 5 # 重試次數(shù)
  lock: 5 # 鎖定次數(shù)
  lock-duration: 1 # 鎖定時(shí)長(zhǎng) min
  disabled: false # A&A開(kāi)關(guān)
  session:
    timeout: 1800000
  loginurl: /login
  jwt:
    maxAliveMinute: 1 # jwt token過(guò)期時(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頁(yè)面基于session訪問(wèn)

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

2.7.2. Mobile基于JWT Token訪問(wèn)

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

寫(xiě)在最后

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末兄世,一起剝皮案震驚了整個(gè)濱河市啼辣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌御滩,老刑警劉巖鸥拧,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異削解,居然都是意外死亡富弦,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)氛驮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)腕柜,“玉大人,你說(shuō)我怎么就攤上這事≌电停” “怎么了砰蠢?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)蛾找。 經(jīng)常有香客問(wèn)我娩脾,道長(zhǎng)赵誓,這世上最難降的妖魔是什么打毛? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮俩功,結(jié)果婚禮上幻枉,老公的妹妹穿的比我還像新娘。我一直安慰自己诡蜓,他們只是感情好熬甫,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著蔓罚,像睡著了一般椿肩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上豺谈,一...
    開(kāi)封第一講書(shū)人閱讀 51,679評(píng)論 1 305
  • 那天郑象,我揣著相機(jī)與錄音,去河邊找鬼茬末。 笑死厂榛,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的丽惭。 我是一名探鬼主播击奶,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼责掏!你這毒婦竟也來(lái)了柜砾?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤换衬,失蹤者是張志新(化名)和其女友劉穎痰驱,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體冗疮,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡萄唇,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了术幔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片另萤。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出四敞,到底是詐尸還是另有隱情泛源,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布忿危,位于F島的核電站达箍,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏铺厨。R本人自食惡果不足惜缎玫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望解滓。 院中可真熱鬧赃磨,春花似錦、人聲如沸洼裤。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)腮鞍。三九已至值骇,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間移国,已是汗流浹背吱瘩。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留桥狡,地道東北人搅裙。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像裹芝,于是被迫代替她去往敵國(guó)和親部逮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容