spring-boot-plus集成Shiro+JWT權限管理

SpringBoot+Shiro+JWT權限管理

Shiro

  • Apache Shiro是一個強大且易用的Java安全框架,執行身份驗證、受權、密碼和會話管理。
  • 使用Shiro的易於理解的API,您能夠快速、輕鬆地得到任何應用程序,從最小的移動應用程序到最大的網絡和企業應用程序。

三個核心組件:Subject, SecurityManagerRealms.html

  • Subject表明了當前用戶的安全操做,即「當前操做用戶」。java

  • SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro經過SecurityManager來管理內部組件實例,並經過它來提供安全管理的各類服務。git

  • Realm: Realm充當了Shiro與應用安全數據間的「橋樑」或者「鏈接器」。也就是說,當對用戶執行認證(登陸)和受權(訪問控制)驗證時,Shiro會從應用配置的Realm中查找用戶及其權限信息。github

  • ShiroBasicArchitecture web

    ShiroBasicArchitecture

  • ShiroArchitecture redis

    ShiroArchitecture

JWT

  • JSON Web Token(JWT)是目前最流行的跨域身份驗證解決方案
  • JSON Web令牌是一種開放的行業標準 RFC 7519方法,用於在雙方之間安全地表示聲明。

JWT 數據結構

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJodHRwczovL3NwcmluZ2Jvb3QucGx1cyIsIm5hbWUiOiJzcHJpbmctYm9vdC1wbHVzIiwiaWF0IjoxNTE2MjM5MDIyfQ.1Cm7Ej8oIy1P5pkpu8-Q0B7bTU254I1og-ZukEe84II
複製代碼

jwt

JWT有三部分組成:Header:頭部,Payload:負載,Signature:簽名spring

SpringBoot+Shiro+JWT

pom.xml Shiro依賴

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-starter</artifactId>
    <version>1.4.1</version>
</dependency>
複製代碼

pom.xml JWT依賴

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.8.3</version>
</dependency>
複製代碼

ShiroConfig.java配置

@Slf4j
@Configuration
public class ShiroConfig {

    /** * JWT過濾器名稱 */
    private static final String JWT_FILTER_NAME = "jwtFilter";
    /** * Shiro過濾器名稱 */
    private static final String SHIRO_FILTER_NAME = "shiroFilter";


    @Bean
    public CredentialsMatcher credentialsMatcher() {
        return new JwtCredentialsMatcher();
    }

    /** * JWT數據源驗證 * * @return */
    @Bean
    public JwtRealm jwtRealm(LoginRedisService loginRedisService) {
        JwtRealm jwtRealm = new JwtRealm(loginRedisService);
        jwtRealm.setCachingEnabled(false);
        jwtRealm.setCredentialsMatcher(credentialsMatcher());
        return jwtRealm;
    }

    /** * 禁用session * * @return */
    @Bean
    public DefaultSessionManager sessionManager() {
        DefaultSessionManager manager = new DefaultSessionManager();
        manager.setSessionValidationSchedulerEnabled(false);
        return manager;
    }

    @Bean
    public SessionStorageEvaluator sessionStorageEvaluator() {
        DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        return sessionStorageEvaluator;
    }

    @Bean
    public DefaultSubjectDAO subjectDAO() {
        DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
        defaultSubjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator());
        return defaultSubjectDAO;
    }

    /** * 安全管理器配置 * * @return */
    @Bean
    public DefaultWebSecurityManager securityManager(LoginRedisService loginRedisService) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(jwtRealm(loginRedisService));
        securityManager.setSubjectDAO(subjectDAO());
        securityManager.setSessionManager(sessionManager());
        SecurityUtils.setSecurityManager(securityManager);
        return securityManager;
    }

    /** * ShiroFilterFactoryBean配置 * * @param securityManager * @param loginRedisService * @param shiroProperties * @param jwtProperties * @return */
    @Bean(SHIRO_FILTER_NAME)
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager, LoginService loginService, LoginRedisService loginRedisService, ShiroProperties shiroProperties, JwtProperties jwtProperties) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, Filter> filterMap = new HashedMap();
        filterMap.put(JWT_FILTER_NAME, new JwtFilter(loginService, loginRedisService, jwtProperties));
        shiroFilterFactoryBean.setFilters(filterMap);
        Map<String, String> filterChainMap = shiroFilterChainDefinition(shiroProperties).getFilterChainMap();
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
        return shiroFilterFactoryBean;
    }

    /** * Shiro路徑權限配置 * * @return */
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition(ShiroProperties shiroProperties) {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        // 獲取ini格式配置
        String definitions = shiroProperties.getFilterChainDefinitions();
        if (StringUtils.isNotBlank(definitions)) {
            Map<String, String> section = IniUtil.parseIni(definitions);
            log.debug("definitions:{}", JSON.toJSONString(section));
            for (Map.Entry<String, String> entry : section.entrySet()) {
                chainDefinition.addPathDefinition(entry.getKey(), entry.getValue());
            }
        }

        // 獲取自定義權限路徑配置集合
        List<ShiroPermissionConfig> permissionConfigs = shiroProperties.getPermissionConfig();
        log.debug("permissionConfigs:{}", JSON.toJSONString(permissionConfigs));
        if (CollectionUtils.isNotEmpty(permissionConfigs)) {
            for (ShiroPermissionConfig permissionConfig : permissionConfigs) {
                String url = permissionConfig.getUrl();
                String[] urls = permissionConfig.getUrls();
                String permission = permissionConfig.getPermission();
                if (StringUtils.isBlank(url) && ArrayUtils.isEmpty(urls)) {
                    throw new ShiroConfigException("shiro permission config 路徑配置不能爲空");
                }
                if (StringUtils.isBlank(permission)) {
                    throw new ShiroConfigException("shiro permission config permission不能爲空");
                }

                if (StringUtils.isNotBlank(url)) {
                    chainDefinition.addPathDefinition(url, permission);
                }
                if (ArrayUtils.isNotEmpty(urls)) {
                    for (String string : urls) {
                        chainDefinition.addPathDefinition(string, permission);
                    }
                }
            }
        }
        // 最後一個設置爲JWTFilter
        chainDefinition.addPathDefinition("/**", JWT_FILTER_NAME);

        Map<String, String> filterChainMap = chainDefinition.getFilterChainMap();
        log.debug("filterChainMap:{}", JSON.toJSONString(filterChainMap));

        return chainDefinition;
    }


    /** * ShiroFilter配置 * * @return */
    @Bean
    public FilterRegistrationBean delegatingFilterProxy() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        DelegatingFilterProxy proxy = new DelegatingFilterProxy();
        proxy.setTargetFilterLifecycle(true);
        proxy.setTargetBeanName(SHIRO_FILTER_NAME);
        filterRegistrationBean.setFilter(proxy);
        filterRegistrationBean.setAsyncSupported(true);
        filterRegistrationBean.setEnabled(true);
        filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
        return filterRegistrationBean;
    }

    @Bean
    public Authenticator authenticator(LoginRedisService loginRedisService) {
        ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
        authenticator.setRealms(Arrays.asList(jwtRealm(loginRedisService)));
        authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
        return authenticator;
    }


    /** * Enabling Shiro Annotations * * @return */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /** * depends-on lifecycleBeanPostProcessor * * @return */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

}
複製代碼

JWT過濾器配置

@Slf4j
public class JwtFilter extends AuthenticatingFilter {

    private LoginService loginService;

    private LoginRedisService loginRedisService;

    private JwtProperties jwtProperties;

    public JwtFilter(LoginService loginService, LoginRedisService loginRedisService, JwtProperties jwtProperties) {
        this.loginService = loginService;
        this.loginRedisService = loginRedisService;
        this.jwtProperties = jwtProperties;
    }

    /** * 將JWT Token包裝成AuthenticationToken * * @param servletRequest * @param servletResponse * @return * @throws Exception */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        String token = JwtTokenUtil.getToken();
        if (StringUtils.isBlank(token)) {
            throw new AuthenticationException("token不能爲空");
        }
        if (JwtUtil.isExpired(token)) {
            throw new AuthenticationException("JWT Token已過時,token:" + token);
        }

        // 若是開啓redis二次校驗,或者設置爲單個用戶token登錄,則先在redis中判斷token是否存在
        if (jwtProperties.isRedisCheck() || jwtProperties.isSingleLogin()) {
            boolean redisExpired = loginRedisService.exists(token);
            if (!redisExpired) {
                throw new AuthenticationException("Redis Token不存在,token:" + token);
            }
        }

        String username = JwtUtil.getUsername(token);
        String salt;
        if (jwtProperties.isSaltCheck()){
            salt = loginRedisService.getSalt(username);
        }else{
            salt = jwtProperties.getSecret();
        }
        return JwtToken.build(token, username, salt, jwtProperties.getExpireSecond());
    }

    /** * 訪問失敗處理 * * @param request * @param response * @return * @throws Exception */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        // 返回401
        httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        // 設置響應碼爲401或者直接輸出消息
        String url = httpServletRequest.getRequestURI();
        log.error("onAccessDenied url:{}", url);
        ApiResult apiResult = ApiResult.fail(ApiCode.UNAUTHORIZED);
        HttpServletResponseUtil.printJSON(httpServletResponse, apiResult);
        return false;
    }

    /** * 判斷是否容許訪問 * * @param request * @param response * @param mappedValue * @return */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        String url = WebUtils.toHttp(request).getRequestURI();
        log.debug("isAccessAllowed url:{}", url);
        if (this.isLoginRequest(request, response)) {
            return true;
        }
        boolean allowed = false;
        try {
            allowed = executeLogin(request, response);
        } catch (IllegalStateException e) { //not found any token
            log.error("Token不能爲空", e);
        } catch (Exception e) {
            log.error("訪問錯誤", e);
        }
        return allowed || super.isPermissive(mappedValue);
    }

    /** * 登錄成功處理 * * @param token * @param subject * @param request * @param response * @return * @throws Exception */
    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
        String url = WebUtils.toHttp(request).getRequestURI();
        log.debug("鑑權成功,token:{},url:{}", token, url);
        // 刷新token
        JwtToken jwtToken = (JwtToken) token;
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        loginService.refreshToken(jwtToken, httpServletResponse);
        return true;
    }

    /** * 登錄失敗處理 * * @param token * @param e * @param request * @param response * @return */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        log.error("登錄失敗,token:" + token + ",error:" + e.getMessage(), e);
        return false;
    }
}
複製代碼

JWT Realm配置

@Slf4j
public class JwtRealm extends AuthorizingRealm {

    private LoginRedisService loginRedisService;

    public JwtRealm(LoginRedisService loginRedisService) {
        this.loginRedisService = loginRedisService;
    }

    @Override
    public boolean supports(AuthenticationToken token) {
        return token != null && token instanceof JwtToken;
    }

    /** * 受權認證,設置角色/權限信息 * * @param principalCollection * @return */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        log.debug("doGetAuthorizationInfo principalCollection...");
        // 設置角色/權限信息
        String token = principalCollection.toString();
        // 獲取username
        String username = JwtUtil.getUsername(token);
        // 獲取登錄用戶角色權限信息
        LoginSysUserRedisVo loginSysUserRedisVo = loginRedisService.getLoginSysUserRedisVo(username);
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 設置角色
        authorizationInfo.setRoles(loginSysUserRedisVo.getRoles());
        // 設置權限
        authorizationInfo.setStringPermissions(loginSysUserRedisVo.getPermissions());
        return authorizationInfo;
    }

    /** * 登錄認證 * * @param authenticationToken * @return * @throws AuthenticationException */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.debug("doGetAuthenticationInfo authenticationToken...");
        // 校驗token
        JwtToken jwtToken = (JwtToken) authenticationToken;
        if (jwtToken == null) {
            throw new AuthenticationException("jwtToken不能爲空");
        }
        String salt = jwtToken.getSalt();
        if (StringUtils.isBlank(salt)) {
            throw new AuthenticationException("salt不能爲空");
        }
        return new SimpleAuthenticationInfo(
                jwtToken,
                salt,
                getName()
        );

    }

}
複製代碼

更多配置:github.com/geekidea/sp…

application.yml配置

############################## spring-boot-plus start ##############################
spring-boot-plus:
  ######################## Spring Shiro start ########################
 shiro:
    # shiro ini 多行字符串配置
 filter-chain-definitions: | /=anon /static/**=anon /templates/**=anon # 權限配置  permission-config:
        # 排除登錄登出相關
 - urls: /login,/logout
 permission: anon
        # 排除靜態資源
 - urls: /static/**,/templates/**
 permission: anon
        # 排除Swagger
 - urls: /docs,/swagger-ui.html, /webjars/springfox-swagger-ui/**,/swagger-resources/**,/v2/api-docs
 permission: anon
        # 排除SpringBootAdmin
 - urls: /,/favicon.ico,/actuator/**,/instances/**,/assets/**,/sba-settings.js,/applications/**
 permission: anon
        # 測試
 - url: /sysUser/getPageList
 permission: anon
  ######################## Spring Shiro end ##########################

  ############################ JWT start #############################
 jwt:
 token-name: token
 secret: 666666
 issuer: spring-boot-plus
 audience: web
    # 默認過時時間1小時,單位:秒
 expire-second: 3600
    # 是否刷新token
 refresh-token: true
    # 刷新token的時間間隔,默認10分鐘,單位:秒
 refresh-token-countdown: 600
    # redis校驗jwt token是否存在,可選
 redis-check: true
    # true: 同一個帳號只能是最後一次登錄token有效,false:同一個帳號可屢次登錄
 single-login: false
    # 鹽值校驗,若是不加自定義鹽值,則使用secret校驗
 salt-check: true
  ############################ JWT end ###############################

############################### spring-boot-plus end ###############################
複製代碼

Redis存儲信息

使用Redis緩存JWTToken和鹽值:方便鑑權,token後臺過時控制等apache

  • Redis二次校驗和鹽值校驗是可選的
127.0.0.1:6379> keys *
1) "login:user:token:admin:0f2c5d670f9f5b00201c78293304b5b5"
2) "login:salt:admin"
3) "login:user:admin"
4) "login:token:0f2c5d670f9f5b00201c78293304b5b5"
複製代碼
  • Redis存儲的JwtToken信息
127.0.0.1:6379> get login:token:0f2c5d670f9f5b00201c78293304b5b5
複製代碼
{
  "@class": "io.geekidea.springbootplus.shiro.vo.JwtTokenRedisVo",
  "host": "127.0.0.1",
  "username": "admin",
  "salt": "f80b2eed0110a7ea5a94c35cbea1fe003d9bb450803473428b74862cceb697f8",
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ3ZWIiLCJpc3MiOiJzcHJpbmctYm9vdC1wbHVzIiwiZXhwIjoxNTcwMzU3ODY1LCJpYXQiOjE1NzAzNTQyNjUsImp0aSI6IjE2MWQ1MDQxZmUwZjRmYTBhOThjYmQ0ZjRlNDI1ZGQ3IiwidXNlcm5hbWUiOiJhZG1pbiJ9.0ExWSiniq7ThMXfqCOi9pCdonY8D1azeu78_vLNa2v0",
  "createDate": [
    "java.util.Date",
    1570354265000
  ],
  "expireSecond": 3600,
  "expireDate": [
    "java.util.Date",
    1570357865000
  ]
}
複製代碼

Reference

Shirojson

JWTapi

spring-boot-plus

相關文章
相關標籤/搜索